refactor(toolkit): type source any with proper types

Genuinely type composable any usages (useStepper/useStorage/useForm/
createEventHook/useSorted/etc.) as proper generics/unknown; keep idiomatic
any-function and overload-impl signatures with comments; skipped test -> .todo.
This commit is contained in:
2026-06-15 16:55:07 +07:00
parent 44848bc9e6
commit aa2938cb34
283 changed files with 3505 additions and 3482 deletions
@@ -40,7 +40,7 @@ const stateColor = computed(() => {
case 'running': return 'bg-emerald-500';
case 'paused': return 'bg-amber-500';
case 'finished': return 'bg-sky-500';
default: return 'bg-(--border-strong)';
default: return 'bg-border-strong';
}
});
@@ -48,7 +48,7 @@ const rates = [0.5, 1, 2] as const;
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<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"
@@ -57,28 +57,28 @@ const rates = [0.5, 1, 2] as const;
</div>
<template v-else>
<div class="flex h-28 items-center justify-center overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset)">
<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"
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)">
<div class="rounded-lg border border-border bg-bg-inset p-3">
<div class="demo-label">
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>
<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)">
<div class="rounded-lg border border-border bg-bg-inset p-3">
<div class="demo-label">
Current time
</div>
<div class="mt-1 font-mono text-sm tabular-nums text-(--fg)">
<div class="mt-1 font-mono text-sm tabular-nums text-fg">
{{ elapsed }}
</div>
</div>
@@ -86,31 +86,31 @@ const rates = [0.5, 1, 2] as const;
<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"
class="demo-btn-primary"
@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"
class="demo-btn"
@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"
class="demo-btn"
@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"
class="demo-btn"
@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"
class="demo-btn col-span-2"
@click="cancel"
>
Cancel
@@ -118,7 +118,7 @@ const rates = [0.5, 1, 2] as const;
</div>
<div class="flex flex-col gap-2">
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-label">
Playback rate
</div>
<div class="flex gap-2">
@@ -127,8 +127,8 @@ const rates = [0.5, 1, 2] as const;
: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)'"
? '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 }}×
@@ -37,9 +37,9 @@ function toggle() {
</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)">
<div class="demo-stack max-w-sm">
<div class="demo-card p-5 text-center">
<div class="demo-label">
Time remaining
</div>
<div
@@ -48,22 +48,22 @@ function toggle() {
? 'text-emerald-600 dark:text-emerald-400'
: remaining <= 10 && remaining > 0
? 'text-amber-600 dark:text-amber-400'
: 'text-(--fg)'"
: 'text-fg'"
>
{{ minutes }}:{{ seconds }}
</div>
<div class="mt-4 h-1.5 w-full overflow-hidden rounded-full bg-(--bg-inset)">
<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"
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)">
<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)'"
:class="isActive ? 'bg-emerald-500' : justFinished ? 'bg-sky-500' : 'bg-border-strong'"
/>
{{ justFinished ? 'Completed' : isActive ? 'Counting down' : 'Paused' }}
</div>
@@ -73,7 +73,7 @@ function toggle() {
<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"
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` }}
@@ -82,20 +82,20 @@ function toggle() {
<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"
class="demo-btn-primary flex-1 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"
class="demo-btn"
@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"
class="demo-btn"
@click="stop"
>
Stop
@@ -27,46 +27,46 @@ 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)">
<div class="demo-stack max-w-md">
<div class="demo-card p-4">
<div class="demo-label">
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'"
: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)">
<label class="demo-label">
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)"
class="demo-input"
>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<label class="demo-label">
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)"
class="demo-input font-mono"
>
<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 }"
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 }}
@@ -75,7 +75,7 @@ const isValid = computed(() => formatted.value !== 'Invalid Date');
</div>
<div class="flex flex-col gap-1.5">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<label class="demo-label">
Locale
</label>
<div class="flex flex-wrap gap-2">
@@ -84,8 +84,8 @@ const isValid = computed(() => formatted.value !== 'Invalid Date');
: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)'"
? '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 }}
@@ -52,6 +52,7 @@ const REGEX_FORMAT
// `20240101`); JS lacks possessive quantifiers to disambiguate it.
// eslint-disable-next-line regexp/no-misleading-capturing-group
const REGEX_PARSE = /* #__PURE__ */ /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[T\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/i;
const REGEX_ISO_SUFFIX = /* #__PURE__ */ /z$/i;
const ORDINAL_SUFFIXES = ['th', 'st', 'nd', 'rd'] as const;
@@ -82,7 +83,7 @@ function formatOrdinal(num: number): string {
export function normalizeDate(date: DateLike): Date {
if (date === null || date === undefined) return new Date();
if (isDate(date)) return new Date(date.getTime());
if (isString(date) && !/z$/i.test(date)) {
if (isString(date) && !REGEX_ISO_SUFFIX.test(date)) {
const d = REGEX_PARSE.exec(date);
if (d) {
const month = d[2] ? Number(d[2]) - 1 : 0;
@@ -27,12 +27,12 @@ function toggle() {
</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)">
<div class="demo-stack max-w-sm">
<div class="demo-card p-5 text-center">
<div class="demo-label">
Ticks elapsed
</div>
<div class="mt-2 font-mono text-5xl font-bold tabular-nums text-(--fg)">
<div class="demo-stat mt-2 text-5xl">
{{ counter }}
</div>
@@ -41,21 +41,21 @@ function toggle() {
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)'"
:class="on ? 'bg-accent' : 'bg-bg-inset'"
/>
</div>
<div class="mt-4 flex items-center justify-center gap-2 text-xs text-(--fg-subtle)">
<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)'"
: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)">
<div class="demo-label">
Interval speed
</div>
<div class="flex gap-2">
@@ -64,8 +64,8 @@ function toggle() {
: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)'"
? '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 }}
@@ -75,13 +75,13 @@ function toggle() {
<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"
class="demo-btn-primary flex-1"
@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"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="counter === 0"
@click="reset"
>
@@ -31,22 +31,22 @@ function clear() {
</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 class="demo-stack max-w-sm">
<div class="demo-card flex items-center justify-between p-4">
<div>
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-label">
Interval callback
</div>
<div class="mt-1 flex items-center gap-2 text-sm text-(--fg-muted)">
<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)'"
: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"
class="demo-btn-primary"
@click="toggle"
>
{{ isActive ? 'Pause' : 'Start' }}
@@ -54,7 +54,7 @@ function clear() {
</div>
<div class="flex flex-col gap-1.5">
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-label">
Interval
</div>
<div class="flex gap-2">
@@ -63,24 +63,24 @@ function clear() {
: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)'"
? '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)">
<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)">
<div class="demo-label">
Tick log
</div>
<button
class="text-xs text-(--accent-text) transition hover:underline disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"
class="text-xs text-accent-text transition hover:underline disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"
:disabled="logs.length === 0"
@click="clear"
>
@@ -88,17 +88,17 @@ function 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)">
<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)"
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)" />
<span class="inline-block size-1.5 rounded-full bg-accent" />
{{ log.time }}
</li>
</ul>
@@ -106,14 +106,14 @@ function clear() {
<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"
class="demo-btn flex-1 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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!isActive"
@click="pause"
>
@@ -20,23 +20,23 @@ const secondAngle = computed(() => {
</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="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col items-center gap-3">
<div class="demo-label">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>
<span class="demo-stat text-3xl">{{ 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="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="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 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)"
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>
@@ -44,11 +44,11 @@ const secondAngle = computed(() => {
<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)"
class="demo-badge"
>
<span
class="size-1.5 rounded-full transition"
:class="isActive ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
:class="isActive ? 'bg-emerald-500' : 'bg-fg-subtle'"
/>
{{ isActive ? 'Ticking (RAF)' : 'Paused' }}
</span>
@@ -56,7 +56,7 @@ const secondAngle = computed(() => {
<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"
class="demo-btn"
@click="toggle"
>
{{ isActive ? 'Pause' : 'Resume' }}
@@ -64,7 +64,7 @@ const secondAngle = computed(() => {
<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"
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="resume"
>
Resume
@@ -72,7 +72,7 @@ const secondAngle = computed(() => {
<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"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="pause"
>
Pause
@@ -35,46 +35,46 @@ const limitLabel = computed(() => (fpsLimit.value === 0 ? 'Unlimited' : `${fpsLi
</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="demo-stack max-w-sm">
<div class="demo-card 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="demo-label">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)"
class="demo-badge"
>
<span class="size-1.5 rounded-full transition" :class="isActive ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
<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="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"
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 class="rounded-lg border border-border bg-bg-inset p-2 text-center">
<div class="demo-stat text-lg">{{ 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 class="rounded-lg border border-border bg-bg-inset p-2 text-center">
<div class="demo-stat text-lg">{{ 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 class="rounded-lg border border-border bg-bg-inset p-2 text-center">
<div class="demo-stat text-lg">{{ 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>
<label class="demo-label" for="fps-limit">FPS limit</label>
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ limitLabel }}</span>
</div>
<input
id="fps-limit"
@@ -83,14 +83,14 @@ const limitLabel = computed(() => (fpsLimit.value === 0 ? 'Unlimited' : `${fpsLi
min="0"
max="60"
step="5"
class="w-full accent-(--accent) cursor-pointer"
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>
<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"
class="demo-btn-primary"
@click="toggle"
>
{{ isActive ? 'Pause loop' : 'Resume loop' }}
@@ -42,15 +42,15 @@ const absolute = computed(() =>
</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 class="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col items-center gap-2">
<span class="demo-label">Relative time</span>
<span class="demo-stat text-3xl 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>
<span class="demo-label">Pick an instant</span>
<div class="grid grid-cols-2 gap-2">
<button
v-for="preset in presets"
@@ -58,8 +58,8 @@ const absolute = computed(() =>
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)'"
? '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 }}
@@ -69,14 +69,14 @@ const absolute = computed(() =>
<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)"
class="demo-badge"
>
<span class="size-1.5 rounded-full transition" :class="isActive ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
<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"
class="demo-btn"
@click="toggle"
>
{{ isActive ? 'Pause' : 'Resume' }}
@@ -165,10 +165,12 @@ const DEFAULT_UNITS: Array<UseTimeAgoUnit<UseTimeAgoUnitName>> = [
{ max: Number.POSITIVE_INFINITY, value: 31536000000, name: 'year' },
];
const REGEX_DIGIT = /* #__PURE__ */ /\d/;
const DEFAULT_MESSAGES: UseTimeAgoMessages<UseTimeAgoUnitName> = {
justNow: 'just now',
past: n => /\d/.test(n) ? `${n} ago` : n,
future: n => /\d/.test(n) ? `in ${n}` : n,
past: n => REGEX_DIGIT.test(n) ? `${n} ago` : n,
future: n => REGEX_DIGIT.test(n) ? `in ${n}` : n,
month: (n, past) => n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`,
year: (n, past) => n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`,
day: (n, past) => n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`,
@@ -24,21 +24,21 @@ function cancel() {
</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="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col items-center gap-3">
<span class="demo-label">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)'"
: '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>
<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>
@@ -46,8 +46,8 @@ function cancel() {
<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>
<label class="demo-label" 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"
@@ -56,14 +56,14 @@ function cancel() {
min="500"
max="5000"
step="500"
class="w-full accent-(--accent) cursor-pointer"
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"
class="demo-btn-primary flex-1"
@click="restart"
>
{{ ready ? 'Start' : 'Restart' }}
@@ -71,7 +71,7 @@ function cancel() {
<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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="cancel"
>
Cancel
@@ -42,23 +42,23 @@ function undo() {
<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>
<span class="demo-label">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="demo-card flex items-center justify-between gap-3 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 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"
class="demo-btn shrink-0 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="archive(mail)"
>
Archive
@@ -66,7 +66,7 @@ function undo() {
</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)">
<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>
@@ -21,7 +21,7 @@ export interface UseTimeoutFnOptions {
immediateCallback?: boolean;
}
export interface UseTimeoutFnReturn<Args extends any[]> {
export interface UseTimeoutFnReturn<Args extends unknown[]> {
/**
* Whether the timeout is currently pending
*/
@@ -45,33 +45,33 @@ function resetOffset() {
</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)">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4 text-center">
<div class="demo-label">
Reactive timestamp
</div>
<div class="mt-2 font-mono text-3xl font-bold tabular-nums text-(--fg)">
<div class="demo-stat mt-2 text-3xl">
{{ clockTime }}
</div>
<div class="mt-1 text-sm text-(--fg-muted)">
<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)">
<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)'"
: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">
<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"
class="demo-btn-primary"
@click="toggle"
>
{{ isActive ? 'Pause' : 'Resume' }}
@@ -79,13 +79,13 @@ function resetOffset() {
<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"
class="demo-btn"
@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"
class="demo-btn"
@click="shift(3600_000)"
>
+1h
@@ -93,13 +93,13 @@ function resetOffset() {
</div>
</div>
<div class="flex items-center justify-between text-sm text-(--fg-muted)">
<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 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"
class="text-accent-text transition hover:underline disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"
:disabled="offset === 0"
@click="resetOffset"
>
@@ -40,19 +40,19 @@ function randomize() {
<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="demo-card p-4">
<div class="flex items-baseline justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Eased value
</span>
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
<span class="demo-stat text-3xl">
{{ value.toFixed(1) }}
</span>
</div>
<div class="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-(--bg-inset)">
<div class="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-bg-inset">
<div
class="h-full rounded-full bg-(--accent)"
class="h-full rounded-full bg-accent"
:style="{ width: `${Math.max(0, Math.min(100, value))}%` }"
/>
</div>
@@ -63,10 +63,10 @@ function randomize() {
type="range"
min="0"
max="100"
class="h-1.5 flex-1 cursor-pointer accent-(--accent)"
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"
class="demo-btn"
@click="randomize"
>
Random
@@ -75,21 +75,21 @@ function randomize() {
</div>
<div class="flex flex-col gap-2">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<label class="demo-label">
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)"
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)">
<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>
<span class="font-mono text-fg tabular-nums">{{ duration }}ms</span>
</label>
<input
v-model.number="duration"
@@ -97,21 +97,21 @@ function randomize() {
min="100"
max="2000"
step="100"
class="h-1.5 w-full cursor-pointer accent-(--accent)"
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="demo-card p-4">
<div class="flex items-center gap-3">
<div
class="size-12 shrink-0 rounded-lg border border-(--border)"
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)">
<div class="demo-label">
Animated tuple
</div>
<div class="font-mono text-sm text-(--fg) tabular-nums">
<div class="font-mono text-sm text-fg tabular-nums">
{{ colorCss }}
</div>
</div>
@@ -121,7 +121,7 @@ function randomize() {
<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"
class="demo-badge transition hover:border-border-strong cursor-pointer"
@click="colorTarget = [...rgb]"
>
<span class="size-2.5 rounded-full" :style="{ backgroundColor: `rgb(${rgb.join(',')})` }" />
@@ -4,6 +4,7 @@ import { clamp, isFunction, isNumber, lerp, noop } from '@robonen/stdlib';
import { defaultWindow } from '@/types';
import type { ConfigurableWindow } from '@/types';
import { useRafFn } from '@/composables/animation/useRafFn';
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';
/**
* Cubic bezier control points `[x1, y1, x2, y2]` (the implied endpoints are
@@ -356,5 +357,10 @@ export function useTransition<T extends TransitionValue>(
},
);
// The RAF loop is torn down by useRafFn on scope dispose, but a pending start
// delay (window.setTimeout) is not — clear it so the timer can't fire into a
// disposed scope.
tryOnScopeDispose(clearDelay);
return computed(() => outputRef.value);
}
@@ -40,16 +40,16 @@ function toggle(track: Track) {
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Library tap to add / remove from playlist
</span>
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-(--fg-muted)">
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-fg-muted">
<input
v-model="symmetric"
type="checkbox"
class="size-4 cursor-pointer accent-(--accent)"
class="size-4 cursor-pointer accent-accent"
>
Symmetric
</label>
@@ -61,8 +61,8 @@ function toggle(track: Track) {
:key="track.id"
class="inline-flex items-center justify-between gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
:class="inPlaylist(track)
? '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)'"
? '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="toggle(track)"
>
<span class="truncate">{{ track.title }}</span>
@@ -70,12 +70,12 @@ function toggle(track: Track) {
</button>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="demo-card p-4">
<div class="flex items-baseline justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
{{ symmetric ? 'In exactly one (XOR)' : 'Not in playlist' }}
</span>
<span class="font-mono text-sm tabular-nums text-(--fg-muted)">
<span class="font-mono text-sm tabular-nums text-fg-muted">
{{ diff.length }}
</span>
</div>
@@ -84,12 +84,12 @@ function toggle(track: Track) {
<li
v-for="track in diff"
:key="track.id"
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)"
class="demo-badge"
>
{{ track.title }}
</li>
</ul>
<p v-else class="mt-3 text-sm text-(--fg-subtle)">
<p v-else class="mt-3 text-sm text-fg-subtle">
No difference every track matches.
</p>
</div>
@@ -1,6 +1,6 @@
import { computed, toValue } from 'vue';
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
import { isObject, isString } from '@robonen/stdlib';
import { isFunction, isNumber, isObject, isString, isSymbol } from '@robonen/stdlib';
/**
* Comparator deciding whether two array elements are considered equal.
@@ -24,7 +24,7 @@ export interface UseArrayDifferenceOptions<T> {
comparator?: UseArrayDifferenceComparatorFn<T> | keyof T;
}
export type UseArrayDifferenceReturn<T = any>
export type UseArrayDifferenceReturn<T = unknown>
= ComputedRef<T[]>;
function isArrayDifferenceOptions<T>(value: unknown): value is UseArrayDifferenceOptions<T> {
@@ -101,11 +101,11 @@ export function useArrayDifference<T>(
// Resolve the comparator once instead of rebuilding it on every recompute.
let compare: UseArrayDifferenceComparatorFn<T>;
if (isString(resolved) || typeof resolved === 'symbol' || typeof resolved === 'number') {
if (isString(resolved) || isSymbol(resolved) || isNumber(resolved)) {
const key = resolved as keyof T;
compare = (value, othVal) => value[key] === othVal[key];
}
else if (typeof resolved === 'function') {
else if (isFunction(resolved)) {
compare = resolved;
}
else {
@@ -30,22 +30,22 @@ function reset() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
class="rounded-xl border p-4 transition"
:class="allDone
? 'border-emerald-500/30 bg-emerald-500/10'
: 'border-(--border) bg-(--bg-elevated)'"
: 'border-border bg-bg-elevated'"
>
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Release readiness
</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="allDone
? 'border-emerald-500/30 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
<span
class="size-2 rounded-full"
@@ -54,7 +54,7 @@ function reset() {
{{ allDone ? 'Ready to ship' : 'Blocked' }}
</span>
</div>
<div class="mt-2 font-mono text-sm tabular-nums text-(--fg-muted)">
<div class="mt-2 font-mono text-sm tabular-nums text-fg-muted">
{{ completed }} / {{ checklist.length }} complete
</div>
</div>
@@ -62,18 +62,18 @@ function reset() {
<ul class="flex flex-col gap-2">
<li v-for="item in checklist" :key="item.id">
<button
class="flex w-full items-center gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2 text-left text-sm text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.99] cursor-pointer"
class="flex w-full items-center gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2 text-left text-sm text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.99] cursor-pointer"
@click="toggle(item)"
>
<span
class="flex size-5 shrink-0 items-center justify-center rounded-md border text-xs transition"
:class="item.done
? 'border-transparent bg-(--accent) text-(--accent-fg)'
: 'border-(--border-strong) text-transparent'"
? 'border-transparent bg-accent text-accent-fg'
: 'border-border-strong text-transparent'"
>
</span>
<span :class="item.done ? 'line-through text-(--fg-subtle)' : ''">
<span :class="item.done ? 'line-through text-fg-subtle' : ''">
{{ item.label }}
</span>
</button>
@@ -81,7 +81,7 @@ function reset() {
</ul>
<button
class="inline-flex items-center justify-center gap-1.5 self-start 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"
class="demo-btn self-start"
@click="reset"
>
Reset
@@ -35,18 +35,18 @@ const formatted = computed(() =>
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<div class="demo-stack max-w-md">
<input
v-model="query"
type="text"
placeholder="Search products…"
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)"
class="demo-input"
>
<div class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<label class="flex items-center justify-between text-sm text-(--fg-muted)">
<div class="demo-card flex flex-col gap-3 p-4">
<label class="flex items-center justify-between text-sm text-fg-muted">
<span>Max price</span>
<span class="font-mono text-(--fg) tabular-nums">${{ maxPrice }}</span>
<span class="font-mono text-fg tabular-nums">${{ maxPrice }}</span>
</label>
<input
v-model.number="maxPrice"
@@ -54,21 +54,21 @@ const formatted = computed(() =>
min="25"
max="400"
step="5"
class="h-1.5 w-full cursor-pointer accent-(--accent)"
class="h-1.5 w-full cursor-pointer accent-accent"
>
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-(--fg-muted)">
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-fg-muted">
<input
v-model="inStockOnly"
type="checkbox"
class="size-4 cursor-pointer accent-(--accent)"
class="size-4 cursor-pointer accent-accent"
>
In stock only
</label>
</div>
<div class="flex items-center justify-between text-xs">
<span class="font-medium uppercase tracking-wide text-(--fg-subtle)">Results</span>
<span class="font-mono tabular-nums text-(--fg-muted)">
<span class="font-medium uppercase tracking-wide text-fg-subtle">Results</span>
<span class="font-mono tabular-nums text-fg-muted">
{{ formatted.length }} / {{ products.length }}
</span>
</div>
@@ -77,10 +77,10 @@ const formatted = computed(() =>
<li
v-for="product in formatted"
:key="product.name"
class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
>
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-(--fg)">{{ product.name }}</span>
<span class="text-sm font-medium text-fg">{{ product.name }}</span>
<span
v-if="!product.inStock"
class="inline-flex items-center rounded-md border border-amber-500/30 bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium uppercase text-amber-600 dark:text-amber-400"
@@ -88,12 +88,12 @@ const formatted = computed(() =>
Out
</span>
</div>
<span class="font-mono text-sm tabular-nums text-(--fg-muted)">{{ product.priceLabel }}</span>
<span class="font-mono text-sm tabular-nums text-fg-muted">{{ product.priceLabel }}</span>
</li>
</ul>
<div
v-else
class="rounded-lg border border-dashed border-(--border) bg-(--bg-inset) px-3 py-6 text-center text-sm text-(--fg-subtle)"
class="rounded-lg border border-dashed border-border bg-bg-inset px-3 py-6 text-center text-sm text-fg-subtle"
>
No products match your filters.
</div>
@@ -35,13 +35,13 @@ const matchIndex = computed(() =>
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="space-y-3">
<div class="flex items-center justify-between">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="maxPrice">
<label class="demo-label" for="maxPrice">
Max price
</label>
<span class="font-mono text-sm tabular-nums text-(--fg)">${{ maxPrice }}</span>
<span class="font-mono text-sm tabular-nums text-fg">${{ maxPrice }}</span>
</div>
<input
id="maxPrice"
@@ -50,28 +50,28 @@ const matchIndex = computed(() =>
min="20"
max="400"
step="5"
class="w-full accent-(--accent)"
class="w-full accent-accent"
>
<label class="flex cursor-pointer items-center gap-2 text-sm text-(--fg-muted)">
<input v-model="inStockOnly" type="checkbox" class="accent-(--accent)">
<label class="flex cursor-pointer items-center gap-2 text-sm text-fg-muted">
<input v-model="inStockOnly" type="checkbox" class="accent-accent">
In stock only
</label>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<p class="mb-1 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label mb-1">
First match
</p>
<template v-if="firstMatch">
<div class="flex items-baseline justify-between">
<span class="text-sm font-medium text-(--fg)">{{ firstMatch.name }}</span>
<span class="font-mono text-sm tabular-nums text-(--fg)">${{ firstMatch.price }}</span>
<span class="text-sm font-medium text-fg">{{ firstMatch.name }}</span>
<span class="font-mono text-sm tabular-nums text-fg">${{ firstMatch.price }}</span>
</div>
<p class="mt-1 font-mono text-xs text-(--fg-subtle)">
<p class="mt-1 font-mono text-xs text-fg-subtle">
index {{ matchIndex }} · id {{ firstMatch.id }}
</p>
</template>
<p v-else class="text-sm text-(--fg-subtle)">
<p v-else class="text-sm text-fg-subtle">
No product matches the filters
</p>
</div>
@@ -82,8 +82,8 @@ const matchIndex = computed(() =>
:key="product.id"
class="flex items-center justify-between rounded-lg border px-3 py-2 text-sm transition"
:class="product.id === firstMatch?.id
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
: 'border-(--border) bg-(--bg-elevated) text-(--fg-muted)'"
? 'border-accent bg-accent-subtle text-accent-text'
: 'border-border bg-bg-elevated text-fg-muted'"
>
<span class="flex items-center gap-2">
{{ product.name }}
@@ -24,15 +24,15 @@ function toggle(index: number) {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-sm">
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label">
Next pending index
</p>
<p class="mt-1 font-mono text-3xl font-bold tabular-nums text-(--fg)">
<p class="demo-stat mt-1 text-3xl">
{{ nextIndex }}
</p>
<p class="mt-1 text-sm text-(--fg-subtle)">
<p class="mt-1 text-sm text-fg-subtle">
{{ nextIndex === -1 ? 'All steps complete' : `${steps[nextIndex]!.label}` }}
</p>
</div>
@@ -43,16 +43,16 @@ function toggle(index: number) {
:key="step.label"
class="flex items-center justify-between rounded-lg border px-3 py-2 text-sm transition"
:class="index === nextIndex
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
: 'border-(--border) bg-(--bg-elevated) text-(--fg-muted)'"
? 'border-accent bg-accent-subtle text-accent-text'
: 'border-border bg-bg-elevated text-fg-muted'"
>
<span class="flex items-center gap-2">
<span class="font-mono text-xs tabular-nums text-(--fg-subtle)">{{ index }}</span>
<span class="font-mono text-xs tabular-nums text-fg-subtle">{{ index }}</span>
<span :class="step.done ? 'line-through opacity-60' : ''">{{ step.label }}</span>
</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"
class="demo-btn"
@click="toggle(index)"
>
{{ step.done ? 'Undo' : 'Done' }}
@@ -42,7 +42,7 @@ const tone: Record<LogEntry['level'], string> = {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex gap-1.5">
<button
v-for="level in levels"
@@ -50,38 +50,38 @@ const tone: Record<LogEntry['level'], string> = {
type="button"
class="flex-1 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
:class="filter === level
? 'border-transparent bg-(--accent) text-(--accent-fg)'
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset)'"
? 'border-transparent bg-accent text-accent-fg'
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset'"
@click="filter = level"
>
{{ level }}
</button>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label">
Latest {{ filter }} entry
</p>
<template v-if="latest">
<p class="mt-1 font-mono text-sm text-(--fg)">{{ latest.message }}</p>
<p class="mt-1 font-mono text-xs text-(--fg-subtle)">#{{ latest.id }}</p>
<p class="mt-1 font-mono text-sm text-fg">{{ latest.message }}</p>
<p class="mt-1 font-mono text-xs text-fg-subtle">#{{ latest.id }}</p>
</template>
<p v-else class="mt-1 text-sm text-(--fg-subtle)">
<p v-else class="mt-1 text-sm text-fg-subtle">
No {{ filter }} entries yet
</p>
</div>
<ul class="flex max-h-44 flex-col gap-1 overflow-y-auto rounded-lg border border-(--border) bg-(--bg-elevated) p-2">
<ul class="flex max-h-44 flex-col gap-1 overflow-y-auto rounded-lg border border-border bg-bg-elevated p-2">
<li
v-for="entry in log"
:key="entry.id"
class="flex items-center gap-2 rounded-md px-2 py-1 font-mono text-xs transition"
:class="entry.id === latest?.id ? 'bg-(--accent-subtle)' : ''"
:class="entry.id === latest?.id ? 'bg-accent-subtle' : ''"
>
<span class="w-10 shrink-0 font-semibold uppercase" :class="tone[entry.level]">
{{ entry.level }}
</span>
<span class="truncate text-(--fg-muted)">{{ entry.message }}</span>
<span class="truncate text-fg-muted">{{ entry.message }}</span>
</li>
</ul>
@@ -90,7 +90,7 @@ const tone: Record<LogEntry['level'], string> = {
v-for="level in levels"
:key="level"
type="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"
class="demo-btn flex-1"
@click="append(level)"
>
+ {{ level }}
@@ -26,9 +26,9 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
</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">
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4">
<p class="demo-label mb-2">
Member by key
</p>
<div class="flex flex-wrap gap-1.5">
@@ -37,11 +37,11 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
:key="user.id"
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
:class="user.id === searchId
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
? 'border-accent bg-accent-subtle text-accent-text'
: 'border-border bg-bg-inset text-fg-muted'"
>
{{ user.name }}
<span class="font-mono text-(--fg-subtle)">#{{ user.id }}</span>
<span class="font-mono text-fg-subtle">#{{ user.id }}</span>
</span>
</div>
@@ -49,21 +49,21 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
<input
v-model.number="searchId"
type="number"
class="w-24 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)"
class="w-24 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"
>
<span
class="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium"
:class="isMember
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'bg-(--bg-inset) text-(--fg-subtle)'"
: 'bg-bg-inset text-fg-subtle'"
>
{{ isMember ? 'includes id' : 'not found' }}
</span>
</div>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-card p-4">
<p class="demo-label mb-2">
Primitive search (fromIndex 2)
</p>
<div class="flex flex-wrap gap-1.5">
@@ -72,8 +72,8 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
:key="i"
class="inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="i < 2
? 'border-(--border) bg-(--bg-inset) text-(--fg-subtle) opacity-50 line-through'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
? 'border-border bg-bg-inset text-fg-subtle opacity-50 line-through'
: 'border-border bg-bg-inset text-fg-muted'"
>
{{ tag }}
</span>
@@ -83,13 +83,13 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
v-model="query"
type="text"
placeholder="search a tag…"
class="mt-3 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)"
class="demo-input mt-3"
>
<p class="mt-2 text-sm text-(--fg-muted)">
<p class="mt-2 text-sm text-fg-muted">
Searching from index 2
<span
class="font-mono font-semibold"
:class="hasTag ? 'text-emerald-600 dark:text-emerald-400' : 'text-(--fg-subtle)'"
:class="hasTag ? 'text-emerald-600 dark:text-emerald-400' : 'text-fg-subtle'"
>{{ hasTag }}</span>
</p>
</div>
@@ -1,6 +1,6 @@
import { computed, toValue } from 'vue';
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
import { isObject, isString } from '@robonen/stdlib';
import { isFunction, isNumber, isObject, isString, isSymbol } from '@robonen/stdlib';
/**
* Comparator deciding whether an array element equals the searched value.
@@ -83,11 +83,11 @@ export function useArrayIncludes<T, V = T>(
// Resolve the comparator once instead of on every recompute.
let compare: UseArrayIncludesComparatorFn<T, V>;
if (isString(resolved) || typeof resolved === 'symbol' || typeof resolved === 'number') {
if (isString(resolved) || isSymbol(resolved) || isNumber(resolved)) {
const key = resolved as keyof T;
compare = (element, searched) => element[key] === (searched as unknown);
}
else if (typeof resolved === 'function') {
else if (isFunction(resolved)) {
compare = resolved;
}
else {
@@ -30,19 +30,19 @@ function remove(index: number) {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<p class="mb-1 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-sm">
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label mb-1">
Joined result
</p>
<p class="break-all font-mono text-sm text-(--fg) tabular-nums">
<p class="break-all font-mono text-sm text-fg tabular-nums">
<span v-if="joined">{{ joined }}</span>
<span v-else class="text-(--fg-subtle)">empty</span>
<span v-else class="text-fg-subtle">empty</span>
</p>
</div>
<div class="flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Separator</span>
<span class="demo-label">Separator</span>
<div class="flex gap-1.5">
<button
v-for="sep in separators"
@@ -50,8 +50,8 @@ function remove(index: number) {
type="button"
class="flex-1 rounded-lg border px-2 py-1.5 text-xs font-medium transition active:scale-[0.98] cursor-pointer"
:class="separator === sep.value
? 'border-transparent bg-(--accent) text-(--accent-fg)'
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset)'"
? 'border-transparent bg-accent text-accent-fg'
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset'"
@click="separator = sep.value"
>
{{ sep.label }}
@@ -63,16 +63,16 @@ function remove(index: number) {
<li
v-for="(segment, index) in segments"
:key="index"
class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm text-(--fg)"
class="flex items-center justify-between rounded-lg border border-border bg-bg-elevated px-3 py-1.5 text-sm text-fg"
>
<span class="flex items-center gap-2">
<span class="font-mono text-xs text-(--fg-subtle)">{{ index }}</span>
<span class="font-mono text-xs text-fg-subtle">{{ index }}</span>
{{ segment }}
</span>
<button
type="button"
aria-label="Remove segment"
class="rounded-md px-2 py-0.5 text-xs font-medium text-(--fg-subtle) transition hover:bg-(--bg-inset) hover:text-(--fg) cursor-pointer"
class="rounded-md px-2 py-0.5 text-xs font-medium text-fg-subtle transition hover:bg-bg-inset hover:text-fg cursor-pointer"
@click="remove(index)"
>
@@ -85,11 +85,11 @@ function remove(index: number) {
v-model="draft"
type="text"
placeholder="add a segment…"
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)"
class="demo-input"
>
<button
type="submit"
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"
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40"
:disabled="!draft.trim()"
>
Add
@@ -1,5 +1,6 @@
import { computed, toValue } from 'vue';
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
import { isFunction } from '@robonen/stdlib';
export type UseArrayJoinReturn = ComputedRef<string>;
@@ -30,7 +31,7 @@ export function useArrayJoin(
// reactive items first lets the computed track per-item ref dependencies.
let needsUnwrap = false;
for (const item of resolved) {
if (typeof item === 'function' || (typeof item === 'object' && item !== null && 'value' in item)) {
if (isFunction(item) || (typeof item === 'object' && item !== null && 'value' in item)) {
needsUnwrap = true;
break;
}
@@ -34,17 +34,17 @@ function bump(index: number, delta: number) {
</script>
<template>
<div class="w-full max-w-md flex flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Cart</span>
<label class="flex items-center gap-2 text-sm text-(--fg-muted)">
<span class="demo-label">Cart</span>
<label class="flex items-center gap-2 text-sm text-fg-muted">
Tax {{ taxRate }}%
<input
v-model.number="taxRate"
type="range"
min="0"
max="25"
class="accent-(--accent)"
class="accent-accent"
>
</label>
</div>
@@ -53,41 +53,41 @@ function bump(index: number, delta: number) {
<li
v-for="(item, index) in priced"
:key="item.name"
class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
>
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-(--fg)">
<p class="truncate text-sm font-medium text-fg">
{{ item.name }}
</p>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
base {{ formatter.format(item.price) }}
</p>
</div>
<div class="flex items-center gap-1.5">
<button
class="inline-flex size-7 items-center justify-center rounded-md border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
class="inline-flex size-7 items-center justify-center rounded-md border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset active:scale-[0.98] cursor-pointer"
aria-label="Decrease price"
@click="bump(index, -10)"
>
&minus;
</button>
<button
class="inline-flex size-7 items-center justify-center rounded-md border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
class="inline-flex size-7 items-center justify-center rounded-md border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset active:scale-[0.98] cursor-pointer"
aria-label="Increase price"
@click="bump(index, 10)"
>
+
</button>
</div>
<span class="w-20 text-right font-mono text-sm tabular-nums text-(--fg)">
<span class="w-20 text-right font-mono text-sm tabular-nums text-fg">
{{ formatter.format(item.gross) }}
</span>
</li>
</ul>
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Total with tax</span>
<span class="font-mono text-xl font-bold tabular-nums text-(--fg)">
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3">
<span class="demo-label">Total with tax</span>
<span class="demo-stat text-xl">
{{ formatter.format(total) }}
</span>
</div>
@@ -36,14 +36,14 @@ function removeAt(index: number) {
</script>
<template>
<div class="w-full max-w-md flex flex-col gap-4">
<label class="flex items-center justify-between gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Starting budget</span>
<div class="demo-stack max-w-md">
<label class="demo-card flex items-center justify-between gap-3 p-4">
<span class="demo-label">Starting budget</span>
<input
v-model.number="startingBudget"
type="number"
step="50"
class="w-28 rounded-lg border border-(--border) bg-(--bg) px-3 py-1.5 text-right font-mono text-sm tabular-nums text-(--fg) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
class="w-28 rounded-lg border border-border bg-bg px-3 py-1.5 text-right font-mono text-sm tabular-nums text-fg transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
>
</label>
@@ -51,34 +51,34 @@ function removeAt(index: number) {
<li
v-for="(expense, index) in expenses"
:key="index"
class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
>
<span class="flex-1 truncate text-sm text-(--fg)">{{ expense.label }}</span>
<span class="flex-1 truncate text-sm text-fg">{{ expense.label }}</span>
<span class="font-mono text-sm tabular-nums text-rose-600 dark:text-rose-400">
&minus;{{ formatter.format(expense.amount) }}
</span>
<button
class="inline-flex size-6 items-center justify-center rounded-md text-(--fg-subtle) transition hover:bg-(--bg-inset) hover:text-(--fg) active:scale-[0.98] cursor-pointer"
class="inline-flex size-6 items-center justify-center rounded-md text-fg-subtle transition hover:bg-bg-inset hover:text-fg active:scale-[0.98] cursor-pointer"
aria-label="Remove expense"
@click="removeAt(index)"
>
&times;
</button>
</li>
<li v-if="expenses.length === 0" class="rounded-lg border border-dashed border-(--border) px-3 py-4 text-center text-sm text-(--fg-subtle)">
<li v-if="expenses.length === 0" class="rounded-lg border border-dashed border-border px-3 py-4 text-center text-sm text-fg-subtle">
No expenses full budget remains.
</li>
</ul>
<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"
class="demo-btn"
@click="add"
>
+ Add charge
</button>
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Remaining</span>
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3">
<span class="demo-label">Remaining</span>
<span
class="font-mono text-2xl font-bold tabular-nums"
:class="remaining < 0 ? 'text-rose-600 dark:text-rose-400' : 'text-emerald-600 dark:text-emerald-400'"
@@ -27,7 +27,7 @@ function load(index: number, delta: number) {
</script>
<template>
<div class="w-full max-w-md flex flex-col gap-4">
<div class="demo-stack max-w-md">
<div
class="flex items-center gap-3 rounded-xl border p-4 transition"
:class="hasOverloaded
@@ -43,7 +43,7 @@ function load(index: number, delta: number) {
</p>
</div>
<label class="flex items-center justify-between gap-3 text-sm text-(--fg-muted)">
<label class="flex items-center justify-between gap-3 text-sm text-fg-muted">
<span>Alert threshold</span>
<span class="flex items-center gap-2">
<input
@@ -51,9 +51,9 @@ function load(index: number, delta: number) {
type="range"
min="40"
max="100"
class="accent-(--accent)"
class="accent-accent"
>
<span class="w-10 text-right font-mono tabular-nums text-(--fg)">{{ threshold }}%</span>
<span class="w-10 text-right font-mono tabular-nums text-fg">{{ threshold }}%</span>
</span>
</label>
@@ -61,31 +61,31 @@ function load(index: number, delta: number) {
<li
v-for="(server, index) in servers"
:key="server.name"
class="rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2.5"
class="rounded-lg border border-border bg-bg-elevated px-3 py-2.5"
>
<div class="mb-1.5 flex items-center justify-between">
<span class="font-mono text-sm text-(--fg)">{{ server.name }}</span>
<span class="font-mono text-sm text-fg">{{ server.name }}</span>
<span class="flex items-center gap-2">
<span
class="font-mono text-sm tabular-nums"
:class="server.cpu > threshold ? 'text-amber-600 dark:text-amber-400' : 'text-(--fg-muted)'"
:class="server.cpu > threshold ? 'text-amber-600 dark:text-amber-400' : 'text-fg-muted'"
>{{ server.cpu }}%</span>
<button
class="inline-flex size-6 items-center justify-center rounded-md border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
class="inline-flex size-6 items-center justify-center rounded-md border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset active:scale-[0.98] cursor-pointer"
aria-label="Decrease load"
@click="load(index, -10)"
>&minus;</button>
<button
class="inline-flex size-6 items-center justify-center rounded-md border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
class="inline-flex size-6 items-center justify-center rounded-md border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset active:scale-[0.98] cursor-pointer"
aria-label="Increase load"
@click="load(index, 10)"
>+</button>
</span>
</div>
<div class="h-1.5 w-full overflow-hidden rounded-full bg-(--bg-inset)">
<div class="h-1.5 w-full overflow-hidden rounded-full bg-bg-inset">
<div
class="h-full rounded-full transition-all"
:class="server.cpu > threshold ? 'bg-amber-500' : 'bg-(--accent)'"
:class="server.cpu > threshold ? 'bg-amber-500' : 'bg-accent'"
:style="{ width: `${server.cpu}%` }"
/>
</div>
@@ -35,40 +35,40 @@ function addTag() {
</script>
<template>
<div class="w-full max-w-md flex flex-col gap-4">
<div class="demo-stack max-w-md">
<form class="flex gap-2" @submit.prevent="addTag">
<input
v-model="draft"
type="text"
placeholder="Add a tag, e.g. TypeScript"
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)"
class="demo-input"
>
<button
type="submit"
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"
class="demo-btn-primary"
>
Add
</button>
</form>
<label class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2.5 text-sm text-(--fg)">
<label class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2.5 text-sm text-fg">
<span>Case-insensitive comparator</span>
<input
v-model="caseInsensitive"
type="checkbox"
class="size-4 accent-(--accent) cursor-pointer"
class="size-4 accent-accent cursor-pointer"
>
</label>
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Source ({{ raw.length }})
</span>
<div class="flex flex-wrap gap-1.5 rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<div class="flex flex-wrap gap-1.5 rounded-lg border border-border bg-bg-inset p-3">
<span
v-for="(tag, index) in raw"
:key="`${tag}-${index}`"
class="inline-flex items-center rounded-md border border-(--border) bg-(--bg-elevated) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
class="inline-flex items-center rounded-md border border-border bg-bg-elevated px-2 py-0.5 text-xs font-medium text-fg-muted"
>
{{ tag }}
</span>
@@ -76,18 +76,18 @@ function addTag() {
</div>
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Unique ({{ unique.length }})
</span>
<div class="flex flex-wrap gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) p-3">
<div class="flex flex-wrap gap-1.5 rounded-lg border border-border bg-bg-elevated p-3">
<span
v-for="tag in unique"
:key="tag"
class="inline-flex items-center gap-1.5 rounded-md border border-(--accent) bg-(--accent-subtle) px-2 py-0.5 text-xs font-medium text-(--accent-text)"
class="inline-flex items-center gap-1.5 rounded-md border border-accent bg-accent-subtle px-2 py-0.5 text-xs font-medium text-accent-text"
>
{{ tag }}
</span>
<span v-if="unique.length === 0" class="text-xs text-(--fg-subtle)">No tags yet.</span>
<span v-if="unique.length === 0" class="text-xs text-fg-subtle">No tags yet.</span>
</div>
</div>
</div>
@@ -1,6 +1,6 @@
import { computed, toValue } from 'vue';
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
import { isString, unique } from '@robonen/stdlib';
import { isFunction, isNumber, isString, isSymbol, unique } from '@robonen/stdlib';
/**
* Equality comparator deciding whether two array elements are duplicates.
@@ -66,12 +66,12 @@ export function useArrayUnique<T>(
// Resolve the comparison strategy once, not on every recompute.
// Key of T (string | number | symbol) -> O(n) first-seen-wins key de-dup.
if (isString(comparator) || typeof comparator === 'symbol' || typeof comparator === 'number') {
if (isString(comparator) || isSymbol(comparator) || isNumber(comparator)) {
const key = comparator as keyof T;
return computed<T[]>(() => uniqueByKey(resolve(list), element => element[key] as PropertyKey));
}
if (typeof comparator === 'function') {
if (isFunction(comparator)) {
// A unary key extractor stays O(n); a binary comparator falls back to O(n²)
// pairwise comparison (unavoidable for arbitrary equality). Branch on arity.
if (comparator.length <= 1) {
@@ -38,23 +38,23 @@ const keys: { id: SortKey; label: string }[] = [
</script>
<template>
<div class="w-full max-w-md flex flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex items-center justify-between gap-2">
<div class="inline-flex rounded-lg border border-(--border) bg-(--bg-elevated) p-0.5">
<div class="inline-flex rounded-lg border border-border bg-bg-elevated p-0.5">
<button
v-for="key in keys"
:key="key.id"
class="rounded-md px-3 py-1 text-sm font-medium transition cursor-pointer"
:class="sortKey === key.id
? 'bg-(--accent) text-(--accent-fg)'
: 'text-(--fg-muted) hover:text-(--fg)'"
? 'bg-accent text-accent-fg'
: 'text-fg-muted hover:text-fg'"
@click="sortKey = key.id"
>
{{ key.label }}
</button>
</div>
<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"
class="demo-btn"
@click="descending = !descending"
>
{{ descending ? 'Desc ↓' : 'Asc ↑' }}
@@ -65,22 +65,22 @@ const keys: { id: SortKey; label: string }[] = [
<li
v-for="(player, index) in sorted"
:key="player.name"
class="flex items-center gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2.5"
class="flex items-center gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2.5"
>
<span class="w-6 text-center font-mono text-sm tabular-nums text-(--fg-subtle)">
<span class="w-6 text-center font-mono text-sm tabular-nums text-fg-subtle">
{{ index + 1 }}
</span>
<span class="flex-1 text-sm font-medium text-(--fg)">{{ player.name }}</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="flex-1 text-sm font-medium text-fg">{{ player.name }}</span>
<span class="demo-badge">
Lv {{ player.level }}
</span>
<span class="w-16 text-right font-mono text-sm font-semibold tabular-nums text-(--fg)">
<span class="w-16 text-right font-mono text-sm font-semibold tabular-nums text-fg">
{{ player.score.toLocaleString() }}
</span>
</li>
</ol>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Stable sort players with an equal {{ sortKey }} keep their original order. The source array is left untouched.
</p>
</div>
@@ -2,13 +2,13 @@ import { computed, isRef, toValue, watchEffect } from 'vue';
import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
import { isFunction } from '@robonen/stdlib';
export type UseSortedCompareFn<T = any>
export type UseSortedCompareFn<T = unknown>
= (a: T, b: T) => number;
export type UseSortedFn<T = any>
export type UseSortedFn<T = unknown>
= (arr: T[], compareFn: UseSortedCompareFn<T>) => T[];
export interface UseSortedOptions<T = any> {
export interface UseSortedOptions<T = unknown> {
/**
* The sort algorithm to apply. Receives a copy of the array (or the source
* itself in `dirty` mode) and the resolved compare function.
@@ -101,13 +101,13 @@ const defaultSortFn: UseSortedFn = <T>(source: T[], compareFn: UseSortedCompareF
*
* @since 0.0.15
*/
export function useSorted<T = any>(source: Ref<T[]>, compareFn?: UseSortedCompareFn<T>): Ref<T[]>;
export function useSorted<T = any>(source: MaybeRefOrGetter<T[]>, compareFn?: UseSortedCompareFn<T>): ComputedRef<T[]>;
export function useSorted<T = any>(source: Ref<T[]>, options?: UseSortedOptions<T>): Ref<T[]>;
export function useSorted<T = any>(source: MaybeRefOrGetter<T[]>, options?: UseSortedOptions<T>): ComputedRef<T[]>;
export function useSorted<T = any>(source: Ref<T[]>, compareFn?: UseSortedCompareFn<T>, options?: Omit<UseSortedOptions<T>, 'compareFn'>): Ref<T[]>;
export function useSorted<T = any>(source: MaybeRefOrGetter<T[]>, compareFn?: UseSortedCompareFn<T>, options?: Omit<UseSortedOptions<T>, 'compareFn'>): ComputedRef<T[]>;
export function useSorted<T = any>(
export function useSorted<T = unknown>(source: Ref<T[]>, compareFn?: UseSortedCompareFn<T>): Ref<T[]>;
export function useSorted<T = unknown>(source: MaybeRefOrGetter<T[]>, compareFn?: UseSortedCompareFn<T>): ComputedRef<T[]>;
export function useSorted<T = unknown>(source: Ref<T[]>, options?: UseSortedOptions<T>): Ref<T[]>;
export function useSorted<T = unknown>(source: MaybeRefOrGetter<T[]>, options?: UseSortedOptions<T>): ComputedRef<T[]>;
export function useSorted<T = unknown>(source: Ref<T[]>, compareFn?: UseSortedCompareFn<T>, options?: Omit<UseSortedOptions<T>, 'compareFn'>): Ref<T[]>;
export function useSorted<T = unknown>(source: MaybeRefOrGetter<T[]>, compareFn?: UseSortedCompareFn<T>, options?: Omit<UseSortedOptions<T>, 'compareFn'>): ComputedRef<T[]>;
export function useSorted<T = unknown>(
source: MaybeRefOrGetter<T[]>,
compareFnOrOptions?: UseSortedCompareFn<T> | UseSortedOptions<T>,
maybeOptions?: Omit<UseSortedOptions<T>, 'compareFn'>,
@@ -27,9 +27,9 @@ function setQuantity(delta: number): void {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Shared cart</span>
<span class="demo-label">Shared cart</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="supported
@@ -41,15 +41,15 @@ function setQuantity(delta: number): void {
</span>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="demo-card p-4">
<div class="flex flex-wrap gap-2">
<button
v-for="product in products"
:key="product"
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="cart.item === product
? '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)'"
? '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="pick(product)"
>
{{ product }}
@@ -57,18 +57,18 @@ function setQuantity(delta: number): void {
</div>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-(--fg-muted)">Quantity</span>
<span class="text-sm text-fg-muted">Quantity</span>
<div class="flex items-center gap-2">
<button
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border bg-bg-elevated 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="cart.quantity <= 1"
@click="setQuantity(-1)"
>
&minus;
</button>
<span class="w-8 text-center font-mono text-lg font-bold tabular-nums text-(--fg)">{{ cart.quantity }}</span>
<span class="demo-stat w-8 text-center text-lg">{{ cart.quantity }}</span>
<button
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.98] cursor-pointer"
@click="setQuantity(1)"
>
+
@@ -77,21 +77,21 @@ function setQuantity(delta: number): void {
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
<div class="flex justify-between">
<span class="text-(--fg-muted)">{{ cart.item }} &times; {{ cart.quantity }}</span>
<span class="text-fg-muted">{{ cart.item }} &times; {{ cart.quantity }}</span>
<span class="font-bold">${{ subtotal }}</span>
</div>
</div>
<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"
class="demo-btn"
@click="theme = theme === 'light' ? 'dark' : 'light'"
>
Toggle shared theme: <span class="font-mono">{{ theme }}</span>
</button>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Open this page in a second tab. Every change you make here is broadcast and mirrored instantly in the other tab.
</p>
</div>
@@ -21,42 +21,42 @@ const device = (): string => (isDesktop.value ? 'Desktop' : isMobile.value ? 'Mo
</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">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Active breakpoint</span>
<div class="demo-stack max-w-sm">
<div class="demo-card p-4">
<span class="demo-label">Active breakpoint</span>
<div class="mt-1 flex items-baseline gap-2">
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ active || 'none' }}</span>
<span class="rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">{{ device() }}</span>
<span class="demo-stat text-3xl">{{ active || 'none' }}</span>
<span class="rounded-md border border-border bg-bg-inset px-2 py-0.5 text-xs font-medium text-fg-muted">{{ device() }}</span>
</div>
</div>
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Tailwind breakpoints</span>
<span class="demo-label">Tailwind breakpoints</span>
<div
v-for="row in rows"
:key="row.key"
class="flex items-center justify-between rounded-lg border px-3 py-2 text-sm transition"
:class="bp[row.key].value
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
? 'border-accent bg-accent-subtle text-accent-text'
: 'border-border bg-bg-inset text-fg-muted'"
>
<span class="font-mono font-medium">{{ row.key }}</span>
<span class="font-mono tabular-nums text-(--fg-subtle)">&ge; {{ row.width }}</span>
<span class="font-mono tabular-nums text-fg-subtle">&ge; {{ row.width }}</span>
<span
class="h-2 w-2 rounded-full transition"
:class="bp[row.key].value ? 'bg-(--accent)' : 'bg-(--border-strong)'"
:class="bp[row.key].value ? 'bg-accent' : 'bg-border-strong'"
/>
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
<div class="flex justify-between">
<span class="text-(--fg-muted)">current()</span>
<span class="text-fg-muted">current()</span>
<span>[{{ current.length ? current.join(', ') : '—' }}]</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Resize your browser window the matched breakpoints update live.
</p>
</div>
@@ -5,6 +5,7 @@ import { defaultWindow } from '@/types';
import type { ConfigurableWindow } from '@/types';
import { useMediaQuery } from '@/composables/browser/useMediaQuery';
import type { UseMediaQueryOptions } from '@/composables/browser/useMediaQuery';
import { pxValue } from '@robonen/platform/browsers';
/**
* A breakpoints map: name → viewport width. Numbers are treated as pixels;
@@ -61,22 +62,6 @@ export type UseBreakpointsReturn<K extends string = string>
active: () => ComputedRef<K | ''>;
};
/**
* Parse a CSS length token (`"1024px"`, `"48em"`, `"30rem"`, `"50%"`) into a
* pixel number. `em`/`rem` use the conventional 16px root size.
*/
function pxValue(value: string): number {
const number = Number.parseFloat(value);
if (Number.isNaN(number))
return Number.NaN;
if (/(?:em|rem)\s*$/i.test(value))
return number * 16;
return number;
}
/**
* Add `delta` to the numeric portion of a CSS length, preserving its unit.
* Used to build the strict (`> / <`) variants from inclusive media queries via
@@ -14,9 +14,9 @@ const snippets = [
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Clipboard API</span>
<span class="demo-label">Clipboard API</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="isSupported
@@ -32,11 +32,11 @@ const snippets = [
<input
v-model="draft"
type="text"
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)"
class="demo-input"
placeholder="Type something to copy…"
>
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="copyPending || !draft"
@click="copy(draft)"
>
@@ -45,21 +45,21 @@ const snippets = [
</div>
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Quick copy</span>
<span class="demo-label">Quick copy</span>
<button
v-for="snippet in snippets"
:key="snippet"
class="inline-flex items-center justify-between gap-2 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2 text-left text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.99] cursor-pointer"
class="inline-flex items-center justify-between gap-2 rounded-lg border border-border bg-bg-elevated px-3 py-2 text-left text-sm font-medium text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.99] cursor-pointer"
@click="copy(snippet)"
>
<span class="truncate font-mono text-xs text-(--fg-muted)">{{ snippet }}</span>
<span class="shrink-0 text-xs text-(--fg-subtle)">Copy</span>
<span class="truncate font-mono text-xs text-fg-muted">{{ snippet }}</span>
<span class="shrink-0 text-xs text-fg-subtle">Copy</span>
</button>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Last copied</span>
<p class="mt-1 break-all font-mono text-sm text-(--fg)">{{ text || '—' }}</p>
<div class="rounded-lg border border-border bg-bg-inset p-3">
<span class="demo-label">Last copied</span>
<p class="mt-1 break-all font-mono text-sm text-fg">{{ text || '—' }}</p>
</div>
</template>
@@ -34,9 +34,9 @@ function typesOf(item: ClipboardItem): string {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">ClipboardItem API</span>
<span class="demo-label">ClipboardItem API</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="isSupported
@@ -48,42 +48,42 @@ function typesOf(item: ClipboardItem): string {
</div>
<template v-if="isSupported">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Rich payload</span>
<p class="mt-1 text-sm text-(--fg)" v-html="html" />
<p class="mt-1 font-mono text-xs text-(--fg-subtle)">text/plain &middot; text/html</p>
<div class="demo-card p-4">
<span class="demo-label">Rich payload</span>
<p class="mt-1 text-sm text-fg" v-html="html" />
<p class="mt-1 font-mono text-xs text-fg-subtle">text/plain &middot; text/html</p>
</div>
<div class="flex gap-2">
<button
class="inline-flex flex-1 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"
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="copyPending"
@click="copyRich"
>
{{ copyPending ? 'Copying…' : copied ? 'Copied!' : 'Copy rich content' }}
</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"
class="demo-btn"
@click="readClipboard"
>
Read clipboard
</button>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="rounded-lg border border-border bg-bg-inset p-3">
<span class="demo-label">
content ({{ content.length }} {{ content.length === 1 ? 'item' : 'items' }})
</span>
<ul v-if="content.length" class="mt-2 flex flex-col gap-1">
<li
v-for="(item, i) in content"
:key="i"
class="font-mono text-xs text-(--fg)"
class="font-mono text-xs text-fg"
>
#{{ i + 1 }}: {{ typesOf(item) }}
</li>
</ul>
<p v-else class="mt-2 font-mono text-xs text-(--fg-subtle)">No items read yet</p>
<p v-else class="mt-2 font-mono text-xs text-fg-subtle">No items read yet</p>
</div>
<div
@@ -20,9 +20,9 @@ onMounted(() => {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">CloseWatcher API</span>
<span class="demo-label">CloseWatcher API</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="isSupported
@@ -34,7 +34,7 @@ onMounted(() => {
</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 disabled:cursor-not-allowed disabled:opacity-40"
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40"
:disabled="open"
@click="open = true"
>
@@ -47,19 +47,19 @@ onMounted(() => {
leave-active-class="transition duration-100 ease-in"
leave-to-class="opacity-0 translate-y-1"
>
<div v-if="open" class="rounded-xl border border-(--border-strong) bg-(--bg-elevated) p-4 shadow-lg">
<div v-if="open" class="rounded-xl border border-border-strong bg-bg-elevated p-4 shadow-lg">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-sm font-semibold text-(--fg)">Unsaved changes</p>
<p class="mt-1 text-sm text-(--fg-muted)">
Press <kbd class="rounded border border-(--border) bg-(--bg-inset) px-1.5 py-0.5 font-mono text-xs text-(--fg)">Esc</kbd>
<p class="text-sm font-semibold text-fg">Unsaved changes</p>
<p class="mt-1 text-sm text-fg-muted">
Press <kbd class="rounded border border-border bg-bg-inset px-1.5 py-0.5 font-mono text-xs text-fg">Esc</kbd>
(or the Android back gesture) to dismiss.
</p>
</div>
</div>
<div class="mt-4 flex justify-end 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"
class="demo-btn"
@click="close()"
>
Dismiss via close()
@@ -68,18 +68,18 @@ onMounted(() => {
</div>
</Transition>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
<div class="flex justify-between">
<span class="text-(--fg-muted)">closes</span>
<span class="text-fg-muted">closes</span>
<span class="font-bold">{{ closeCount }}</span>
</div>
<div class="mt-1 flex justify-between">
<span class="text-(--fg-muted)">last</span>
<span class="text-fg-muted">last</span>
<span>{{ lastClosedAt ?? '—' }}</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Open the dialog, then dismiss it with Esc, the system back gesture, or the programmatic <code class="font-mono">close()</code> call.
</p>
</div>
@@ -30,11 +30,11 @@ const options = [
<template>
<div
ref="target"
class="flex w-full max-w-sm flex-col gap-4"
class="demo-stack max-w-sm"
>
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Color mode</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="demo-label">Color mode</span>
<span class="demo-badge">
system: {{ mode.system.value }}
</span>
</div>
@@ -46,8 +46,8 @@ const options = [
type="button"
class="inline-flex flex-col items-center justify-center gap-1 rounded-lg border px-2 py-3 text-xs font-medium transition active:scale-[0.98] cursor-pointer"
:class="mode === opt.value
? 'border-transparent bg-(--accent) text-(--accent-fg)'
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
? 'border-transparent bg-accent text-accent-fg'
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
@click="mode = opt.value"
>
<span class="text-base leading-none">{{ opt.icon }}</span>
@@ -55,26 +55,26 @@ const options = [
</button>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="mb-3 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Reactive state</p>
<div class="demo-card p-4">
<p class="demo-label mb-3">Reactive state</p>
<dl class="space-y-2 text-sm">
<div class="flex items-center justify-between">
<dt class="text-(--fg-muted)">selected (emitAuto)</dt>
<dd class="font-mono tabular-nums text-(--fg)">{{ mode }}</dd>
<dt class="text-fg-muted">selected (emitAuto)</dt>
<dd class="font-mono tabular-nums text-fg">{{ mode }}</dd>
</div>
<div class="flex items-center justify-between">
<dt class="text-(--fg-muted)">resolved state</dt>
<dd class="font-mono tabular-nums text-(--fg)">{{ mode.state.value }}</dd>
<dt class="text-fg-muted">resolved state</dt>
<dd class="font-mono tabular-nums text-fg">{{ mode.state.value }}</dd>
</div>
<div class="flex items-center justify-between">
<dt class="text-(--fg-muted)">store</dt>
<dd class="font-mono tabular-nums text-(--fg)">{{ mode.store.value }}</dd>
<dt class="text-fg-muted">store</dt>
<dd class="font-mono tabular-nums text-fg">{{ mode.store.value }}</dd>
</div>
</dl>
</div>
<p class="text-xs text-(--fg-subtle)">
The chosen mode is applied as <code class="font-mono text-(--fg-muted)">data-demo-theme</code> on this card.
<p class="text-xs text-fg-subtle">
The chosen mode is applied as <code class="font-mono text-fg-muted">data-demo-theme</code> on this card.
Pick "Auto" to follow your OS preference.
</p>
</div>
@@ -15,10 +15,10 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
ref="target"
class="flex items-center justify-center rounded-xl border border-(--border) bg-(--bg-inset) p-6"
class="flex items-center justify-center rounded-xl border border-border bg-bg-inset p-6"
>
<div
class="shadow-lg transition-all duration-300 ease-out"
@@ -31,11 +31,11 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
/>
</div>
<div class="flex flex-col gap-4 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="demo-card flex flex-col gap-4 p-4">
<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="hue">Hue</label>
<span class="font-mono text-sm tabular-nums text-(--fg)">--demo-hue: {{ hue }}</span>
<label class="demo-label" for="hue">Hue</label>
<span class="font-mono text-sm tabular-nums text-fg">--demo-hue: {{ hue }}</span>
</div>
<input
id="hue"
@@ -43,14 +43,14 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
type="range"
min="0"
max="360"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
<div class="flex gap-1.5">
<button
v-for="s in swatches"
:key="s"
type="button"
class="h-6 w-6 rounded-md border border-(--border) transition hover:scale-110 active:scale-95 cursor-pointer"
class="h-6 w-6 rounded-md border border-border transition hover:scale-110 active:scale-95 cursor-pointer"
:style="{ background: `hsl(${s} 80% 55%)` }"
:aria-label="`Set hue ${s}`"
@click="hue = s"
@@ -60,8 +60,8 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
<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="radius">Radius</label>
<span class="font-mono text-sm tabular-nums text-(--fg)">--demo-radius: {{ radius }}</span>
<label class="demo-label" for="radius">Radius</label>
<span class="font-mono text-sm tabular-nums text-fg">--demo-radius: {{ radius }}</span>
</div>
<input
id="radius"
@@ -69,14 +69,14 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
type="range"
min="0"
max="48"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
</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="size">Size</label>
<span class="font-mono text-sm tabular-nums text-(--fg)">--demo-size: {{ size }}</span>
<label class="demo-label" for="size">Size</label>
<span class="font-mono text-sm tabular-nums text-fg">--demo-size: {{ size }}</span>
</div>
<input
id="size"
@@ -84,12 +84,12 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
type="range"
min="48"
max="140"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
background: {{ accent }};
</div>
</div>
@@ -23,9 +23,9 @@ function toggle() {
<div
ref="target"
data-demo-mode
class="flex w-full max-w-sm flex-col gap-4"
class="demo-stack max-w-sm"
>
<div class="flex items-center justify-between rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="demo-card flex items-center justify-between p-4">
<div class="flex items-center gap-3">
<span
class="flex h-10 w-10 items-center justify-center rounded-lg text-lg transition-colors"
@@ -36,8 +36,8 @@ function toggle() {
{{ isDark ? '☾' : '☀' }}
</span>
<div>
<p class="text-sm font-medium text-(--fg)">{{ isDark ? 'Dark mode' : 'Light mode' }}</p>
<p class="text-xs text-(--fg-subtle)">isDark = {{ isDark }}</p>
<p class="text-sm font-medium text-fg">{{ isDark ? 'Dark mode' : 'Light mode' }}</p>
<p class="text-xs text-fg-subtle">isDark = {{ isDark }}</p>
</div>
</div>
@@ -45,30 +45,30 @@ function toggle() {
type="button"
role="switch"
:aria-checked="isDark"
class="relative inline-flex h-7 w-12 items-center rounded-full border border-(--border) transition focus:outline-none focus:ring-2 focus:ring-(--ring) cursor-pointer"
:class="isDark ? 'bg-(--accent)' : 'bg-(--bg-inset)'"
class="relative inline-flex h-7 w-12 items-center rounded-full border border-border transition focus:outline-none focus:ring-2 focus:ring-ring cursor-pointer"
:class="isDark ? 'bg-accent' : 'bg-bg-inset'"
@click="toggle"
>
<span
class="inline-block h-5 w-5 transform rounded-full bg-(--bg) shadow transition-transform"
class="inline-block h-5 w-5 transform rounded-full bg-bg shadow transition-transform"
:class="isDark ? 'translate-x-6' : 'translate-x-1'"
/>
</button>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Preview surface</p>
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label mb-2">Preview surface</p>
<div class="flex items-center gap-2">
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg px-2 py-0.5 text-xs font-medium text-fg-muted">
data-demo-mode = "{{ isDark ? 'dark' : 'light' }}"
</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Writing the boolean toggles the attribute on this card. When the requested state
matches your OS preference, <code class="font-mono text-(--fg-muted)">useDark</code>
falls back to <code class="font-mono text-(--fg-muted)">auto</code> to keep tracking it.
matches your OS preference, <code class="font-mono text-fg-muted">useDark</code>
falls back to <code class="font-mono text-fg-muted">auto</code> to keep tracking it.
</p>
</div>
</template>
@@ -51,7 +51,7 @@ watch(isOpen, (openNow) => {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
v-if="!isSupported"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-400"
@@ -62,14 +62,14 @@ watch(isOpen, (openNow) => {
<template v-else>
<div
ref="host"
class="min-h-[7rem] rounded-xl border border-(--border) bg-(--bg-inset) p-1"
class="min-h-[7rem] rounded-xl border border-border bg-bg-inset p-1"
>
<div
ref="player"
class="flex h-full flex-col items-center justify-center gap-1 rounded-lg bg-(--bg-elevated) p-6"
class="flex h-full flex-col items-center justify-center gap-1 rounded-lg bg-bg-elevated p-6"
>
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Live timer</span>
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
<span class="demo-label">Live timer</span>
<span class="demo-stat text-3xl">
{{ String(Math.floor(elapsed / 60)).padStart(2, '0') }}:{{ String(elapsed % 60).padStart(2, '0') }}
</span>
</div>
@@ -78,7 +78,7 @@ watch(isOpen, (openNow) => {
<div class="flex items-center gap-2">
<button
type="button"
class="inline-flex flex-1 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"
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="isOpen"
@click="popOut"
>
@@ -86,7 +86,7 @@ watch(isOpen, (openNow) => {
</button>
<button
type="button"
class="inline-flex flex-1 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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!isOpen"
@click="close"
>
@@ -94,17 +94,17 @@ watch(isOpen, (openNow) => {
</button>
</div>
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-sm">
<span class="text-(--fg-muted)">isOpen</span>
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3 text-sm">
<span class="text-fg-muted">isOpen</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="isOpen
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg) text-(--fg-muted)'"
: 'border-border bg-bg text-fg-muted'"
>
<span
class="h-1.5 w-1.5 rounded-full"
:class="isOpen ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
:class="isOpen ? 'bg-emerald-500' : 'bg-fg-subtle'"
/>
{{ isOpen ? 'floating' : 'docked' }}
</span>
@@ -118,7 +118,7 @@ watch(isOpen, (openNow) => {
</p>
<p
v-else
class="text-xs text-(--fg-subtle)"
class="text-xs text-fg-subtle"
>
"Pop out" moves the live timer into an always-on-top window. Closing it returns the element to the page.
</p>
@@ -46,33 +46,33 @@ function toggleListening() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">pointermove on element</span>
<span class="demo-label">pointermove on element</span>
<div
ref="pad"
class="relative h-32 overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset) touch-none"
class="relative h-32 overflow-hidden rounded-xl border border-border bg-bg-inset touch-none"
>
<div
class="pointer-events-none absolute h-5 w-5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-(--accent) transition-opacity"
class="pointer-events-none absolute h-5 w-5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent transition-opacity"
:class="inside ? 'opacity-100' : 'opacity-0'"
:style="{ left: `${pos.x}px`, top: `${pos.y}px` }"
/>
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
<span class="text-xs text-(--fg-subtle)">{{ inside ? '' : 'Hover here' }}</span>
<span class="text-xs text-fg-subtle">{{ inside ? '' : 'Hover here' }}</span>
</div>
<div class="pointer-events-none absolute bottom-2 left-2 rounded-md border border-(--border) bg-(--bg) px-2 py-0.5 font-mono text-xs tabular-nums text-(--fg-muted)">
<div class="pointer-events-none absolute bottom-2 left-2 rounded-md border border-border bg-bg px-2 py-0.5 font-mono text-xs tabular-nums text-fg-muted">
x: {{ pos.x }} · y: {{ pos.y }}
</div>
</div>
</div>
<div class="flex flex-col gap-2 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="demo-card flex flex-col gap-2 p-4">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">keydown on window</span>
<span class="demo-label">keydown on window</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-xs font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-border bg-bg-elevated px-3 py-1.5 text-xs font-medium text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.98] cursor-pointer"
@click="toggleListening"
>
{{ listening ? 'Stop listening' : 'Start listening' }}
@@ -81,30 +81,30 @@ function toggleListening() {
<div class="flex items-center gap-3">
<kbd
class="flex min-w-[3.5rem] items-center justify-center rounded-lg border border-(--border-strong) bg-(--bg-inset) px-3 py-2 font-mono text-sm font-medium text-(--fg)"
class="flex min-w-[3.5rem] items-center justify-center rounded-lg border border-border-strong bg-bg-inset px-3 py-2 font-mono text-sm font-medium text-fg"
>
{{ lastKey || '—' }}
</kbd>
<div class="flex flex-col">
<span class="text-xs text-(--fg-subtle)">presses captured</span>
<span class="font-mono text-lg font-bold tabular-nums text-(--fg)">{{ keyCount }}</span>
<span class="text-xs text-fg-subtle">presses captured</span>
<span class="demo-stat text-lg">{{ keyCount }}</span>
</div>
<span
class="ml-auto inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="listening
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
<span
class="h-1.5 w-1.5 rounded-full"
:class="listening ? 'bg-emerald-500 animate-pulse' : 'bg-(--fg-subtle)'"
:class="listening ? 'bg-emerald-500 animate-pulse' : 'bg-fg-subtle'"
/>
{{ listening ? 'active' : 'stopped' }}
</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Listeners auto-detach on unmount. The returned stop function lets you detach early press any key, then toggle listening.
</p>
</div>
@@ -6,8 +6,8 @@ import type { MaybeRefOrGetter } from 'vue';
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';
interface InferEventTarget<Events> {
addEventListener: (event: Events, listener?: any, options?: any) => any;
removeEventListener: (event: Events, listener?: any, options?: any) => any;
addEventListener: (event: Events, listener?: GeneralEventListener, options?: boolean | AddEventListenerOptions) => void;
removeEventListener: (event: Events, listener?: GeneralEventListener, options?: boolean | EventListenerOptions) => void;
}
export type GeneralEventListener<E = Event> = (evt: E) => void;
@@ -27,7 +27,7 @@ type ListenerOptions = boolean | AddEventListenerOptions;
*/
export function useEventListener<E extends WindowEventName>(
event: Arrayable<E>,
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => void>,
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
): VoidFunction;
@@ -41,7 +41,7 @@ export function useEventListener<E extends WindowEventName>(
export function useEventListener<E extends WindowEventName>(
target: Window,
event: Arrayable<E>,
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => void>,
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
): VoidFunction;
@@ -55,7 +55,7 @@ export function useEventListener<E extends WindowEventName>(
export function useEventListener<E extends DocumentEventName>(
target: Document,
event: Arrayable<E>,
listener: Arrayable<(this: Document, ev: DocumentEventMap[E]) => any>,
listener: Arrayable<(this: Document, ev: DocumentEventMap[E]) => void>,
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
): VoidFunction;
@@ -69,7 +69,7 @@ export function useEventListener<E extends DocumentEventName>(
export function useEventListener<E extends ElementEventName>(
target: MaybeRefOrGetter<HTMLElement | null | undefined>,
event: Arrayable<E>,
listener: Arrayable<(this: HTMLElement, ev: HTMLElementEventMap[E]) => any>,
listener: Arrayable<(this: HTMLElement, ev: HTMLElementEventMap[E]) => void>,
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
): VoidFunction;
@@ -101,6 +101,7 @@ export function useEventListener<EventType = Event>(
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
): VoidFunction;
// Variadic implementation signature behind the typed overloads above; args are narrowed at runtime.
export function useEventListener(...args: any[]) {
let target: MaybeRefOrGetter<EventTarget> | undefined = defaultWindow;
let _events: Arrayable<string>;
@@ -34,7 +34,7 @@ async function pick() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
v-if="!isSupported"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-center text-sm text-amber-600 dark:text-amber-400"
@@ -44,33 +44,33 @@ async function pick() {
<template v-else>
<div
class="flex h-32 items-center justify-center rounded-xl border border-(--border) transition-colors duration-300"
class="flex h-32 items-center justify-center rounded-xl border border-border transition-colors duration-300"
:style="{ backgroundColor: hex, color: readableText }"
>
<span class="font-mono text-2xl font-bold tabular-nums">{{ hex }}</span>
</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="pick">
<button class="demo-btn-primary" @click="pick">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m2 22 1-1h3l9-9" /><path d="M3 21v-3l9-9" /><path d="m15 6 3.4-3.4a2.1 2.1 0 1 1 3 3L21 6l3 3-3 3-3-3-9 9" />
</svg>
Pick a color from screen
</button>
<p v-if="error" class="text-center text-xs text-(--fg-subtle)">
<p v-if="error" class="text-center text-xs text-fg-subtle">
{{ error }}
</p>
<div v-if="history.length" class="flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Recent</span>
<span class="demo-label">Recent</span>
<div class="flex flex-wrap gap-2">
<button
v-for="color in history"
:key="color"
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"
class="demo-badge transition hover:border-border-strong cursor-pointer"
@click="sRGBHex = color"
>
<span class="size-3 rounded-full border border-(--border)" :style="{ backgroundColor: color }" />
<span class="size-3 rounded-full border border-border" :style="{ backgroundColor: color }" />
{{ color }}
</button>
</div>
@@ -31,34 +31,34 @@ function select(preset: Preset) {
</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">
<div class="flex items-center gap-2 rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4">
<div class="flex items-center gap-2 rounded-lg border border-border bg-bg-inset px-3 py-2">
<div class="flex gap-1.5">
<span class="size-2.5 rounded-full bg-red-500/70" />
<span class="size-2.5 rounded-full bg-amber-500/70" />
<span class="size-2.5 rounded-full bg-emerald-500/70" />
</div>
<div class="ml-2 flex flex-1 items-center gap-2 rounded-md bg-(--bg) px-2 py-1">
<div class="ml-2 flex flex-1 items-center gap-2 rounded-md bg-bg px-2 py-1">
<span class="text-base leading-none">{{ presets.find(p => p.label === active)?.emoji }}</span>
<span class="truncate text-xs text-(--fg-muted)">My Awesome App</span>
<span class="truncate text-xs text-fg-muted">My Awesome App</span>
</div>
</div>
<p class="mt-2 text-center text-xs text-(--fg-subtle)">
<p class="mt-2 text-center text-xs text-fg-subtle">
Look at the real browser tab its favicon updates live.
</p>
</div>
<div class="flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Choose a favicon</span>
<span class="demo-label">Choose a favicon</span>
<div class="grid grid-cols-2 gap-2">
<button
v-for="preset in presets"
:key="preset.label"
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="active === preset.label
? '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)'"
? '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="select(preset)"
>
<span class="text-base leading-none">{{ preset.emoji }}</span>
@@ -67,7 +67,7 @@ function select(preset: Preset) {
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg) break-all">
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg break-all">
favicon.value = "{{ presets.find(p => p.label === active)?.emoji }} svg"
</div>
</div>
@@ -35,26 +35,26 @@ function pick() {
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex items-center justify-between gap-3">
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-(--fg-muted)">
<input v-model="multiple" type="checkbox" class="size-4 rounded border-(--border) accent-(--accent)">
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-fg-muted">
<input v-model="multiple" type="checkbox" class="size-4 rounded border-border accent-accent">
Allow multiple
</label>
<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="demo-badge">
{{ status }}
</span>
</div>
<div class="flex gap-2">
<button class="inline-flex flex-1 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="pick">
<button class="demo-btn-primary flex-1" @click="pick">
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" y1="3" x2="12" y2="15" />
</svg>
Choose images
</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"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!selected.length"
@click="reset"
>
@@ -64,25 +64,25 @@ function pick() {
<div
v-if="!selected.length"
class="flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-(--border) bg-(--bg-inset) p-6 text-center"
class="flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-border bg-bg-inset p-6 text-center"
>
<span class="text-sm text-(--fg-muted)">No files selected</span>
<span class="text-xs text-(--fg-subtle)">Click Choose images to open the native dialog</span>
<span class="text-sm text-fg-muted">No files selected</span>
<span class="text-xs text-fg-subtle">Click Choose images to open the native dialog</span>
</div>
<div v-else class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">{{ selected.length }} file(s)</span>
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ formatBytes(totalBytes) }} total</span>
<span class="demo-label">{{ selected.length }} file(s)</span>
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ formatBytes(totalBytes) }} total</span>
</div>
<ul class="flex max-h-44 flex-col gap-1.5 overflow-auto">
<li
v-for="file in selected"
:key="file.name + file.lastModified"
class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
>
<span class="truncate text-sm text-(--fg)">{{ file.name }}</span>
<span class="shrink-0 font-mono text-xs tabular-nums text-(--fg-subtle)">{{ formatBytes(file.size) }}</span>
<span class="truncate text-sm text-fg">{{ file.name }}</span>
<span class="shrink-0 font-mono text-xs tabular-nums text-fg-subtle">{{ formatBytes(file.size) }}</span>
</li>
</ul>
</div>
@@ -47,7 +47,7 @@ async function newFile() {
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<div class="demo-stack max-w-md">
<div
v-if="!isSupported"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-center text-sm text-amber-600 dark:text-amber-400"
@@ -57,28 +57,28 @@ async function newFile() {
<template v-else>
<div class="flex flex-wrap 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="open()">
<button class="demo-btn" @click="open()">
Open
</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="newFile">
<button class="demo-btn" @click="newFile">
New
</button>
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100" :disabled="data === undefined" @click="save()">
<button class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100" :disabled="data === undefined" @click="save()">
Save
</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="data === undefined" @click="saveAs()">
<button class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100" :disabled="data === undefined" @click="saveAs()">
Save As
</button>
</div>
<div v-if="fileName" class="flex flex-wrap items-center gap-2">
<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="demo-badge">
{{ fileName }}
</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="demo-badge">
{{ fileMIME || 'text/plain' }}
</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-mono tabular-nums text-(--fg-muted)">
<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-mono tabular-nums text-fg-muted">
{{ formatBytes(fileSize) }}
</span>
</div>
@@ -88,19 +88,19 @@ async function newFile() {
v-model="text"
rows="6"
spellcheck="false"
class="w-full resize-none 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)"
class="demo-input resize-none font-mono"
placeholder="File contents…"
/>
<div
v-else
class="flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-(--border) bg-(--bg-inset) p-6 text-center"
class="flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-border bg-bg-inset p-6 text-center"
>
<span class="text-sm text-(--fg-muted)">No file open</span>
<span class="text-xs text-(--fg-subtle)">Open an existing file or create a new one, edit it, then save back to disk.</span>
<span class="text-sm text-fg-muted">No file open</span>
<span class="text-xs text-fg-subtle">Open an existing file or create a new one, edit it, then save back to disk.</span>
</div>
<p v-if="lastError" class="text-center text-xs text-(--fg-subtle)">
<p v-if="lastError" class="text-center text-xs text-fg-subtle">
{{ lastError }}
</p>
</template>
@@ -7,7 +7,7 @@ const { isSupported, isFullscreen, enter, exit, toggle } = useFullscreen(target)
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
v-if="!isSupported"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-center text-sm text-amber-600 dark:text-amber-400"
@@ -17,27 +17,27 @@ const { isSupported, isFullscreen, enter, exit, toggle } = useFullscreen(target)
<div
ref="target"
class="relative flex h-44 flex-col items-center justify-center gap-3 overflow-hidden rounded-xl border border-(--border) bg-(--bg-elevated) transition-colors"
:class="isFullscreen && 'bg-(--bg-inset)'"
class="demo-card relative flex h-44 flex-col items-center justify-center gap-3 overflow-hidden transition-colors"
:class="isFullscreen && 'bg-bg-inset'"
>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="isFullscreen
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
<span class="size-1.5 rounded-full" :class="isFullscreen ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
<span class="size-1.5 rounded-full" :class="isFullscreen ? 'bg-emerald-500' : 'bg-fg-subtle'" />
{{ isFullscreen ? 'Fullscreen' : 'Windowed' }}
</span>
<p class="px-6 text-center text-sm text-(--fg-muted)">
<p class="px-6 text-center text-sm text-fg-muted">
This panel becomes the fullscreen target. Press
<kbd class="rounded border border-(--border) bg-(--bg) px-1.5 py-0.5 font-mono text-xs text-(--fg)">Esc</kbd>
<kbd class="rounded border border-border bg-bg px-1.5 py-0.5 font-mono text-xs text-fg">Esc</kbd>
to leave.
</p>
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!isSupported"
@click="toggle"
>
@@ -55,14 +55,14 @@ const { isSupported, isFullscreen, enter, exit, toggle } = useFullscreen(target)
<div class="flex gap-2">
<button
class="inline-flex flex-1 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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!isSupported || isFullscreen"
@click="enter"
>
Enter
</button>
<button
class="inline-flex flex-1 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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!isSupported || !isFullscreen"
@click="exit"
>
@@ -146,7 +146,7 @@ export function useFullscreen(
const isCurrentElementFullScreen = (): boolean => {
if (fullscreenElementMethod)
return (document as any)?.[fullscreenElementMethod] === targetRef.value;
return (document as Record<string, unknown> | undefined)?.[fullscreenElementMethod] === targetRef.value;
return false;
};
@@ -155,12 +155,12 @@ export function useFullscreen(
if (!flag)
return false;
const docFlag = document && (document as any)[flag];
const docFlag = document && (document as unknown as Record<string, unknown>)[flag];
if (docFlag !== null && docFlag !== undefined)
return Boolean(docFlag);
// Fallback for WebKit / iOS Safari, where the flag lives on the element itself.
const elFlag = (targetRef.value as any)?.[flag];
const elFlag = (targetRef.value as unknown as Record<string, unknown> | null | undefined)?.[flag];
if (elFlag !== null && elFlag !== undefined)
return Boolean(elFlag);
@@ -173,13 +173,15 @@ export function useFullscreen(
const method = exitMethod.value;
if (method) {
if (typeof (document as any)?.[method] === 'function')
await (document as any)[method]();
const docMethod = (document as unknown as Record<string, unknown> | undefined)?.[method];
if (isFunction(docMethod))
await docMethod.call(document);
else {
// Fallback for Safari iOS, where exit lives on the element.
const el = targetRef.value as any;
if (isFunction(el?.[method]))
await el[method]();
const el = targetRef.value as unknown as Record<string, unknown> | null | undefined;
const elMethod = el?.[method];
if (isFunction(elMethod))
await elMethod.call(targetRef.value);
}
}
@@ -193,10 +195,11 @@ export function useFullscreen(
if (isElementFullScreen())
await exit();
const el = targetRef.value as any;
const el = targetRef.value as unknown as Record<string, unknown> | null | undefined;
const method = requestMethod.value;
if (method && isFunction(el?.[method])) {
await el[method]();
const elMethod = method ? el?.[method] : undefined;
if (isFunction(elMethod)) {
await elMethod.call(targetRef.value);
isFullscreen.value = true;
}
}
@@ -18,7 +18,7 @@ const { isLoading, isReady, error, state, execute } = useImage(
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex flex-wrap gap-2">
<button
v-for="(sample, index) in samples"
@@ -26,15 +26,15 @@ const { isLoading, isReady, error, state, execute } = useImage(
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="current === index
? '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)'"
? '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="current = index"
>
{{ sample.label }}
</button>
</div>
<div class="relative aspect-[8/5] w-full overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset)">
<div class="relative aspect-[8/5] w-full overflow-hidden rounded-xl border border-border bg-bg-inset">
<Transition
enter-active-class="transition duration-300"
enter-from-class="opacity-0 scale-[1.02]"
@@ -61,8 +61,8 @@ const { isLoading, isReady, error, state, execute } = useImage(
v-else
class="absolute inset-0 flex flex-col items-center justify-center gap-3"
>
<span class="h-7 w-7 animate-spin rounded-full border-2 border-(--border-strong) border-t-(--accent)" />
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="h-7 w-7 animate-spin rounded-full border-2 border-border-strong border-t-accent" />
<p class="demo-label">
Loading
</p>
</div>
@@ -83,7 +83,7 @@ const { isLoading, isReady, error, state, execute } = useImage(
</span>
<span
v-if="isReady && state"
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) tabular-nums"
class="demo-badge tabular-nums"
>
{{ state.naturalWidth }}×{{ state.naturalHeight }}
</span>
@@ -91,7 +91,7 @@ const { isLoading, isReady, error, state, execute } = useImage(
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="isLoading"
@click="execute()"
>
@@ -40,7 +40,7 @@ export interface UseImageOptions {
export interface UseImageAsyncStateOptions
extends UseAsyncStateOptions<true, HTMLImageElement | undefined>, ConfigurableWindow {}
export type UseImageReturn = UseAsyncStateReturn<HTMLImageElement | undefined, any[], true>;
export type UseImageReturn = UseAsyncStateReturn<HTMLImageElement | undefined, [], true>;
interface LoadImageContext {
window?: Window;
@@ -34,14 +34,14 @@ function familyStyle(name: string) {
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex items-center justify-between gap-3">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<p class="demo-label">
Local Font Access
</p>
<span
v-if="fonts.length"
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) tabular-nums"
class="demo-badge tabular-nums"
>
{{ fonts.length }} faces · {{ familyCount }} families
</span>
@@ -57,7 +57,7 @@ function familyStyle(name: string) {
<template v-else>
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="loading"
@click="pickFonts"
>
@@ -76,28 +76,28 @@ function familyStyle(name: string) {
v-model="filter"
type="search"
placeholder="Filter by name…"
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)"
class="demo-input"
>
<ul class="max-h-56 divide-y divide-(--border) overflow-y-auto rounded-xl border border-(--border) bg-(--bg-elevated)">
<ul class="demo-card max-h-56 divide-y divide-border overflow-y-auto">
<li
v-for="font in filtered"
:key="font.postscriptName"
class="flex items-baseline justify-between gap-3 px-3 py-2"
>
<span
class="truncate text-base text-(--fg)"
class="truncate text-base text-fg"
:style="familyStyle(font.fullName)"
>
{{ font.fullName }}
</span>
<span class="shrink-0 font-mono text-xs text-(--fg-subtle)">
<span class="shrink-0 font-mono text-xs text-fg-subtle">
{{ font.style }}
</span>
</li>
<li
v-if="!filtered.length"
class="px-3 py-6 text-center text-sm text-(--fg-subtle)"
class="px-3 py-6 text-center text-sm text-fg-subtle"
>
No fonts match "{{ filter }}"
</li>
@@ -106,7 +106,7 @@ function familyStyle(name: string) {
<p
v-else-if="!error && !loading"
class="rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-6 text-center text-sm text-(--fg-subtle)"
class="rounded-lg border border-border bg-bg-inset px-3 py-6 text-center text-sm text-fg-subtle"
>
Click above to grant the <code class="font-mono">local-fonts</code> permission and list your fonts.
</p>
@@ -22,35 +22,35 @@ const queries = [
</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 text-center">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-md">
<div class="demo-card p-4 text-center">
<p class="demo-label">
Current layout
</p>
<p class="mt-1 font-mono text-3xl font-bold tabular-nums text-(--fg)">
<p class="demo-stat mt-1 text-3xl">
{{ breakpoint ? 'desktop' : isMedium ? 'tablet' : 'mobile' }}
</p>
<p class="mt-1 text-sm text-(--fg-muted)">
<p class="mt-1 text-sm text-fg-muted">
Resize the window to watch these queries flip live.
</p>
</div>
<ul class="divide-y divide-(--border) rounded-xl border border-(--border) bg-(--bg-elevated)">
<ul class="demo-card divide-y divide-border">
<li
v-for="query in queries"
:key="query.label"
class="flex items-center justify-between gap-3 px-3 py-2.5"
>
<code class="font-mono text-sm text-(--fg)">{{ query.label }}</code>
<code class="font-mono text-sm text-fg">{{ query.label }}</code>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
:class="query.match.value
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-subtle)'"
: 'border-border bg-bg-inset text-fg-subtle'"
>
<span
class="h-1.5 w-1.5 rounded-full"
:class="query.match.value ? 'bg-emerald-500' : 'bg-(--border-strong)'"
:class="query.match.value ? 'bg-emerald-500' : 'bg-border-strong'"
/>
{{ query.match.value ? 'matches' : 'no match' }}
</span>
@@ -5,6 +5,7 @@ import { defaultWindow } from '@/types';
import type { ConfigurableWindow } from '@/types';
import { useSupported } from '@/composables/utilities/useSupported';
import { useEventListener } from '@/composables/browser/useEventListener';
import { pxValue } from '@robonen/platform/browsers';
export interface UseMediaQueryOptions extends ConfigurableWindow {
/**
@@ -20,22 +21,6 @@ export interface UseMediaQueryOptions extends ConfigurableWindow {
ssrWidth?: number;
}
/**
* Convert a CSS length token (e.g. `"1024px"`, `"48em"`, `"30rem"`) to pixels.
* Falls back to treating `em`/`rem` as the conventional 16px root size.
*/
function pxValue(value: string): number {
const number = Number.parseFloat(value);
if (Number.isNaN(number))
return Number.NaN;
if (/(?:em|rem)\s*$/i.test(value))
return number * 16;
return number;
}
/**
* Best-effort evaluation of `min-width` / `max-width` media queries against a
* known viewport width, for SSR. Comma-separated queries are OR-combined and
@@ -44,19 +44,19 @@ function generateSample() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<label
class="flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed p-6 text-center transition"
:class="dragging
? 'border-(--accent) bg-(--accent-subtle)'
: 'border-(--border) bg-(--bg-inset) hover:border-(--border-strong)'"
? 'border-accent bg-accent-subtle'
: 'border-border bg-bg-inset hover:border-border-strong'"
@dragover.prevent="dragging = true"
@dragleave.prevent="dragging = false"
@drop.prevent="onDrop"
>
<span class="text-2xl">📎</span>
<span class="text-sm font-medium text-(--fg)">Drop a file or click to choose</span>
<span class="text-xs text-(--fg-subtle)">An object URL is created instantly</span>
<span class="text-sm font-medium text-fg">Drop a file or click to choose</span>
<span class="text-xs text-fg-subtle">An object URL is created instantly</span>
<input
type="file"
class="hidden"
@@ -67,14 +67,14 @@ function generateSample() {
<div class="flex gap-2">
<button
type="button"
class="inline-flex flex-1 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"
class="demo-btn flex-1"
@click="generateSample"
>
Use sample image
</button>
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!file"
@click="clear"
>
@@ -84,26 +84,26 @@ function generateSample() {
<div
v-if="file"
class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4"
class="demo-card flex flex-col gap-3 p-4"
>
<img
v-if="isImage && url"
:src="url"
alt="Selected file preview"
class="mx-auto max-h-40 rounded-lg border border-(--border) object-contain"
class="mx-auto max-h-40 rounded-lg border border-border object-contain"
>
<div class="flex items-center justify-between gap-3 text-sm">
<span class="truncate font-medium text-(--fg)">{{ file.name }}</span>
<span class="shrink-0 font-mono text-xs text-(--fg-muted) tabular-nums">{{ sizeLabel }}</span>
<span class="truncate font-medium text-fg">{{ file.name }}</span>
<span class="shrink-0 font-mono text-xs text-fg-muted tabular-nums">{{ sizeLabel }}</span>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg) break-all">
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg break-all">
{{ url }}
</div>
</div>
<p
v-else
class="rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-6 text-center text-sm text-(--fg-subtle)"
class="rounded-lg border border-border bg-bg-inset px-3 py-6 text-center text-sm text-fg-subtle"
>
No source the URL ref is <code class="font-mono">undefined</code>
</p>
@@ -41,9 +41,9 @@ function simulate() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">WebOTP</span>
<span class="demo-label">WebOTP</span>
<span
class="rounded-full px-2 py-0.5 text-xs font-medium"
:class="isSupported
@@ -61,27 +61,27 @@ function simulate() {
autocomplete="one-time-code"
maxlength="6"
placeholder="••••••"
class="w-full rounded-xl border border-(--border) bg-(--bg-inset) px-4 py-3 text-center font-mono text-2xl tracking-[0.4em] tabular-nums text-(--fg) outline-none transition focus:border-(--accent)"
class="w-full rounded-xl border border-border bg-bg-inset px-4 py-3 text-center font-mono text-2xl tracking-[0.4em] tabular-nums text-fg outline-none transition focus:border-accent"
@input="onInput"
>
<div class="flex gap-2">
<button
v-if="!isReceiving"
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg 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"
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg 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="listen"
>
Listen for code
</button>
<button
v-else
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) px-3 py-1.5 text-sm font-medium text-(--fg-muted) transition hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-fg-muted transition hover:border-border-strong active:scale-[0.98] cursor-pointer"
@click="abort()"
>
Cancel
</button>
<button
class="inline-flex items-center justify-center rounded-lg border border-(--border) px-3 py-1.5 text-sm font-medium text-(--fg-muted) transition hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="inline-flex items-center justify-center rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-fg-muted transition hover:border-border-strong active:scale-[0.98] cursor-pointer"
@click="simulate"
>
Simulate
@@ -92,13 +92,13 @@ function simulate() {
<span
class="size-2 rounded-full"
:class="{
'bg-(--fg-subtle)': status.tone === 'idle',
'bg-fg-subtle': status.tone === 'idle',
'animate-pulse bg-amber-500': status.tone === 'pending',
'bg-emerald-500': status.tone === 'ok',
'bg-red-500': status.tone === 'error',
}"
/>
<span class="text-(--fg-muted)">{{ status.label }}</span>
<span class="text-fg-muted">{{ status.label }}</span>
</div>
</div>
</template>
@@ -32,13 +32,13 @@ const meta = computed(() => {
case 'prompt':
return { label: 'prompt', dot: 'bg-amber-500', text: 'text-amber-600 dark:text-amber-400', ring: 'border-amber-500/30 bg-amber-500/10' };
default:
return { label: 'unknown', dot: 'bg-(--border-strong)', text: 'text-(--fg-subtle)', ring: 'border-(--border) bg-(--bg-inset)' };
return { label: 'unknown', dot: 'bg-border-strong', text: 'text-fg-subtle', ring: 'border-border bg-bg-inset' };
}
});
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
v-if="!isSupported"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-300"
@@ -48,12 +48,12 @@ const meta = computed(() => {
<template v-else>
<div class="flex flex-col gap-1.5">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<label class="demo-label">
Permission
</label>
<select
v-model="selected"
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)"
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 names" :key="name" :value="name">
{{ name }}
@@ -61,19 +61,19 @@ const meta = computed(() => {
</select>
</div>
<ul class="divide-y divide-(--border) rounded-xl border border-(--border) bg-(--bg-elevated)">
<ul class="demo-card divide-y divide-border">
<li
v-for="perm in permissions"
:key="perm.name"
class="flex items-center justify-between gap-3 px-3 py-2.5"
>
<code class="font-mono text-sm text-(--fg)">{{ perm.name }}</code>
<span class="font-mono text-xs text-(--fg-muted)">{{ perm.state.value ?? 'unknown' }}</span>
<code class="font-mono text-sm text-fg">{{ perm.name }}</code>
<span class="font-mono text-xs text-fg-muted">{{ perm.state.value ?? 'unknown' }}</span>
</li>
</ul>
<div class="flex flex-col items-center gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-card flex flex-col items-center gap-3 p-5">
<span class="demo-label">
{{ selected }}
</span>
<span
@@ -87,13 +87,13 @@ const meta = computed(() => {
<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"
class="demo-btn-primary"
@click="active.query()"
>
Re-check "{{ selected }}"
</button>
<p class="text-center text-xs text-(--fg-subtle)">
<p class="text-center text-xs text-fg-subtle">
Status updates live if you change a permission in your browser settings.
</p>
</template>
@@ -22,12 +22,12 @@ const previewClass = computed(() =>
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Preferred color scheme
</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="demo-badge">
<span class="size-1.5 rounded-full bg-emerald-500" />
live
</span>
@@ -39,22 +39,22 @@ const previewClass = computed(() =>
:key="option.value"
class="flex items-center gap-3 rounded-lg border px-3 py-2.5 transition"
:class="scheme === option.value
? 'border-(--accent) bg-(--accent-subtle)'
: 'border-(--border) bg-(--bg-elevated)'"
? 'border-accent bg-accent-subtle'
: 'border-border bg-bg-elevated'"
>
<span class="text-lg leading-none">{{ option.icon }}</span>
<span class="flex flex-1 flex-col">
<span
class="text-sm font-medium"
:class="scheme === option.value ? 'text-(--accent-text)' : 'text-(--fg)'"
:class="scheme === option.value ? 'text-accent-text' : 'text-fg'"
>
{{ option.label }}
</span>
<span class="font-mono text-xs text-(--fg-subtle)">{{ option.hint }}</span>
<span class="font-mono text-xs text-fg-subtle">{{ option.hint }}</span>
</span>
<span
v-if="scheme === option.value"
class="text-(--accent-text)"
class="text-accent-text"
aria-hidden="true"
></span>
</li>
@@ -73,7 +73,7 @@ const previewClass = computed(() =>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Read-only: change your OS appearance setting to see this update instantly.
</p>
</div>
@@ -17,22 +17,22 @@ const active = computed(() => levels.find(l => l.value === contrast.value));
const cardClass = computed(() => {
switch (contrast.value) {
case 'more':
return 'border-(--border-strong) bg-(--bg-inset)';
return 'border-border-strong bg-bg-inset';
case 'less':
return 'border-(--border) bg-(--bg-subtle) opacity-90';
return 'border-border bg-bg-subtle opacity-90';
default:
return 'border-(--border) bg-(--bg-elevated)';
return 'border-border bg-bg-elevated';
}
});
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Preferred contrast
</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="demo-badge">
{{ contrast }}
</span>
</div>
@@ -43,30 +43,30 @@ const cardClass = computed(() => {
:key="level.value"
class="flex flex-col gap-1 rounded-lg border px-3 py-2.5 transition"
:class="contrast === level.value
? 'border-(--accent) bg-(--accent-subtle)'
: 'border-(--border) bg-(--bg-elevated)'"
? 'border-accent bg-accent-subtle'
: 'border-border bg-bg-elevated'"
>
<span
class="text-sm font-medium"
:class="contrast === level.value ? 'text-(--accent-text)' : 'text-(--fg)'"
:class="contrast === level.value ? 'text-accent-text' : 'text-fg'"
>
{{ level.label }}
</span>
<span class="text-xs leading-snug text-(--fg-muted)">{{ level.desc }}</span>
<span class="text-xs leading-snug text-fg-muted">{{ level.desc }}</span>
</div>
</div>
<div class="rounded-xl border p-4 transition-colors" :class="cardClass">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Adaptive surface
</span>
<p class="mt-1 text-sm text-(--fg)">
<p class="mt-1 text-sm text-fg">
This card adjusts its borders and fill to match the
<span class="font-mono text-(--fg-muted)">{{ active?.query }}</span> level.
<span class="font-mono text-fg-muted">{{ active?.query }}</span> level.
</p>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Read-only: toggle your OS accessibility "increase / reduce contrast" setting to update.
</p>
</div>
@@ -8,9 +8,9 @@ const label = computed(() => (isDark.value ? 'Dark' : 'Light'));
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
prefers-color-scheme: dark
</span>
<span
@@ -25,7 +25,7 @@ const label = computed(() => (isDark.value ? 'Dark' : 'Light'));
<!-- A miniature sky scene that flips between day and night. -->
<div
class="relative flex h-40 items-end overflow-hidden rounded-xl border border-(--border) p-4 transition-colors duration-500"
class="relative flex h-40 items-end overflow-hidden rounded-xl border border-border p-4 transition-colors duration-500"
:class="isDark
? 'bg-gradient-to-b from-slate-900 to-slate-700'
: 'bg-gradient-to-b from-sky-300 to-sky-100'"
@@ -60,12 +60,12 @@ const label = computed(() => (isDark.value ? 'Dark' : 'Light'));
</span>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
const isDark = usePreferredDark()
<span class="text-(--fg-subtle)"> // </span>{{ isDark }}
<span class="text-fg-subtle"> // </span>{{ isDark }}
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Read-only: switch your OS to dark/light mode to watch the scene change.
</p>
</div>
@@ -30,25 +30,25 @@ const primary = computed(() => languages.value[0] ?? 'en');
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
navigator.languages
</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="demo-badge">
{{ languages.length }} {{ languages.length === 1 ? 'locale' : 'locales' }}
</span>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-card p-4">
<span class="demo-label">
Primary language
</span>
<div class="mt-1 flex items-center gap-2.5">
<span class="text-2xl leading-none">{{ flagOf(primary) }}</span>
<span class="flex flex-col">
<span class="text-base font-semibold text-(--fg)">{{ nameOf(primary) }}</span>
<span class="font-mono text-xs text-(--fg-subtle)">{{ primary }}</span>
<span class="text-base font-semibold text-fg">{{ nameOf(primary) }}</span>
<span class="font-mono text-xs text-fg-subtle">{{ primary }}</span>
</span>
</div>
</div>
@@ -57,26 +57,26 @@ const primary = computed(() => languages.value[0] ?? 'en');
<li
v-for="(lang, index) in languages"
:key="`${lang}-${index}`"
class="flex items-center gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
class="flex items-center gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
>
<span class="w-5 text-center font-mono text-xs text-(--fg-subtle) tabular-nums">
<span class="w-5 text-center font-mono text-xs text-fg-subtle tabular-nums">
{{ index + 1 }}
</span>
<span class="text-lg leading-none">{{ flagOf(lang) }}</span>
<span class="flex flex-1 flex-col">
<span class="text-sm font-medium text-(--fg)">{{ nameOf(lang) }}</span>
<span class="font-mono text-xs text-(--fg-subtle)">{{ lang }}</span>
<span class="text-sm font-medium text-fg">{{ nameOf(lang) }}</span>
<span class="font-mono text-xs text-fg-subtle">{{ lang }}</span>
</span>
<span
v-if="index === 0"
class="inline-flex items-center rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
class="inline-flex items-center rounded-md border border-border bg-bg-inset px-2 py-0.5 text-xs font-medium text-fg-muted"
>
preferred
</span>
</li>
</ol>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Read-only: updates automatically on the browser's <span class="font-mono">languagechange</span> event.
</p>
</div>
@@ -12,9 +12,9 @@ const duration = computed(() => (reduced.value ? 0 : 1.2));
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
prefers-reduced-motion
</span>
<span
@@ -28,14 +28,14 @@ const duration = computed(() => (reduced.value ? 0 : 1.2));
</div>
<!-- Animated demo box: the orbiting dot pauses when motion is reduced. -->
<div class="flex h-40 items-center justify-center rounded-xl border border-(--border) bg-(--bg-inset)">
<div class="flex h-40 items-center justify-center rounded-xl border border-border bg-bg-inset">
<div class="relative size-24">
<div class="absolute inset-0 rounded-full border-2 border-dashed border-(--border-strong)" />
<div class="absolute inset-0 rounded-full border-2 border-dashed border-border-strong" />
<div
class="orbit absolute left-1/2 top-1/2 size-24"
:style="{ animationDuration: `${duration}s`, animationPlayState: reduced ? 'paused' : 'running' }"
>
<span class="absolute -left-2 -top-2 size-4 rounded-full bg-(--accent) shadow-lg" />
<span class="absolute -left-2 -top-2 size-4 rounded-full bg-accent shadow-lg" />
</div>
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-2xl">{{ reduced ? '⏸' : '🎞️' }}</span>
@@ -43,24 +43,24 @@ const duration = computed(() => (reduced.value ? 0 : 1.2));
</div>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-card p-4">
<span class="demo-label">
Derived setting
</span>
<div class="mt-1 flex items-baseline gap-2">
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
<span class="demo-stat text-3xl">
{{ reduced ? 0 : 1200 }}
</span>
<span class="text-sm text-(--fg-muted)">ms transition</span>
<span class="text-sm text-fg-muted">ms transition</span>
</div>
<p class="mt-2 text-sm text-(--fg-muted)">
<p class="mt-2 text-sm text-fg-muted">
{{ reduced
? 'Reduced motion requested — animations are disabled.'
: 'Full motion — animations run at normal speed.' }}
</p>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Read-only: enable "Reduce motion" in your OS accessibility settings to pause the orbit.
</p>
</div>
@@ -8,9 +8,9 @@ const isReduced = computed(() => transparency.value === 'reduce');
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
prefers-reduced-transparency
</span>
<span
@@ -28,23 +28,23 @@ const isReduced = computed(() => transparency.value === 'reduce');
</div>
<!-- Live preview: a frosted glass panel that flattens when reduce is preferred -->
<div class="relative overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset) p-4">
<div class="relative overflow-hidden rounded-xl border border-border bg-bg-inset p-4">
<div
class="pointer-events-none absolute -left-6 -top-8 size-28 rounded-full bg-(--accent) opacity-60 blur-xl"
class="pointer-events-none absolute -left-6 -top-8 size-28 rounded-full bg-accent opacity-60 blur-xl"
/>
<div
class="pointer-events-none absolute -bottom-10 right-2 size-24 rounded-full bg-sky-500 opacity-50 blur-xl"
/>
<div
class="relative rounded-lg border border-(--border) p-4 transition"
class="relative rounded-lg border border-border p-4 transition"
:class="isReduced
? 'bg-(--bg-elevated)'
: 'bg-(--bg-elevated)/60 backdrop-blur-md'"
? 'bg-bg-elevated'
: 'bg-bg-elevated/60 backdrop-blur-md'"
>
<p class="text-sm font-medium text-(--fg)">
<p class="text-sm font-medium text-fg">
Glass card
</p>
<p class="mt-1 text-sm text-(--fg-muted)">
<p class="mt-1 text-sm text-fg-muted">
{{ isReduced
? 'Translucency removed for clarity.'
: 'Background blurs through the panel.' }}
@@ -52,8 +52,8 @@ const isReduced = computed(() => transparency.value === 'reduce');
</div>
</div>
<p class="text-xs leading-relaxed text-(--fg-subtle)">
Toggle <span class="font-mono text-(--fg-muted)">Reduce transparency</span> in your OS
<p class="text-xs leading-relaxed text-fg-subtle">
Toggle <span class="font-mono text-fg-muted">Reduce transparency</span> in your OS
accessibility settings to see this update live.
</p>
</div>
@@ -35,7 +35,7 @@ function onUnload() {
}
const statusStyles = {
idle: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)',
idle: 'border-border bg-bg-inset text-fg-muted',
loading: 'border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-400',
loaded: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
error: 'border-red-500/30 bg-red-500/10 text-red-600 dark:text-red-400',
@@ -43,10 +43,10 @@ const statusStyles = {
</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">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4">
<div class="flex items-center justify-between gap-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Script status
</span>
<span
@@ -56,7 +56,7 @@ const statusStyles = {
<span
class="size-1.5 rounded-full"
:class="{
'bg-(--fg-subtle)': status === 'idle',
'bg-fg-subtle': status === 'idle',
'bg-sky-500 animate-pulse': status === 'loading',
'bg-emerald-500': status === 'loaded',
'bg-red-500': status === 'error',
@@ -66,24 +66,24 @@ const statusStyles = {
</span>
</div>
<div class="mt-3 truncate rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg-muted)">
<div class="mt-3 truncate rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg-muted">
{{ src }}
</div>
<dl class="mt-3 grid grid-cols-2 gap-2 text-sm">
<div>
<dt class="text-xs text-(--fg-subtle)">
<dt class="text-xs text-fg-subtle">
&lt;script&gt; element
</dt>
<dd class="font-mono text-(--fg)">
<dd class="font-mono text-fg">
{{ scriptTag ? 'present' : 'null' }}
</dd>
</div>
<div>
<dt class="text-xs text-(--fg-subtle)">
<dt class="text-xs text-fg-subtle">
Loaded at
</dt>
<dd class="font-mono text-(--fg)">
<dd class="font-mono text-fg">
{{ loadedAt ?? '—' }}
</dd>
</div>
@@ -94,7 +94,7 @@ const statusStyles = {
<button
type="button"
:disabled="status === 'loading' || status === 'loaded'"
class="inline-flex flex-1 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"
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="onLoad"
>
{{ status === 'loading' ? 'Loading…' : 'Load script' }}
@@ -102,15 +102,15 @@ const statusStyles = {
<button
type="button"
:disabled="status !== 'loaded'"
class="inline-flex flex-1 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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="onUnload"
>
Unload
</button>
</div>
<p class="text-xs leading-relaxed text-(--fg-subtle)">
Manual mode the tag is injected into <span class="font-mono text-(--fg-muted)">&lt;head&gt;</span>
<p class="text-xs leading-relaxed text-fg-subtle">
Manual mode the tag is injected into <span class="font-mono text-fg-muted">&lt;head&gt;</span>
only when you click, and removed on unload.
</p>
</div>
@@ -26,48 +26,48 @@ async function onShare() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Web Share API
</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="isSupported
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
<span
class="size-1.5 rounded-full"
:class="isSupported ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
:class="isSupported ? 'bg-emerald-500' : 'bg-fg-subtle'"
/>
{{ isSupported ? 'Supported' : 'Unsupported' }}
</span>
</div>
<div class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="demo-card flex flex-col gap-3 p-4">
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Title</span>
<span class="demo-label">Title</span>
<input
v-model="payload.title"
type="text"
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)"
class="demo-input"
>
</label>
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Text</span>
<span class="demo-label">Text</span>
<input
v-model="payload.text"
type="text"
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)"
class="demo-input"
>
</label>
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">URL</span>
<span class="demo-label">URL</span>
<input
v-model="payload.url"
type="url"
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)"
class="demo-input"
>
</label>
</div>
@@ -75,7 +75,7 @@ async function onShare() {
<button
type="button"
:disabled="!isSupported"
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"
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="onShare"
>
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@@ -88,7 +88,7 @@ async function onShare() {
Share
</button>
<p v-if="!isSupported" class="text-xs leading-relaxed text-(--fg-subtle)">
<p v-if="!isSupported" class="text-xs leading-relaxed text-fg-subtle">
The Web Share API is not available in this browser. It works on most mobile
browsers and Safari.
</p>
@@ -97,7 +97,7 @@ async function onShare() {
class="text-xs font-medium"
:class="lastResult === 'shared'
? 'text-emerald-600 dark:text-emerald-400'
: 'text-(--fg-muted)'"
: 'text-fg-muted'"
>
{{ lastResult === 'shared' ? 'Content shared.' : 'Share sheet dismissed.' }}
</p>
@@ -11,41 +11,41 @@ const { id, css, isLoaded, load, unload } = useStyleTag(initialCss, { immediate:
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Injected style tag
</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="isLoaded
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
<span
class="size-1.5 rounded-full"
:class="isLoaded ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
:class="isLoaded ? 'bg-emerald-500' : 'bg-fg-subtle'"
/>
{{ isLoaded ? 'Loaded' : 'Unloaded' }}
</span>
</div>
<!-- Live target affected by the injected stylesheet -->
<div class="rounded-xl border border-(--border) bg-(--bg-inset) p-4">
<div class="rounded-xl border border-border bg-bg-inset p-4">
<div
class="styletag-demo-box rounded-lg border border-(--border) bg-(--bg-elevated) px-4 py-6 text-center text-sm font-semibold text-(--fg) transition-all"
class="styletag-demo-box rounded-lg border border-border bg-bg-elevated px-4 py-6 text-center text-sm font-semibold text-fg transition-all"
>
Styled by &lt;style id="{{ id }}"&gt;
</div>
</div>
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">CSS source</span>
<span class="demo-label">CSS source</span>
<textarea
v-model="css"
rows="5"
spellcheck="false"
class="w-full resize-none rounded-lg border border-(--border) bg-(--bg) px-3 py-2 font-mono text-xs leading-relaxed text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
class="w-full resize-none rounded-lg border border-border bg-bg px-3 py-2 font-mono text-xs leading-relaxed text-fg placeholder:text-fg-subtle transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
/>
</label>
@@ -53,7 +53,7 @@ const { id, css, isLoaded, load, unload } = useStyleTag(initialCss, { immediate:
<button
type="button"
:disabled="isLoaded"
class="inline-flex flex-1 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"
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="load"
>
Load
@@ -61,14 +61,14 @@ const { id, css, isLoaded, load, unload } = useStyleTag(initialCss, { immediate:
<button
type="button"
:disabled="!isLoaded"
class="inline-flex flex-1 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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="unload"
>
Unload
</button>
</div>
<p class="text-xs leading-relaxed text-(--fg-subtle)">
<p class="text-xs leading-relaxed text-fg-subtle">
Editing the CSS updates the live stylesheet instantly while loaded.
</p>
</div>
@@ -5,20 +5,20 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Web Locks election
</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="isSupported
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
<span
class="size-1.5 rounded-full"
:class="isSupported ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
:class="isSupported ? 'bg-emerald-500' : 'bg-fg-subtle'"
/>
{{ isSupported ? 'Supported' : 'Unsupported' }}
</span>
@@ -28,14 +28,14 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
<div
class="flex flex-col items-center gap-2 rounded-xl border p-6 transition-colors"
:class="isLeader
? 'border-(--accent) bg-(--accent-subtle)'
: 'border-(--border) bg-(--bg-elevated)'"
? 'border-accent bg-accent-subtle'
: 'border-border bg-bg-elevated'"
>
<div
class="flex size-12 items-center justify-center rounded-full transition-colors"
:class="isLeader
? 'bg-(--accent) text-(--accent-fg)'
: 'bg-(--bg-inset) text-(--fg-subtle)'"
? 'bg-accent text-accent-fg'
: 'bg-bg-inset text-fg-subtle'"
>
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m12 2 3 7h7l-5.5 4.5L18.5 21 12 16.5 5.5 21 7.5 13.5 2 9h7z" />
@@ -43,11 +43,11 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
</div>
<p
class="text-lg font-bold"
:class="isLeader ? 'text-(--accent-text)' : 'text-(--fg)'"
:class="isLeader ? 'text-accent-text' : 'text-fg'"
>
{{ isLeader ? 'Leader tab' : 'Follower tab' }}
</p>
<p class="text-center text-xs text-(--fg-muted)">
<p class="text-center text-xs text-fg-muted">
{{ isLeader
? 'This tab holds the lock and would run exclusive work.'
: 'Another tab is the leader, or leadership was released.' }}
@@ -58,7 +58,7 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
<button
type="button"
:disabled="!isSupported || isLeader"
class="inline-flex flex-1 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"
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="acquire"
>
Acquire
@@ -66,17 +66,17 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
<button
type="button"
:disabled="!isSupported || !isLeader"
class="inline-flex flex-1 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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="release"
>
Release
</button>
</div>
<p v-if="!isSupported" class="text-xs leading-relaxed text-(--fg-subtle)">
<p v-if="!isSupported" class="text-xs leading-relaxed text-fg-subtle">
The Web Locks API is not available in this browser.
</p>
<p v-else class="text-xs leading-relaxed text-(--fg-subtle)">
<p v-else class="text-xs leading-relaxed text-fg-subtle">
Open this page in a second tab only one tab is the leader at a time. Release
here and watch another tab take over.
</p>
@@ -28,9 +28,9 @@ function clear(): void {
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex flex-col gap-1.5">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<label class="demo-label">
Auto-growing textarea
</label>
<textarea
@@ -38,16 +38,16 @@ function clear(): void {
v-model="input"
placeholder="Start typing…"
rows="1"
class="w-full resize-none overflow-y-auto rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm leading-relaxed text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
class="demo-input resize-none overflow-y-auto leading-relaxed"
/>
</div>
<div class="flex flex-col gap-2 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="demo-card flex flex-col gap-2 p-4">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Max height
</span>
<span class="font-mono text-sm tabular-nums text-(--fg)">{{ maxHeight }}px</span>
<span class="font-mono text-sm tabular-nums text-fg">{{ maxHeight }}px</span>
</div>
<input
v-model.number="maxHeight"
@@ -55,11 +55,11 @@ function clear(): void {
min="80"
max="400"
step="20"
class="w-full accent-(--accent)"
class="w-full accent-accent"
>
<div class="flex items-center justify-between border-t border-(--border) pt-2 text-xs">
<span class="text-(--fg-muted)">{{ input.length }} chars</span>
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 font-medium text-(--fg-muted)">
<div class="flex items-center justify-between border-t border-border pt-2 text-xs">
<span class="text-fg-muted">{{ input.length }} chars</span>
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg-inset px-2 py-0.5 font-medium text-fg-muted">
{{ resizes }} resizes
</span>
</div>
@@ -68,21 +68,21 @@ function clear(): void {
<div class="flex flex-wrap gap-2">
<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"
class="demo-btn-primary"
@click="loadSample"
>
Load sample
</button>
<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"
class="demo-btn"
@click="triggerResize"
>
Trigger resize
</button>
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!input"
@click="clear"
>
@@ -19,42 +19,42 @@ watch(appName, () => {
</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">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4">
<span class="demo-label">
Live document title
</span>
<div class="mt-2 flex items-center gap-2 rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2.5">
<svg viewBox="0 0 24 24" fill="none" class="size-4 shrink-0 text-(--fg-subtle)">
<div class="mt-2 flex items-center gap-2 rounded-lg border border-border bg-bg-inset px-3 py-2.5">
<svg viewBox="0 0 24 24" fill="none" class="size-4 shrink-0 text-fg-subtle">
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" />
<path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" stroke="currentColor" stroke-width="1.5" />
</svg>
<span class="truncate font-mono text-sm text-(--fg)">
<span class="truncate font-mono text-sm text-fg">
{{ title || 'Untitled' }} · {{ appName }}
</span>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<label class="demo-label">
Page title
</label>
<input
v-model="title"
type="text"
placeholder="Enter a page title"
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)"
class="demo-input"
>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<label class="demo-label">
App name (template suffix)
</label>
<input
v-model="appName"
type="text"
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)"
class="demo-input"
>
</div>
@@ -63,15 +63,15 @@ watch(appName, () => {
v-for="preset in presets"
:key="preset"
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"
:class="title === preset ? 'border-(--accent) text-(--accent-text)' : ''"
class="demo-btn"
:class="title === preset ? 'border-accent text-accent-text' : ''"
@click="title = preset"
>
{{ preset }}
</button>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Check your browser tab it updates in real time.
</p>
</div>
@@ -44,21 +44,21 @@ const queryString = computed(() => {
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex flex-col gap-1.5">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<label class="demo-label">
Search query
</label>
<input
v-model="params.q"
type="text"
placeholder="Search…"
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)"
class="demo-input"
>
</div>
<div class="flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Sort by</span>
<span class="demo-label">Sort by</span>
<div class="flex flex-wrap gap-2">
<button
v-for="s in sorts"
@@ -66,8 +66,8 @@ const queryString = computed(() => {
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="params.sort === s
? '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)'"
? '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="params.sort = s"
>
{{ s }}
@@ -76,7 +76,7 @@ const queryString = computed(() => {
</div>
<div class="flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Tags (repeated keys array)
</span>
<div class="flex flex-wrap gap-2">
@@ -86,8 +86,8 @@ const queryString = computed(() => {
type="button"
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition cursor-pointer"
:class="activeTags.includes(tag)
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted) hover:border-(--border-strong)'"
? 'border-accent bg-accent-subtle text-accent-text'
: 'border-border bg-bg-inset text-fg-muted hover:border-border-strong'"
@click="toggleTag(tag)"
>
#{{ tag }}
@@ -96,13 +96,13 @@ const queryString = computed(() => {
</div>
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Live URL query
</span>
<div class="overflow-x-auto rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="overflow-x-auto rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
<span class="whitespace-nowrap">{{ queryString }}</span>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
The browser address bar updates as you edit. Falsy values are dropped.
</p>
</div>
@@ -54,6 +54,9 @@ export interface UseUrlSearchParamsOptions<T> extends ConfigurableWindow {
stringify?: (params: URLSearchParams) => string;
}
// `Record<string, any>` is the idiomatic "any object shape" constraint here: `T` is
// caller-supplied and flows straight back out, so an interface (no implicit index
// signature) must still satisfy the bound — `Record<string, unknown>` would reject those.
export type UseUrlSearchParamsReturn<T extends Record<string, any> = UrlParams>
= T;
@@ -87,6 +90,8 @@ export type UseUrlSearchParamsReturn<T extends Record<string, any> = UrlParams>
*
* @since 0.0.15
*/
// `Record<string, any>` constraint mirrors `UseUrlSearchParamsReturn`: caller-supplied
// `T` flows back out, so interface types must satisfy the bound (see note above).
export function useUrlSearchParams<T extends Record<string, any> = UrlParams>(
mode: UrlSearchParamsMode = 'history',
options: UseUrlSearchParamsOptions<T> = {},
@@ -104,7 +109,7 @@ export function useUrlSearchParams<T extends Record<string, any> = UrlParams>(
if (!window)
return reactive({ ...initialValue }) as UseUrlSearchParamsReturn<T>;
const state = reactive<Record<string, any>>({});
const state = reactive<Record<string, string | string[] | null | undefined>>({});
const getRawParams = (): string => {
if (mode === 'history')
@@ -160,7 +165,9 @@ export function useUrlSearchParams<T extends Record<string, any> = UrlParams>(
else if (removeFalsyValues && !value)
params.delete(key);
else
params.set(key, value);
// `set` coerces to string at runtime; do it explicitly so null/undefined (when
// not stripped above) match the WHATWG ToString behaviour the old `any` masked.
params.set(key, String(value));
}
return params;
};
@@ -32,7 +32,7 @@ function toggleLoop(): void {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
v-if="!isSupported"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-300"
@@ -40,27 +40,27 @@ function toggleLoop(): void {
The Vibration API is not supported in this browser. Try a mobile device.
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="demo-card p-4">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Current pattern (ms)
</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="isSupported
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
{{ isSupported ? 'supported' : 'unsupported' }}
</span>
</div>
<div class="mt-2 overflow-x-auto rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="mt-2 overflow-x-auto rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
[{{ Array.isArray(pattern) ? pattern.join(', ') : pattern }}]
</div>
</div>
<div class="flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Presets</span>
<span class="demo-label">Presets</span>
<div class="grid grid-cols-2 gap-2">
<button
v-for="(_, name) in presets"
@@ -69,8 +69,8 @@ function toggleLoop(): void {
:disabled="!isSupported"
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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:class="activePreset === name
? '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)'"
? '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="applyPreset(name)"
>
{{ name }}
@@ -82,7 +82,7 @@ function toggleLoop(): void {
<button
type="button"
:disabled="!isSupported"
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"
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="vibrate()"
>
Vibrate now
@@ -90,7 +90,7 @@ function toggleLoop(): void {
<button
type="button"
:disabled="!isSupported"
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"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="toggleLoop"
>
{{ looping ? 'Stop loop' : 'Loop every 1.5s' }}
@@ -98,7 +98,7 @@ function toggleLoop(): void {
<button
type="button"
:disabled="!isSupported"
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"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="stop"
>
Stop
@@ -21,7 +21,7 @@ async function toggle(): Promise<void> {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
v-if="!isSupported"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-300"
@@ -29,12 +29,12 @@ async function toggle(): Promise<void> {
The Screen Wake Lock API is not supported in this browser.
</div>
<div class="flex flex-col items-center gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-6">
<div class="demo-card flex flex-col items-center gap-3 p-6">
<div
class="flex size-16 items-center justify-center rounded-full border transition"
:class="isActive
? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-subtle)'"
: 'border-border bg-bg-inset text-fg-subtle'"
>
<svg viewBox="0 0 24 24" fill="none" class="size-7">
<rect x="4" y="3" width="16" height="14" rx="2" stroke="currentColor" stroke-width="1.5" />
@@ -43,18 +43,18 @@ async function toggle(): Promise<void> {
</svg>
</div>
<div class="text-center">
<p class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
<p class="demo-stat text-3xl">
{{ isActive ? 'AWAKE' : 'IDLE' }}
</p>
<p class="mt-1 text-xs text-(--fg-muted)">
<p class="mt-1 text-xs text-fg-muted">
{{ isActive ? 'Screen will stay on' : 'Screen may sleep normally' }}
</p>
</div>
</div>
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2.5 text-sm">
<span class="text-(--fg-muted)">Lock held</span>
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-elevated) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset px-3 py-2.5 text-sm">
<span class="text-fg-muted">Lock held</span>
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg-elevated px-2 py-0.5 text-xs font-medium text-fg-muted">
{{ sentinel ? 'yes' : 'none' }}
</span>
</div>
@@ -62,7 +62,7 @@ async function toggle(): Promise<void> {
<button
type="button"
:disabled="!isSupported"
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"
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
@click="toggle"
>
{{ sentinel ? 'Release wake lock' : 'Request wake lock' }}
@@ -71,7 +71,7 @@ async function toggle(): Promise<void> {
<p v-if="error" class="text-xs text-red-600 dark:text-red-400">
{{ error }}
</p>
<p v-else class="text-xs text-(--fg-subtle)">
<p v-else class="text-xs text-fg-subtle">
The lock auto-releases when the tab is hidden and re-acquires when visible again.
</p>
</div>
@@ -39,7 +39,7 @@ function notify() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
v-if="!isSupported"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-400"
@@ -48,21 +48,21 @@ function notify() {
</div>
<template v-else>
<div class="flex items-center justify-between rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="demo-card flex items-center justify-between p-4">
<div>
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-label">
Permission
</div>
<div class="mt-1 flex items-center gap-2 text-sm text-(--fg-muted)">
<div class="mt-1 flex items-center gap-2 text-sm text-fg-muted">
<span
class="inline-block size-2 rounded-full transition"
:class="permissionGranted ? 'bg-emerald-500' : 'bg-(--border-strong)'"
:class="permissionGranted ? 'bg-emerald-500' : 'bg-border-strong'"
/>
{{ permissionGranted ? 'Granted' : 'Not granted' }}
</div>
</div>
<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"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="permissionGranted"
@click="requestPermission"
>
@@ -72,33 +72,33 @@ function notify() {
<div class="flex flex-col gap-3">
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Title</span>
<span class="demo-label">Title</span>
<input
v-model="title"
type="text"
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)"
class="demo-input"
>
</label>
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Body</span>
<span class="demo-label">Body</span>
<textarea
v-model="body"
rows="2"
class="w-full resize-none 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)"
class="demo-input resize-none"
/>
</label>
</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"
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!permissionGranted"
@click="notify"
>
Show notification
</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"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!notification"
@click="close"
>
@@ -106,12 +106,12 @@ function notify() {
</button>
</div>
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-sm">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Last event</span>
<span class="font-mono text-(--fg)">{{ lastEvent || '—' }}</span>
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3 text-sm">
<span class="demo-label">Last event</span>
<span class="font-mono text-fg">{{ lastEvent || '—' }}</span>
</div>
<p v-if="!permissionGranted" class="text-xs text-(--fg-subtle)">
<p v-if="!permissionGranted" class="text-xs text-fg-subtle">
Grant access first, then trigger a notification. Switch back to this tab and it auto-closes.
</p>
</template>
@@ -26,14 +26,14 @@ function toggle(member: Member) {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<!-- Templates are captured here, rendered wherever Reuse* appears -->
<DefineStat v-slot="{ label, value }">
<div class="flex-1 rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="flex-1 rounded-lg border border-border bg-bg-inset p-3">
<div class="demo-label">
{{ label }}
</div>
<div class="mt-1 font-mono text-2xl font-bold tabular-nums text-(--fg)">
<div class="demo-stat mt-1 text-2xl">
{{ value }}
</div>
</div>
@@ -43,14 +43,14 @@ function toggle(member: Member) {
<div class="flex items-center gap-3">
<span
class="inline-block size-2 shrink-0 rounded-full transition"
:class="online ? 'bg-emerald-500' : 'bg-(--border-strong)'"
:class="online ? 'bg-emerald-500' : 'bg-border-strong'"
/>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium text-(--fg)">{{ name }}</div>
<div class="text-xs text-(--fg-subtle)">{{ role }}</div>
<div class="truncate text-sm font-medium text-fg">{{ name }}</div>
<div class="text-xs text-fg-subtle">{{ role }}</div>
</div>
<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)"
class="demo-badge"
>
{{ online ? 'Online' : 'Away' }}
</span>
@@ -62,14 +62,14 @@ function toggle(member: Member) {
<ReuseStat label="Online" :value="String(team.filter(m => m.online).length)" />
</div>
<div class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-card flex flex-col gap-3 p-4">
<div class="demo-label">
Team click a row to toggle status
</div>
<button
v-for="member in team"
:key="member.name"
class="rounded-lg p-2 text-left transition hover:bg-(--bg-inset) active:scale-[0.99] cursor-pointer"
class="rounded-lg p-2 text-left transition hover:bg-bg-inset active:scale-[0.99] cursor-pointer"
@click="toggle(member)"
>
<ReuseMember
@@ -80,7 +80,7 @@ function toggle(member: Member) {
</button>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Both card and row markup are declared once via <code class="font-mono">DefineTemplate</code> and
rendered from multiple <code class="font-mono">ReuseTemplate</code> call sites.
</p>
@@ -1,16 +1,26 @@
import type { ComponentObjectPropsOptions, DefineComponent, Slot } from 'vue';
import { camelize, defineComponent, shallowRef } from 'vue';
/** Map of slot name -> slot props object (or `undefined` for prop-less slots) */
/**
* Map of slot name -> slot props object (or `undefined` for prop-less slots).
* The inner `Record<string, any>` is the idiomatic "any slot-props shape" bound:
* interface-typed slot props (which lack an implicit index signature) must satisfy
* it, so `Record<string, unknown>` would wrongly reject legitimate callers.
*/
type SlotPropsMap = Record<string, Record<string, any> | undefined>;
/** Turn a {@link SlotPropsMap} into a record of typed `Slot`s */
type GenerateSlotsFromSlotMap<T extends SlotPropsMap>
= { [K in keyof T]: Slot<T[K]> };
// `Bindings extends Record<string, any>` is the idiomatic "any object shape" bound,
// matching Vue/Reka's own component-binding generics: an interface-typed `Bindings`
// must satisfy it, which `Record<string, unknown>` would reject. Applies to every
// `extends Record<string, any>` constraint in this file.
export type DefineTemplateComponent<Bindings extends Record<string, any>, Slots extends SlotPropsMap>
= DefineComponent & (new () => {
$slots: {
// Slot render fn: returns `any` to match Vue's own `Slot` return type.
default: (_: Bindings & { $slots: GenerateSlotsFromSlotMap<Slots> }) => any;
};
});
@@ -49,8 +59,8 @@ export interface CreateReusableTemplateOptions<Props extends Record<string, any>
}
/** Re-key an attrs object so every key is camelCased */
function keysToCamelCase(obj: Record<string, any>): Record<string, any> {
const result: Record<string, any> = {};
function keysToCamelCase(obj: Record<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const key in obj)
result[camelize(key)] = obj[key];
@@ -21,10 +21,10 @@ function measure() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
ref="box"
class="flex items-center justify-center rounded-xl border border-dashed border-(--border-strong) bg-(--bg-inset) py-8 text-sm font-medium text-(--fg-muted) transition-[width] duration-300 ease-out"
class="flex items-center justify-center rounded-xl border border-dashed border-border-strong bg-bg-inset py-8 text-sm font-medium text-fg-muted transition-[width] duration-300 ease-out"
:style="{ width: `${width}px` }"
>
Target element
@@ -32,37 +32,37 @@ function measure() {
<label class="flex flex-col gap-1.5">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Width</span>
<span class="font-mono text-sm tabular-nums text-(--fg-muted)">{{ width }}px</span>
<span class="demo-label">Width</span>
<span class="font-mono text-sm tabular-nums text-fg-muted">{{ width }}px</span>
</div>
<input
v-model.number="width"
type="range"
min="120"
max="340"
class="w-full accent-(--accent)"
class="w-full accent-accent"
>
</label>
<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"
class="demo-btn-primary"
@click="measure"
>
Read element via unrefElement
</button>
<div class="flex flex-col gap-2 rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="flex flex-col gap-2 rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
<div class="flex items-center justify-between">
<span class="text-(--fg-subtle)">tagName</span>
<span class="text-fg-subtle">tagName</span>
<span>{{ tag }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-(--fg-subtle)">boundingRect</span>
<span class="text-fg-subtle">boundingRect</span>
<span>{{ rect ? `${rect.w} × ${rect.h}` : '—' }}</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Resize the box, then measure. <code class="font-mono">unrefElement</code> unwraps the template
ref to the real DOM node it also resolves a component ref to its <code class="font-mono">$el</code>.
</p>
@@ -29,10 +29,10 @@ const chips = ['vue', 'reactivity', 'composables', 'ssr', 'typescript', 'dom'];
<template>
<div
class="flex w-full max-w-sm flex-col gap-4 rounded-xl border border-(--border) bg-(--bg-elevated)"
class="demo-stack demo-card max-w-sm"
:style="{ padding: `${padding}px` }"
>
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-label">
Live measurement of this component's root
</div>
@@ -41,7 +41,7 @@ const chips = ['vue', 'reactivity', 'composables', 'ssr', 'typescript', 'dom'];
v-for="chip in chips.slice(0, childCount)"
:key="chip"
data-chip
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)"
class="demo-badge"
>
{{ chip }}
</span>
@@ -49,28 +49,28 @@ const chips = ['vue', 'reactivity', 'composables', 'ssr', 'typescript', 'dom'];
<label class="flex flex-col gap-1.5">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Root padding</span>
<span class="font-mono text-sm tabular-nums text-(--fg-muted)">{{ padding }}px</span>
<span class="demo-label">Root padding</span>
<span class="font-mono text-sm tabular-nums text-fg-muted">{{ padding }}px</span>
</div>
<input
v-model.number="padding"
type="range"
min="8"
max="40"
class="w-full accent-(--accent)"
class="w-full accent-accent"
>
</label>
<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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="childCount <= 1"
@click="childCount--"
>
Remove chip
</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"
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="childCount >= chips.length"
@click="childCount++"
>
@@ -78,22 +78,22 @@ const chips = ['vue', 'reactivity', 'composables', 'ssr', 'typescript', 'dom'];
</button>
</div>
<div class="flex flex-col gap-2 rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="flex flex-col gap-2 rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
<div class="flex items-center justify-between">
<span class="text-(--fg-subtle)">el.value</span>
<span class="text-fg-subtle">el.value</span>
<span>{{ info ? `<${info.tag}>` : 'undefined' }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-(--fg-subtle)">chips in DOM</span>
<span class="text-fg-subtle">chips in DOM</span>
<span>{{ info?.children ?? '' }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-(--fg-subtle)">root height</span>
<span class="text-fg-subtle">root height</span>
<span>{{ info ? `${info.height}px` : '' }}</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
The computed re-reads <code class="font-mono">$el</code> on every update, so the readout tracks
padding and chip changes automatically.
</p>
@@ -19,7 +19,7 @@ const FieldWrapper = defineComponent({
// expose the resolved element so the demo can show it changed live
'data-tag': currentElement.value?.tagName.toLowerCase(),
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)',
'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',
});
},
});
@@ -49,13 +49,13 @@ function fillSample() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-sm">
<div class="demo-card flex flex-col gap-3 p-4">
<div class="demo-label">
Wrapper component
</div>
<FieldWrapper ref="field" />
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
<code class="font-mono">&lt;FieldWrapper&gt;</code> renders an inner input, but
<code class="font-mono">useForwardExpose</code> makes its <code class="font-mono">$el</code>
resolve to that input.
@@ -64,31 +64,31 @@ function fillSample() {
<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"
class="demo-btn-primary flex-1"
@click="focusForwarded"
>
Focus forwarded $el
</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"
class="demo-btn"
@click="fillSample"
>
Fill sample
</button>
</div>
<div class="flex flex-col gap-2 rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="flex flex-col gap-2 rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
<div class="flex items-center justify-between">
<span class="text-(--fg-subtle)">field.$el</span>
<span class="text-fg-subtle">field.$el</span>
<span>{{ resolved ? `<${resolved.tag}>` : 'undefined' }}</span>
</div>
<div class="flex items-center justify-between gap-3">
<span class="text-(--fg-subtle)">value</span>
<span class="text-fg-subtle">value</span>
<span class="truncate">{{ resolved?.value || '""' }}</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
The parent never touches the input directly it holds a ref to the wrapper, whose
<code class="font-mono">$el</code> is forwarded to the inner element.
</p>
@@ -53,7 +53,7 @@ export function useForwardExpose<T extends ComponentPublicInstance>(): UseForwar
// localExpose should only be assigned once else will create infinite loop
const localExpose = instance.exposed;
const ret: Record<string, any> = {};
const ret: Record<string, unknown> = {};
// Collect all property descriptors in a single pass
const descriptors: PropertyDescriptorMap = {};
@@ -50,62 +50,62 @@ const collected = computed(() => refs.value.length);
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Playlist</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="demo-label">Playlist</span>
<span class="demo-badge">
{{ collected }} refs collected
</span>
</div>
<ul class="flex flex-col gap-2 max-h-56 overflow-y-auto rounded-xl border border-(--border) bg-(--bg-elevated) p-2">
<ul class="demo-card flex flex-col gap-2 max-h-56 overflow-y-auto p-2">
<li
v-for="(track, index) in tracks"
:key="track.id"
:ref="set"
class="group flex items-center gap-3 rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2 transition"
:class="lastMeasured?.index === index ? 'border-(--accent) ring-2 ring-(--ring)' : 'hover:border-(--border-strong)'"
class="group flex items-center gap-3 rounded-lg border border-border bg-bg-inset px-3 py-2 transition"
:class="lastMeasured?.index === index ? 'border-accent ring-2 ring-ring' : 'hover:border-border-strong'"
>
<span class="font-mono text-xs tabular-nums text-(--fg-subtle) w-5 text-right">{{ index + 1 }}</span>
<span class="font-mono text-xs tabular-nums text-fg-subtle w-5 text-right">{{ index + 1 }}</span>
<span class="flex min-w-0 flex-1 flex-col">
<span class="truncate text-sm font-medium text-(--fg)">{{ track.title }}</span>
<span class="truncate text-xs text-(--fg-muted)">{{ track.artist }}</span>
<span class="truncate text-sm font-medium text-fg">{{ track.title }}</span>
<span class="truncate text-xs text-fg-muted">{{ track.artist }}</span>
</span>
<button
type="button"
aria-label="Remove track"
class="rounded-md px-1.5 py-0.5 text-xs text-(--fg-subtle) opacity-0 transition hover:text-(--fg) group-hover:opacity-100 cursor-pointer"
class="rounded-md px-1.5 py-0.5 text-xs text-fg-subtle opacity-0 transition hover:text-fg group-hover:opacity-100 cursor-pointer"
@click="removeTrack(track.id)"
>
</button>
</li>
<li v-if="tracks.length === 0" class="px-3 py-6 text-center text-sm text-(--fg-subtle)">
<li v-if="tracks.length === 0" class="px-3 py-6 text-center text-sm text-fg-subtle">
No tracks add one to collect a ref.
</li>
</ul>
<div
v-if="lastMeasured"
class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums"
class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums"
>
refs[{{ lastMeasured.index }}].width = {{ lastMeasured.width }}px
</div>
<p v-else class="text-xs text-(--fg-subtle)">
<p v-else class="text-xs text-fg-subtle">
Add a track to measure the newest collected element directly from the DOM.
</p>
<div class="flex gap-2">
<button
type="button"
class="inline-flex flex-1 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"
class="demo-btn-primary flex-1"
@click="addTrack"
>
Add track
</button>
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="collected === 0"
@click="measureLast"
>
@@ -36,54 +36,54 @@ const visibleRange = computed(() => {
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Virtual list</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="demo-label">Virtual list</span>
<span class="demo-badge">
{{ total.toLocaleString() }} rows
</span>
</div>
<div
v-bind="containerProps"
class="h-64 rounded-xl border border-(--border) bg-(--bg-elevated)"
class="demo-card h-64"
>
<div v-bind="wrapperProps">
<div
v-for="{ data, index } in list"
:key="index"
class="flex items-center gap-3 border-b border-(--border) px-3"
class="flex items-center gap-3 border-b border-border px-3"
:style="{ height: `${itemHeight}px` }"
>
<span
class="size-6 shrink-0 rounded-md border border-(--border)"
class="size-6 shrink-0 rounded-md border border-border"
:style="{ backgroundColor: `hsl(${data.hue} 65% 55%)` }"
/>
<span class="flex-1 truncate font-mono text-sm text-(--fg) tabular-nums">{{ data.label }}</span>
<span class="text-xs text-(--fg-subtle)">idx {{ index }}</span>
<span class="flex-1 truncate font-mono text-sm text-fg tabular-nums">{{ data.label }}</span>
<span class="text-xs text-fg-subtle">idx {{ index }}</span>
</div>
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums flex items-center justify-between">
<span class="text-(--fg-muted)">rendered</span>
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums flex items-center justify-between">
<span class="text-fg-muted">rendered</span>
<span>{{ list.length }} nodes · idx {{ visibleRange }}</span>
</div>
<div class="flex items-end gap-2">
<label class="flex flex-1 flex-col gap-1">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Scroll to index</span>
<span class="demo-label">Scroll to index</span>
<input
v-model.number="jumpTo"
type="number"
:min="0"
:max="total - 1"
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)"
class="demo-input"
>
</label>
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-2 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-accent px-3 py-2 text-sm font-medium text-accent-fg transition hover:bg-accent-hover active:scale-[0.98] cursor-pointer"
@click="go"
>
Jump
@@ -200,7 +200,7 @@ function createMetrics(length: number, itemSize: UseVirtualListItemSize): UseVir
*
* @since 0.0.15
*/
export function useVirtualList<T = any>(
export function useVirtualList<T = unknown>(
list: MaybeRefOrGetter<readonly T[]>,
options: UseVirtualListOptions,
): UseVirtualListReturn<T> {
@@ -15,33 +15,33 @@ function nudgeTint() {
</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)">Render count</span>
<div class="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col items-center gap-2">
<span class="demo-label">Render count</span>
<span
class="font-mono text-3xl font-bold tabular-nums text-(--fg) transition-colors"
class="demo-stat text-3xl transition-colors"
:style="{ color: `hsl(${tint} 70% 55%)` }"
>{{ renderCount }}</span>
<span class="text-xs text-(--fg-subtle)">renders since mount</span>
<span class="text-xs text-fg-subtle">renders since mount</span>
</div>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Bound input</span>
<span class="demo-label">Bound input</span>
<input
v-model="message"
type="text"
placeholder="Type to re-render…"
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)"
class="demo-input"
>
</label>
<p class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-sm text-(--fg)">
<p class="rounded-lg border border-border bg-bg-inset p-3 text-sm text-fg">
{{ message }}
</p>
<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"
class="demo-btn"
@click="nudgeTint"
>
Force re-render (shift color)
@@ -14,34 +14,34 @@ const mountedAt = new Date(lastRendered).toLocaleTimeString();
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Render info</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="demo-label">Render info</span>
<span class="demo-badge">
{{ component ?? 'anonymous' }}
</span>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ count }}</div>
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">renders</div>
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
<div class="demo-stat text-3xl">{{ count }}</div>
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">renders</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ duration.toFixed(2) }}</div>
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">last render ms</div>
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
<div class="demo-stat text-3xl">{{ duration.toFixed(2) }}</div>
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">last render ms</div>
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums flex items-center justify-between">
<span class="text-(--fg-muted)">mounted at</span>
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums flex items-center justify-between">
<span class="text-fg-muted">mounted at</span>
<span>{{ mountedAt }}</span>
</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="rows">Render workload</label>
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ rows }} cells</span>
<label class="demo-label" for="rows">Render workload</label>
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ rows }} cells</span>
</div>
<input
id="rows"
@@ -50,16 +50,16 @@ const mountedAt = new Date(lastRendered).toLocaleTimeString();
min="1"
max="400"
step="1"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
<p class="text-xs text-(--fg-subtle)">Drag to re-render a larger DOM subtree and watch the render duration climb.</p>
<p class="text-xs text-fg-subtle">Drag to re-render a larger DOM subtree and watch the render duration climb.</p>
</div>
<div class="grid grid-cols-10 gap-1">
<span
v-for="i in grid"
:key="i"
class="aspect-square rounded-sm bg-(--accent-subtle)"
class="aspect-square rounded-sm bg-accent-subtle"
/>
</div>
</div>
@@ -27,51 +27,51 @@ function reset() {
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">DOM removal watcher</span>
<span class="demo-label">DOM removal watcher</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)"
class="demo-badge"
>
<span class="size-1.5 rounded-full transition" :class="mounted ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
<span class="size-1.5 rounded-full transition" :class="mounted ? 'bg-emerald-500' : 'bg-fg-subtle'" />
{{ mounted ? 'In DOM' : 'Removed' }}
</span>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 min-h-28 flex items-center justify-center">
<div class="demo-card p-4 min-h-28 flex items-center justify-center">
<div
v-if="mounted"
ref="watched"
class="flex w-full flex-col items-center gap-1 rounded-lg border border-(--accent) bg-(--accent-subtle) px-4 py-6 text-center transition"
class="flex w-full flex-col items-center gap-1 rounded-lg border border-accent bg-accent-subtle px-4 py-6 text-center transition"
>
<span class="text-sm font-medium text-(--accent-text)">Watched element</span>
<span class="text-xs text-(--fg-muted)">Remove me and the callback fires</span>
<span class="text-sm font-medium text-accent-text">Watched element</span>
<span class="text-xs text-fg-muted">Remove me and the callback fires</span>
</div>
<span v-else class="text-sm text-(--fg-subtle)">Element detached from the document</span>
<span v-else class="text-sm text-fg-subtle">Element detached from the document</span>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ removals }}</div>
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">removals fired</div>
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
<div class="demo-stat text-3xl">{{ removals }}</div>
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">removals fired</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center flex flex-col justify-center">
<div class="font-mono text-sm font-medium tabular-nums text-(--fg)">{{ lastEvent ?? '—' }}</div>
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">last fired</div>
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center flex flex-col justify-center">
<div class="font-mono text-sm font-medium tabular-nums text-fg">{{ lastEvent ?? '—' }}</div>
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">last fired</div>
</div>
</div>
<div class="flex gap-2">
<button
type="button"
class="inline-flex flex-1 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"
class="demo-btn-primary flex-1"
@click="toggle"
>
{{ mounted ? 'Remove element' : 'Mount element' }}
</button>
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="removals === 0"
@click="reset"
>
@@ -15,9 +15,9 @@ const activeTag = computed(() => activeElement.value?.tagName.toLowerCase() ?? n
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="space-y-3">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<p class="demo-label">
Focus a field
</p>
<div
@@ -27,39 +27,39 @@ const activeTag = computed(() => activeElement.value?.tagName.toLowerCase() ?? n
>
<label
:for="field.id"
class="text-sm font-medium text-(--fg-muted)"
class="text-sm font-medium text-fg-muted"
>{{ field.label }}</label>
<input
:id="field.id"
:type="field.type"
:placeholder="field.placeholder"
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)"
class="demo-input"
>
</div>
<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"
class="demo-btn"
>
A focusable button
</button>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
<div class="flex items-center justify-between gap-3">
<span class="text-(--fg-subtle)">activeElement</span>
<span class="text-fg-subtle">activeElement</span>
<span
v-if="activeTag"
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-elevated) px-2 py-0.5 text-xs font-medium text-(--accent-text)"
class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg-elevated px-2 py-0.5 text-xs font-medium text-accent-text"
>
&lt;{{ activeTag }}&gt;
</span>
<span
v-else
class="text-xs text-(--fg-subtle)"
class="text-xs text-fg-subtle"
>none</span>
</div>
<div class="mt-2 flex items-center justify-between gap-3">
<span class="text-(--fg-subtle)">id</span>
<span class="text-fg-subtle">id</span>
<span class="truncate">{{ activeId ?? '—' }}</span>
</div>
</div>
@@ -14,9 +14,9 @@ const activeIndex = computed(() => stages.findIndex(s => s.state === readyState.
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
document.readyState
</span>
<span class="inline-flex items-center gap-1.5 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
@@ -31,24 +31,24 @@ const activeIndex = computed(() => stages.findIndex(s => s.state === readyState.
:key="stage.state"
class="flex items-center gap-3 rounded-lg border p-3 transition"
:class="i <= activeIndex
? 'border-(--accent) bg-(--accent-subtle)'
: 'border-(--border) bg-(--bg-elevated)'"
? 'border-accent bg-accent-subtle'
: 'border-border bg-bg-elevated'"
>
<span
class="flex size-6 shrink-0 items-center justify-center rounded-full font-mono text-xs font-bold tabular-nums transition"
:class="i < activeIndex
? 'bg-(--accent) text-(--accent-fg)'
? 'bg-accent text-accent-fg'
: i === activeIndex
? 'bg-(--accent) text-(--accent-fg) ring-4 ring-(--ring)'
: 'bg-(--bg-inset) text-(--fg-subtle)'"
? 'bg-accent text-accent-fg ring-4 ring-ring'
: 'bg-bg-inset text-fg-subtle'"
>
{{ i + 1 }}
</span>
<div class="min-w-0">
<p class="font-mono text-sm font-medium text-(--fg)">
<p class="font-mono text-sm font-medium text-fg">
{{ stage.label }}
</p>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
{{ stage.hint }}
</p>
</div>
@@ -18,8 +18,8 @@ const isVisible = computed(() => visibility.value === 'visible');
</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 text-center">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col items-center gap-3 text-center">
<span
class="flex size-14 items-center justify-center rounded-full text-2xl transition"
:class="isVisible
@@ -29,33 +29,33 @@ const isVisible = computed(() => visibility.value === 'visible');
{{ isVisible ? '👁️' : '💤' }}
</span>
<div>
<p class="font-mono text-2xl font-bold tabular-nums text-(--fg)">
<p class="demo-stat text-2xl">
{{ visibility }}
</p>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
{{ isVisible ? 'This tab is in the foreground' : 'This tab is hidden' }}
</p>
</div>
</div>
<p class="text-center text-xs text-(--fg-subtle)">
<p class="text-center text-xs text-fg-subtle">
Switch to another tab or minimize the window to watch this update.
</p>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
<p class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
<p class="demo-stat text-3xl">
{{ switches }}
</p>
<p class="mt-0.5 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<p class="demo-label mt-0.5">
Times hidden
</p>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
<p class="font-mono text-sm font-medium tabular-nums text-(--fg) truncate">
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
<p class="font-mono text-sm font-medium tabular-nums text-fg truncate">
{{ lastHidden ?? '—' }}
</p>
<p class="mt-0.5 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<p class="demo-label mt-0.5">
Last hidden at
</p>
</div>
@@ -16,20 +16,20 @@ const { x, y, isDragging, style } = useDraggable(
</script>
<template>
<div class="w-full max-w-md flex flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex items-center justify-between gap-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Draggable, clamped to container
</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
:class="isDragging
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
? 'border-accent bg-accent-subtle text-accent-text'
: 'border-border bg-bg-inset text-fg-muted'"
>
<span
class="size-1.5 rounded-full transition"
:class="isDragging ? 'bg-(--accent)' : 'bg-(--fg-subtle)'"
:class="isDragging ? 'bg-accent' : 'bg-fg-subtle'"
/>
{{ isDragging ? 'dragging' : 'idle' }}
</span>
@@ -37,29 +37,29 @@ const { x, y, isDragging, style } = useDraggable(
<div
ref="container"
class="relative h-56 w-full overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset)"
class="relative h-56 w-full overflow-hidden rounded-xl border border-border bg-bg-inset"
>
<div
ref="card"
:style="style"
class="absolute w-36 select-none rounded-lg border border-(--border-strong) bg-(--bg-elevated) shadow-lg"
:class="isDragging ? 'ring-2 ring-(--ring)' : ''"
class="absolute w-36 select-none rounded-lg border border-border-strong bg-bg-elevated shadow-lg"
:class="isDragging ? 'ring-2 ring-ring' : ''"
>
<div
ref="handle"
class="flex items-center gap-1.5 rounded-t-lg border-b border-(--border) bg-(--bg-subtle) px-3 py-2 cursor-grab active:cursor-grabbing"
class="flex items-center gap-1.5 rounded-t-lg border-b border-border bg-bg-subtle px-3 py-2 cursor-grab active:cursor-grabbing"
>
<span class="text-(--fg-subtle)"></span>
<span class="text-xs font-medium text-(--fg-muted)">Drag me</span>
<span class="text-fg-subtle"></span>
<span class="text-xs font-medium text-fg-muted">Drag me</span>
</div>
<div class="p-3 font-mono text-xs tabular-nums text-(--fg)">
<div class="p-3 font-mono text-xs tabular-nums text-fg">
<div>x: {{ Math.round(x) }}</div>
<div>y: {{ Math.round(y) }}</div>
</div>
</div>
</div>
<p class="text-center text-xs text-(--fg-subtle)">
<p class="text-center text-xs text-fg-subtle">
Drag from the header. Movement is clamped to the container bounds.
</p>
</div>
@@ -1,6 +1,6 @@
import { computed, shallowRef, toValue } from 'vue';
import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
import { noop } from '@robonen/stdlib';
import { clamp, noop } from '@robonen/stdlib';
import { defaultWindow } from '@/types';
import { useEventListener } from '@/composables/browser/useEventListener';
import { unrefElement } from '@/composables/component/unrefElement';
@@ -259,13 +259,13 @@ export function useDraggable(
if (axis === 'x' || axis === 'both') {
x = event.clientX - pressedDelta.value.x;
if (container && targetRect)
x = Math.min(Math.max(0, x), container.scrollWidth - targetRect.width);
x = clamp(x, 0, container.scrollWidth - targetRect.width);
}
if (axis === 'y' || axis === 'both') {
y = event.clientY - pressedDelta.value.y;
if (container && targetRect)
y = Math.min(Math.max(0, y), container.scrollHeight - targetRect.height);
y = clamp(y, 0, container.scrollHeight - targetRect.height);
}
position.value = { x, y };
@@ -24,7 +24,7 @@ function formatSize(bytes: number): string {
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div
v-if="!isSupported"
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-center text-sm text-amber-700 dark:text-amber-300"
@@ -37,33 +37,33 @@ function formatSize(bytes: number): string {
ref="dropZone"
class="flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed p-8 text-center transition"
:class="isOverDropZone
? 'border-(--accent) bg-(--accent-subtle)'
: 'border-(--border-strong) bg-(--bg-elevated)'"
? 'border-accent bg-accent-subtle'
: 'border-border-strong bg-bg-elevated'"
>
<span class="text-3xl transition" :class="isOverDropZone ? 'scale-110' : ''">
{{ isOverDropZone ? '📥' : '🖼️' }}
</span>
<p class="text-sm font-medium text-(--fg)">
<p class="text-sm font-medium text-fg">
{{ isOverDropZone ? 'Release to drop' : 'Drop image files here' }}
</p>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Only <span class="font-mono">image/*</span> files are accepted
</p>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<div class="rounded-lg border border-border bg-bg-inset p-3">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Dropped files
</span>
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-elevated) px-2 py-0.5 text-xs font-medium text-(--fg-muted) tabular-nums">
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg-elevated px-2 py-0.5 text-xs font-medium text-fg-muted tabular-nums">
{{ fileList.length }}
</span>
</div>
<p
v-if="fileList.length === 0"
class="mt-3 text-center text-sm text-(--fg-subtle)"
class="mt-3 text-center text-sm text-fg-subtle"
>
Nothing dropped yet.
</p>
@@ -72,10 +72,10 @@ function formatSize(bytes: number): string {
<li
v-for="file in fileList"
:key="file.name"
class="flex items-center justify-between gap-3 rounded-md bg-(--bg-elevated) px-2.5 py-1.5"
class="flex items-center justify-between gap-3 rounded-md bg-bg-elevated px-2.5 py-1.5"
>
<span class="truncate text-sm text-(--fg)">{{ file.name }}</span>
<span class="shrink-0 font-mono text-xs tabular-nums text-(--fg-subtle)">
<span class="truncate text-sm text-fg">{{ file.name }}</span>
<span class="shrink-0 font-mono text-xs tabular-nums text-fg-subtle">
{{ formatSize(file.size) }}
</span>
</li>
@@ -34,20 +34,20 @@ function fmt(n: number) {
</script>
<template>
<div class="w-full max-w-md flex flex-col gap-4">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="demo-card 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)">getBoundingClientRect</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="demo-label">getBoundingClientRect</span>
<span class="demo-badge">
{{ timing }}
</span>
</div>
<!-- The measured target. Resizing it mutates the reactive bounds. -->
<div class="relative h-40 overflow-hidden rounded-lg border border-(--border) bg-(--bg-inset)">
<div class="relative h-40 overflow-hidden rounded-lg border border-border bg-bg-inset">
<div
ref="target"
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 grid place-items-center rounded-lg bg-(--accent) text-(--accent-fg) shadow transition-[width,height] duration-150"
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 grid place-items-center rounded-lg bg-accent text-accent-fg shadow transition-[width,height] duration-150"
:style="{ width: `${size}px`, height: `${size}px` }"
>
<span class="font-mono text-xs font-semibold tabular-nums">{{ fmt(bounds.width.value) }}×{{ fmt(bounds.height.value) }}</span>
@@ -58,18 +58,18 @@ function fmt(n: number) {
<div
v-for="m in metrics"
:key="m.label"
class="rounded-lg border border-(--border) bg-(--bg-inset) p-2 text-center"
class="rounded-lg border border-border bg-bg-inset p-2 text-center"
>
<div class="font-mono text-sm font-bold tabular-nums text-(--fg)">{{ fmt(m.value) }}</div>
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">{{ m.label }}</div>
<div class="demo-stat text-sm">{{ fmt(m.value) }}</div>
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">{{ m.label }}</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="bound-size">Size</label>
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ size }}px</span>
<label class="demo-label" for="bound-size">Size</label>
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ size }}px</span>
</div>
<input
id="bound-size"
@@ -78,7 +78,7 @@ function fmt(n: number) {
min="32"
max="140"
step="2"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
</div>
@@ -89,22 +89,22 @@ function fmt(n: number) {
type="button"
class="flex-1 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="timing === opt
? '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)'"
? '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="timing = opt"
>
{{ opt }}
</button>
<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"
class="demo-btn"
@click="bounds.update()"
>
Update
</button>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
<span class="font-mono">next-frame</span> batches rapid scroll/resize reads into one measurement per animation frame.
</p>
</div>
@@ -25,12 +25,12 @@ function fmt(n: number) {
</script>
<template>
<div class="w-full max-w-md flex flex-col gap-4">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="demo-card 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)">ResizeObserver</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" :class="observing ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
<span class="demo-label">ResizeObserver</span>
<span class="demo-badge">
<span class="size-1.5 rounded-full" :class="observing ? 'bg-emerald-500' : 'bg-fg-subtle'" />
{{ observing ? 'Observing' : 'Stopped' }}
</span>
</div>
@@ -40,25 +40,25 @@ function fmt(n: number) {
ref="target"
readonly
:style="{ padding: `${padding}px` }"
class="w-full min-h-24 resize rounded-lg border border-(--border-strong) bg-(--bg-inset) text-sm leading-relaxed text-(--fg-muted) focus:outline-none focus:ring-2 focus:ring-(--ring)"
class="w-full min-h-24 resize rounded-lg border border-border-strong bg-bg-inset text-sm leading-relaxed text-fg-muted focus:outline-none focus:ring-2 focus:ring-ring"
>Drag the bottom-right corner to resize me. The width and height update live as the ResizeObserver fires. Border-box sizing includes the padding below.</textarea>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ fmt(width) }}</div>
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">width px</div>
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
<div class="demo-stat text-3xl">{{ fmt(width) }}</div>
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">width px</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ fmt(height) }}</div>
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">height px</div>
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
<div class="demo-stat text-3xl">{{ fmt(height) }}</div>
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">height px</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="size-padding">Padding (border-box)</label>
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ padding }}px</span>
<label class="demo-label" for="size-padding">Padding (border-box)</label>
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ padding }}px</span>
</div>
<input
id="size-padding"
@@ -67,20 +67,20 @@ function fmt(n: number) {
min="0"
max="40"
step="2"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
</div>
<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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="!observing"
@click="toggleObserver"
>
{{ observing ? 'Stop observing' : 'Observer stopped' }}
</button>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
With <span class="font-mono">box: 'border-box'</span> the reported size includes padding, so the slider changes the numbers without resizing the element.
</p>
</div>
@@ -22,16 +22,16 @@ watch(isVisible, (visible, was) => {
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">IntersectionObserver</span>
<span class="demo-label">IntersectionObserver</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
:class="isVisible
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
<span class="size-1.5 rounded-full" :class="isVisible ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
<span class="size-1.5 rounded-full" :class="isVisible ? 'bg-emerald-500' : 'bg-fg-subtle'" />
{{ isVisible ? 'In view' : 'Hidden' }}
</span>
</div>
@@ -39,38 +39,38 @@ watch(isVisible, (visible, was) => {
<!-- Scrollable root; the target card sits below the fold until scrolled. -->
<div
ref="root"
class="h-44 overflow-y-auto rounded-xl border border-(--border) bg-(--bg-inset) p-3"
class="h-44 overflow-y-auto rounded-xl border border-border bg-bg-inset p-3"
>
<p class="text-sm text-(--fg-subtle)">Scroll down inside this box</p>
<p class="text-sm text-fg-subtle">Scroll down inside this box</p>
<div class="h-40" />
<div
ref="target"
class="rounded-lg border border-(--border) bg-(--accent) p-4 text-center text-(--accent-fg) shadow transition"
class="rounded-lg border border-border bg-accent p-4 text-center text-accent-fg shadow transition"
:class="isVisible ? 'opacity-100 scale-100' : 'opacity-60 scale-95'"
>
<div class="text-sm font-semibold">Target element</div>
<div class="text-xs opacity-80">at least 50% visible to count</div>
</div>
<div class="h-24" />
<p class="text-sm text-(--fg-subtle)">and back up to hide it again.</p>
<p class="text-sm text-fg-subtle">and back up to hide it again.</p>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ seenCount }}</div>
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">times seen</div>
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
<div class="demo-stat text-3xl">{{ seenCount }}</div>
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">times seen</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center flex flex-col items-center justify-center">
<div class="text-sm font-semibold" :class="isActive ? 'text-emerald-600 dark:text-emerald-400' : 'text-(--fg-muted)'">
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center flex flex-col items-center justify-center">
<div class="text-sm font-semibold" :class="isActive ? 'text-emerald-600 dark:text-emerald-400' : 'text-fg-muted'">
{{ isActive ? 'Active' : 'Paused' }}
</div>
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">observer</div>
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">observer</div>
</div>
</div>
<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"
class="demo-btn"
@click="isActive ? pause() : resume()"
>
{{ isActive ? 'Pause observer' : 'Resume observer' }}
@@ -26,31 +26,31 @@ const fields = [
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<component :is="GuardHost" v-if="enabled" />
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Focus guards</span>
<span class="demo-label">Focus guards</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
:class="enabled
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
<span class="size-1.5 rounded-full" :class="enabled ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
<span class="size-1.5 rounded-full" :class="enabled ? 'bg-emerald-500' : 'bg-fg-subtle'" />
{{ enabled ? 'Mounted' : 'Off' }}
</span>
</div>
<!-- A small focusable form to feel tab order with the guards active. -->
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-3">
<div class="demo-card p-4 flex flex-col gap-3">
<div v-for="f in fields" :key="f.id" class="flex flex-col gap-1">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" :for="f.id">{{ f.label }}</label>
<label class="demo-label" :for="f.id">{{ f.label }}</label>
<input
:id="f.id"
:value="f.value"
type="text"
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)"
class="demo-input"
@focus="focused = f.label"
@blur="focused = null"
>
@@ -58,7 +58,7 @@ const fields = [
<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"
class="demo-btn-primary"
@focus="focused = 'Submit'"
@blur="focused = null"
>
@@ -66,19 +66,19 @@ const fields = [
</button>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
focused: {{ focused ?? 'nothing' }}
</div>
<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"
class="demo-btn"
@click="enabled = !enabled"
>
{{ enabled ? 'Remove focus guards' : 'Mount focus guards' }}
</button>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Guards are invisible <span class="font-mono">tabindex="0"</span> sentinels inserted at the page boundaries. Tab past the last field with them on to feel focus wrap around for overlays and modals.
</p>
</div>
@@ -48,10 +48,10 @@ const activeId = computed(() => {
</script>
<template>
<div class="w-full max-w-md flex flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Scroll spy</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="demo-label">Scroll spy</span>
<span class="demo-badge">
active: {{ activeId ?? '—' }}
</span>
</div>
@@ -64,8 +64,8 @@ const activeId = computed(() => {
:key="s.id"
class="flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition"
:class="activeId === s.id
? 'border-(--border-strong) bg-(--bg-inset) text-(--fg)'
: 'border-transparent text-(--fg-muted)'"
? 'border-border-strong bg-bg-inset text-fg'
: 'border-transparent text-fg-muted'"
>
<span class="size-2 rounded-full transition" :class="[s.color, activeId === s.id ? 'opacity-100' : 'opacity-30']" />
{{ s.label }}
@@ -75,29 +75,29 @@ const activeId = computed(() => {
<!-- Scrollable content; each section is an observed target. -->
<div
ref="root"
class="h-56 flex-1 overflow-y-auto rounded-xl border border-(--border) bg-(--bg-inset) p-3 flex flex-col gap-3 scroll-smooth"
class="h-56 flex-1 overflow-y-auto rounded-xl border border-border bg-bg-inset p-3 flex flex-col gap-3 scroll-smooth"
>
<section
v-for="s in sections"
:key="s.id"
ref="itemEls"
:data-id="s.id"
class="rounded-lg border border-(--border) bg-(--bg-elevated) p-4 transition"
:class="activeId === s.id ? 'ring-2 ring-(--ring)' : ''"
class="rounded-lg border border-border bg-bg-elevated p-4 transition"
:class="activeId === s.id ? 'ring-2 ring-ring' : ''"
>
<h3 class="text-sm font-semibold text-(--fg)">{{ s.label }}</h3>
<p class="mt-1 text-xs text-(--fg-subtle)">
<h3 class="text-sm font-semibold text-fg">{{ s.label }}</h3>
<p class="mt-1 text-xs text-fg-subtle">
Visibility: {{ Math.round((ratios[s.id] ?? 0) * 100) }}%
</p>
<div class="mt-2 h-16 rounded bg-(--bg-inset)" />
<div class="mt-2 h-16 rounded bg-bg-inset" />
</section>
</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="io-threshold">Active threshold</label>
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ Math.round(threshold * 100) }}%</span>
<label class="demo-label" for="io-threshold">Active threshold</label>
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ Math.round(threshold * 100) }}%</span>
</div>
<input
id="io-threshold"
@@ -106,9 +106,9 @@ const activeId = computed(() => {
min="0.1"
max="0.9"
step="0.1"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
<p class="text-xs text-(--fg-subtle)">A section becomes active once at least this much of it is visible inside the scroll root.</p>
<p class="text-xs text-fg-subtle">A section becomes active once at least this much of it is visible inside the scroll root.</p>
</div>
</div>
</template>

Some files were not shown because too many files have changed in this diff Show More