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

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