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
@@ -58,9 +58,9 @@ const ids = [1, 2, 3, 4, 99];
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Fetch user by id</span>
<span class="demo-label">Fetch user by id</span>
<div class="flex flex-wrap gap-1.5">
<button
v-for="id in ids"
@@ -68,8 +68,8 @@ const ids = [1, 2, 3, 4, 99];
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="userId === id
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
@click="userId = id"
>
#{{ id }}
@@ -77,34 +77,34 @@ const ids = [1, 2, 3, 4, 99];
</div>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 min-h-32 flex flex-col justify-center">
<div class="demo-card p-4 min-h-32 flex flex-col justify-center">
<Transition name="fade" mode="out-in">
<div v-if="evaluating" key="loading" class="flex items-center gap-2 text-sm text-(--fg-muted)">
<span class="size-4 animate-spin rounded-full border-2 border-(--border) border-t-(--accent)" />
<div v-if="evaluating" key="loading" class="flex items-center gap-2 text-sm text-fg-muted">
<span class="size-4 animate-spin rounded-full border-2 border-border border-t-accent" />
Resolving promise
</div>
<div v-else-if="error" key="error" class="flex flex-col gap-1">
<span class="text-sm font-medium text-red-600 dark:text-red-400">Evaluation failed</span>
<span class="font-mono text-xs text-(--fg-muted)">{{ error }}</span>
<span class="font-mono text-xs text-fg-muted">{{ error }}</span>
</div>
<div v-else-if="user" key="user" class="flex flex-col gap-1">
<span class="text-lg font-semibold text-(--fg)">{{ user.name }}</span>
<span class="text-sm text-(--fg-muted)">{{ user.role }}</span>
<span class="inline-flex w-fit 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="text-lg font-semibold text-fg">{{ user.name }}</span>
<span class="text-sm text-fg-muted">{{ user.role }}</span>
<span class="demo-badge w-fit">
{{ user.city }}
</span>
</div>
<div v-else key="empty" class="text-sm text-(--fg-subtle)">
<div v-else key="empty" class="text-sm text-fg-subtle">
Awaiting first resolution
</div>
</Transition>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">evaluating</span>
<div class="rounded-lg border border-border bg-bg-inset p-3 flex items-center justify-between">
<span class="demo-label">evaluating</span>
<span
class="inline-flex items-center gap-1.5 font-mono text-sm tabular-nums"
:class="evaluating ? 'text-amber-600 dark:text-amber-400' : 'text-emerald-600 dark:text-emerald-400'"
@@ -26,7 +26,7 @@ const rules = computedEager(() => [
]);
const tones = [
'bg-(--border)',
'bg-border',
'bg-red-500',
'bg-amber-500',
'bg-sky-500',
@@ -35,45 +35,45 @@ const tones = [
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex flex-col gap-2">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="pwd">Password</label>
<label class="demo-label" for="pwd">Password</label>
<input
id="pwd"
v-model="password"
type="text"
placeholder="Type a password…"
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
class="demo-input"
>
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-sm text-(--fg-muted)">Strength</span>
<span class="font-mono text-sm font-medium tabular-nums text-(--fg)">{{ label }}</span>
<span class="text-sm text-fg-muted">Strength</span>
<span class="font-mono text-sm font-medium tabular-nums text-fg">{{ label }}</span>
</div>
<div class="flex gap-1.5">
<div
v-for="i in 4"
:key="i"
class="h-1.5 flex-1 rounded-full transition-colors duration-300"
:class="i <= score ? tones[score] : 'bg-(--bg-inset)'"
:class="i <= score ? tones[score] : 'bg-bg-inset'"
/>
</div>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-2">
<div class="demo-card p-4 flex flex-col gap-2">
<div
v-for="rule in rules"
:key="rule.text"
class="flex items-center gap-2 text-sm transition-colors"
:class="rule.ok ? 'text-(--fg)' : 'text-(--fg-subtle)'"
:class="rule.ok ? 'text-fg' : 'text-fg-subtle'"
>
<span
class="grid size-4 place-items-center rounded-full text-[10px] transition-colors"
:class="rule.ok
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400'
: 'bg-(--bg-inset) text-(--fg-subtle)'"
: 'bg-bg-inset text-fg-subtle'"
>
{{ rule.ok ? '✓' : '○' }}
</span>
@@ -81,8 +81,8 @@ const tones = [
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums flex items-center justify-between">
<span class="text-(--fg-muted)">length</span>
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums flex items-center justify-between">
<span class="text-fg-muted">length</span>
<span>{{ length }}</span>
</div>
</div>
@@ -27,12 +27,12 @@ function stop() {
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col gap-4">
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Base price (tracked)</span>
<span class="font-mono text-sm tabular-nums text-(--accent-text)">{{ source }}</span>
<span class="demo-label">Base price (tracked)</span>
<span class="font-mono text-sm tabular-nums text-accent-text">{{ source }}</span>
</div>
<input
v-model.number="source"
@@ -40,14 +40,14 @@ function stop() {
min="0"
max="500"
step="10"
class="w-full accent-(--accent)"
class="w-full accent-accent"
>
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Tax % (untracked)</span>
<span class="font-mono text-sm tabular-nums text-(--fg-muted)">{{ tax }}</span>
<span class="demo-label">Tax % (untracked)</span>
<span class="font-mono text-sm tabular-nums text-fg-muted">{{ tax }}</span>
</div>
<input
v-model.number="tax"
@@ -55,37 +55,37 @@ function stop() {
min="0"
max="30"
step="1"
class="w-full accent-(--fg-muted)"
class="w-full accent-fg-muted"
>
<span class="text-xs text-(--fg-subtle)">
<span class="text-xs text-fg-subtle">
Changing tax alone won't recompute — trigger to pull it in.
</span>
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Total</span>
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ total }}</span>
<div class="rounded-lg border border-border bg-bg-inset p-3 flex items-center justify-between">
<span class="demo-label">Total</span>
<span class="demo-stat text-3xl">{{ total }}</span>
</div>
<div class="grid grid-cols-3 gap-1.5">
<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="total.trigger()"
>
Trigger
</button>
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="demo-btn"
@click="peek"
>
Peek
</button>
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
:disabled="detached"
@click="stop"
>
@@ -95,12 +95,12 @@ function stop() {
<div class="flex flex-col gap-1.5 text-sm">
<div class="flex items-center justify-between">
<span class="text-(--fg-muted)">Getter runs</span>
<span class="font-mono tabular-nums text-(--fg)">{{ recomputes }}</span>
<span class="text-fg-muted">Getter runs</span>
<span class="font-mono tabular-nums text-fg">{{ recomputes }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-(--fg-muted)">Last peek</span>
<span class="font-mono tabular-nums text-(--fg)">{{ peeked ?? '' }}</span>
<span class="text-fg-muted">Last peek</span>
<span class="font-mono tabular-nums text-fg">{{ peeked ?? '' }}</span>
</div>
<div v-if="detached" class="text-xs text-amber-600 dark:text-amber-400">
Source watcher stopped only Trigger updates the total now.
@@ -2,14 +2,14 @@ import { customRef, watch } from 'vue';
import type {
ComputedGetter,
ComputedRef,
MultiWatchSources,
WatchOptions,
WatchSource,
WatchStopHandle,
WritableComputedOptions,
WritableComputedRef,
} from 'vue';
type MultiWatchSources = Array<WatchSource<unknown> | object>;
import { isFunction } from '@robonen/stdlib';
export interface ComputedWithControlExtra<T> {
/**
@@ -146,7 +146,7 @@ export function computedWithControl<T>(
function isGetter<T>(
fn: ComputedGetter<T> | WritableComputedOptions<T>,
): fn is ComputedGetter<T> {
return typeof fn === 'function';
return isFunction(fn);
}
function noopTrack(): void {}
@@ -27,13 +27,13 @@ function toggleMute() {
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col gap-4">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Volume</span>
<span class="demo-label">Volume</span>
<span
class="font-mono text-3xl font-bold tabular-nums transition-colors"
:class="volume.isMuted ? 'text-(--fg-subtle)' : 'text-(--fg)'"
:class="volume.isMuted ? 'text-fg-subtle' : 'text-fg'"
>{{ volume.percent }}</span>
</div>
@@ -43,20 +43,20 @@ function toggleMute() {
type="range"
min="0"
max="100"
class="w-full accent-(--accent)"
class="w-full accent-accent"
>
<div class="flex gap-1.5">
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg) 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="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-border bg-bg 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="toggleMute"
>
{{ volume.isMuted ? 'Unmute' : 'Mute' }}
</button>
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg) 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="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-border bg-bg 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="volume.max()"
>
Max
@@ -64,24 +64,24 @@ function toggleMute() {
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 flex flex-col gap-2 font-mono text-sm tabular-nums">
<div class="rounded-lg border border-border bg-bg-inset p-3 flex flex-col gap-2 font-mono text-sm tabular-nums">
<div class="flex items-center justify-between">
<span class="text-(--fg-muted)">volume.value</span>
<span class="text-(--fg)">{{ volume.value }}</span>
<span class="text-fg-muted">volume.value</span>
<span class="text-fg">{{ volume.value }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-(--fg-muted)">volume.percent</span>
<span class="text-(--fg)">{{ volume.percent }}</span>
<span class="text-fg-muted">volume.percent</span>
<span class="text-fg">{{ volume.percent }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-(--fg-muted)">volume.isMuted</span>
<span class="text-fg-muted">volume.isMuted</span>
<span :class="volume.isMuted ? 'text-amber-600 dark:text-amber-400' : 'text-emerald-600 dark:text-emerald-400'">
{{ volume.isMuted }}
</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
One ref carries its value, derived properties, and methods no destructuring of an object needed.
</p>
</div>
@@ -28,31 +28,31 @@ function setDiscount(value: number) {
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col gap-4">
<div class="grid grid-cols-2 gap-3">
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Unit price</span>
<span class="demo-label">Unit price</span>
<input
v-model.number="price"
type="number"
min="0"
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) tabular-nums 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 tabular-nums transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
>
</label>
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Quantity</span>
<span class="demo-label">Quantity</span>
<input
v-model.number="quantity"
type="number"
min="1"
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) tabular-nums 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 tabular-nums transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
>
</label>
</div>
<div class="flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Discount</span>
<span class="demo-label">Discount</span>
<div class="flex flex-wrap gap-1.5">
<button
v-for="p in presets"
@@ -60,8 +60,8 @@ function setDiscount(value: number) {
type="button"
class="inline-flex items-center justify-center rounded-lg border px-3 py-1.5 text-sm font-medium tabular-nums transition active:scale-[0.98] cursor-pointer"
:class="cart.discount === p
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
: 'border-(--border) bg-(--bg) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
: 'border-border bg-bg text-fg hover:bg-bg-inset hover:border-border-strong'"
@click="setDiscount(p)"
>
{{ p }}%
@@ -70,24 +70,24 @@ function setDiscount(value: number) {
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 flex flex-col gap-2 font-mono text-sm tabular-nums">
<div class="flex items-center justify-between text-(--fg-muted)">
<div class="rounded-lg border border-border bg-bg-inset p-3 flex flex-col gap-2 font-mono text-sm tabular-nums">
<div class="flex items-center justify-between text-fg-muted">
<span>Subtotal</span>
<span class="text-(--fg)">${{ cart.subtotal }}</span>
<span class="text-fg">${{ cart.subtotal }}</span>
</div>
<div class="flex items-center justify-between text-(--fg-muted)">
<div class="flex items-center justify-between text-fg-muted">
<span>Saved ({{ cart.discount }}%)</span>
<span class="text-emerald-600 dark:text-emerald-400">-${{ cart.saved }}</span>
</div>
<div class="h-px bg-(--border)" />
<div class="h-px bg-border" />
<div class="flex items-center justify-between">
<span class="text-(--fg)">Total</span>
<span class="text-2xl font-bold text-(--fg)">${{ cart.total }}</span>
<span class="text-fg">Total</span>
<span class="text-2xl font-bold text-fg">${{ cart.total }}</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
Each field reads from a single cached getter; writing <code class="text-(--fg-muted)">cart.discount</code> flows back to the source ref.
<p class="text-xs text-fg-subtle">
Each field reads from a single cached getter; writing <code class="text-fg-muted">cart.discount</code> flows back to the source ref.
</p>
</div>
</template>
@@ -20,22 +20,22 @@ const roles = ['admin', 'editor', 'viewer'] as const;
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="space-y-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-sm">
<div class="demo-card space-y-3 p-4">
<p class="demo-label">
Source object
</p>
<label class="flex flex-col gap-1">
<span class="text-xs font-medium text-(--fg-muted)">name</span>
<span class="text-xs font-medium text-fg-muted">name</span>
<input
v-model="user.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"
>
</label>
<div class="flex items-center justify-between gap-3">
<span class="text-xs font-medium text-(--fg-muted)">role</span>
<span class="text-xs font-medium text-fg-muted">role</span>
<div class="flex gap-1.5">
<button
v-for="r in roles"
@@ -43,8 +43,8 @@ const roles = ['admin', 'editor', 'viewer'] as const;
type="button"
class="rounded-md border px-2 py-0.5 text-xs font-medium transition cursor-pointer"
:class="user.role === r
? 'border-transparent bg-(--accent) text-(--accent-fg)'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted) hover:border-(--border-strong)'"
? 'border-transparent bg-accent text-accent-fg'
: 'border-border bg-bg-inset text-fg-muted hover:border-border-strong'"
@click="user.role = r"
>
{{ r }}
@@ -53,11 +53,11 @@ const roles = ['admin', 'editor', 'viewer'] as const;
</div>
<label class="flex items-center justify-between gap-3">
<span class="text-xs font-medium text-(--fg-muted)">active</span>
<span class="text-xs font-medium text-fg-muted">active</span>
<button
type="button"
class="relative h-5 w-9 rounded-full transition cursor-pointer"
:class="user.active ? 'bg-(--accent)' : 'bg-(--bg-inset)'"
:class="user.active ? 'bg-accent' : 'bg-bg-inset'"
role="switch"
:aria-checked="user.active"
@click="user.active = !user.active"
@@ -70,18 +70,18 @@ const roles = ['admin', 'editor', 'viewer'] as const;
</label>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
Omit <code class="text-(--accent-text)">token</code>, <code class="text-(--accent-text)">email</code>
<div class="demo-card p-4">
<p class="demo-label mb-2">
Omit <code class="text-accent-text">token</code>, <code class="text-accent-text">email</code>
</p>
<pre class="overflow-x-auto rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg)">{{ safeUser }}</pre>
<pre class="overflow-x-auto rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg">{{ safeUser }}</pre>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-card p-4">
<p class="demo-label mb-2">
Predicate: drop booleans
</p>
<pre class="overflow-x-auto rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg)">{{ noFlags }}</pre>
<pre class="overflow-x-auto rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg">{{ noFlags }}</pre>
</div>
</div>
</template>
@@ -1,5 +1,5 @@
import { toValue } from 'vue';
import { omit } from '@robonen/stdlib';
import { isFunction, omit } from '@robonen/stdlib';
import { reactiveComputed } from '@/composables/reactivity/reactiveComputed';
/**
@@ -67,7 +67,7 @@ export function reactiveOmit<T extends object, K extends keyof T>(
...keys: Array<K | K[] | ReactiveOmitPredicate<T>>
): ReactiveOmitReturn<T, K> {
const first = keys[0];
const predicate = typeof first === 'function'
const predicate = isFunction(first)
? first as ReactiveOmitPredicate<T>
: undefined;
@@ -15,47 +15,47 @@ const display = reactivePick(settings, 'brightness', 'contrast');
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="space-y-4 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-sm">
<div class="demo-card space-y-4 p-4">
<p class="demo-label">
Picked view (two-way)
</p>
<label class="flex flex-col gap-1.5">
<span class="flex items-center justify-between text-sm text-(--fg)">
<span class="flex items-center justify-between text-sm text-fg">
<span>brightness</span>
<span class="font-mono tabular-nums text-(--fg-muted)">{{ display.brightness }}</span>
<span class="font-mono tabular-nums text-fg-muted">{{ display.brightness }}</span>
</span>
<input
v-model.number="display.brightness"
type="range"
min="0"
max="100"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
</label>
<label class="flex flex-col gap-1.5">
<span class="flex items-center justify-between text-sm text-(--fg)">
<span class="flex items-center justify-between text-sm text-fg">
<span>contrast</span>
<span class="font-mono tabular-nums text-(--fg-muted)">{{ display.contrast }}</span>
<span class="font-mono tabular-nums text-fg-muted">{{ display.contrast }}</span>
</span>
<input
v-model.number="display.contrast"
type="range"
min="0"
max="100"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
</label>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-card p-4">
<p class="demo-label mb-2">
Full source object
</p>
<pre class="overflow-x-auto rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg)">{{ settings }}</pre>
<p class="mt-2 text-xs text-(--fg-subtle)">
<pre class="overflow-x-auto rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg">{{ settings }}</pre>
<p class="mt-2 text-xs text-fg-subtle">
Editing the picked view above writes straight back to the source.
</p>
</div>
@@ -19,17 +19,17 @@ async function copy() {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="space-y-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-sm">
<div class="demo-card space-y-3 p-4">
<p class="demo-label">
Live value
</p>
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<span class="font-mono text-lg font-semibold tabular-nums text-(--fg)">{{ status }}</span>
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3">
<span class="font-mono text-lg font-semibold tabular-nums text-fg">{{ status }}</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
:class="status === 'Idle'
? 'border-(--border) bg-(--bg-elevated) text-(--fg-muted)'
? 'border-border bg-bg-elevated text-fg-muted'
: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'"
>
{{ status === 'Idle' ? 'reset' : 'active' }}
@@ -39,21 +39,21 @@ async function copy() {
<div class="flex flex-wrap gap-2">
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="demo-btn"
@click="flash('Saved')"
>
Save
</button>
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="demo-btn"
@click="flash('Synced')"
>
Sync
</button>
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="demo-btn"
@click="flash('Uploading…')"
>
Upload
@@ -61,10 +61,10 @@ async function copy() {
</div>
</div>
<label class="flex flex-col gap-1.5 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<span class="flex items-center justify-between text-sm text-(--fg)">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">reset delay</span>
<span class="font-mono tabular-nums text-(--fg-muted)">{{ delay }}ms</span>
<label class="demo-card flex flex-col gap-1.5 p-4">
<span class="flex items-center justify-between text-sm text-fg">
<span class="demo-label">reset delay</span>
<span class="font-mono tabular-nums text-fg-muted">{{ delay }}ms</span>
</span>
<input
v-model.number="delay"
@@ -72,15 +72,15 @@ async function copy() {
min="500"
max="4000"
step="250"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
</label>
<div class="flex items-center justify-between rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<span class="text-sm text-(--fg)">Copy-to-clipboard pattern</span>
<div class="demo-card flex items-center justify-between p-4">
<span class="text-sm text-fg">Copy-to-clipboard pattern</span>
<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="copy"
>
{{ copied ? 'Copied!' : 'Copy' }}
@@ -12,22 +12,22 @@ const pending = computed(() => search.value !== debounced.value);
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="demo-stack max-w-sm">
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Type to search
</span>
<input
v-model="search"
placeholder="Start typing…"
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
class="demo-input"
>
</label>
<label class="flex flex-col gap-1.5 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<span class="flex items-center justify-between text-sm text-(--fg)">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">debounce</span>
<span class="font-mono tabular-nums text-(--fg-muted)">{{ ms }}ms</span>
<label class="demo-card flex flex-col gap-1.5 p-4">
<span class="flex items-center justify-between text-sm text-fg">
<span class="demo-label">debounce</span>
<span class="font-mono tabular-nums text-fg-muted">{{ ms }}ms</span>
</span>
<input
v-model.number="ms"
@@ -35,19 +35,19 @@ const pending = computed(() => search.value !== debounced.value);
min="100"
max="1500"
step="50"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
</label>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-3">
<p class="mb-1 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-card p-3">
<p class="demo-label mb-1">
Source
</p>
<p class="truncate font-mono text-sm text-(--fg)">{{ search || '—' }}</p>
<p class="truncate font-mono text-sm text-fg">{{ search || '—' }}</p>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-3">
<p class="mb-1 flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-card p-3">
<p class="demo-label mb-1 flex items-center gap-1.5">
Debounced
<span
v-if="pending"
@@ -55,7 +55,7 @@ const pending = computed(() => search.value !== debounced.value);
aria-label="pending"
/>
</p>
<p class="truncate font-mono text-sm text-(--accent-text)">{{ debounced || '—' }}</p>
<p class="truncate font-mono text-sm text-accent-text">{{ debounced || '—' }}</p>
</div>
</div>
@@ -11,18 +11,18 @@ const name = refDefault(raw, fallback);
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<div class="space-y-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<div class="demo-stack max-w-sm">
<div class="demo-card space-y-3 p-4">
<p class="demo-label">
Resolved value
</p>
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<span class="font-mono text-lg font-semibold text-(--fg)">{{ name }}</span>
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3">
<span class="font-mono text-lg font-semibold text-fg">{{ name }}</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="raw == null
? 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400'
: 'border-(--border) bg-(--bg-elevated) text-(--fg-muted)'"
: 'border-border bg-bg-elevated text-fg-muted'"
>
{{ raw == null ? 'using default' : 'from source' }}
</span>
@@ -30,38 +30,38 @@ const name = refDefault(raw, fallback);
</div>
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Source ref (writes pass through)
</span>
<div class="flex gap-2">
<input
v-model="name"
placeholder="Type a 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"
>
<button
type="button"
class="inline-flex shrink-0 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"
:disabled="raw == null"
@click="raw = null"
>
Clear
</button>
</div>
<span class="font-mono text-xs text-(--fg-subtle)">
<span class="font-mono text-xs text-fg-subtle">
raw.value = {{ raw === null ? 'null' : `"${raw}"` }}
</span>
</label>
<label class="flex flex-col gap-1.5 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<label class="demo-card flex flex-col gap-1.5 p-4">
<span class="demo-label">
Reactive fallback
</span>
<input
v-model="fallback"
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"
>
<span class="text-xs text-(--fg-subtle)">
<span class="text-xs text-fg-subtle">
Changes here update the resolved value while the source is empty.
</span>
</label>
@@ -53,27 +53,27 @@ onUnmounted(stop);
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="grid grid-cols-2 gap-3">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-1">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Source</span>
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ source }}</span>
<span class="text-xs text-(--fg-muted)">{{ sourceUpdates }} updates</span>
<div class="demo-card p-4 flex flex-col gap-1">
<span class="demo-label">Source</span>
<span class="demo-stat text-3xl">{{ source }}</span>
<span class="text-xs text-fg-muted">{{ sourceUpdates }} updates</span>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-1">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Throttled</span>
<span class="font-mono text-3xl font-bold tabular-nums text-(--accent-text)">{{ throttled }}</span>
<span class="text-xs text-(--fg-muted)">{{ throttledUpdates }} updates</span>
<div class="demo-card p-4 flex flex-col gap-1">
<span class="demo-label">Throttled</span>
<span class="font-mono text-3xl font-bold tabular-nums text-accent-text">{{ throttled }}</span>
<span class="text-xs text-fg-muted">{{ throttledUpdates }} updates</span>
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Lag behind source</span>
<span class="font-mono text-sm tabular-nums text-(--fg)">+{{ lag }}</span>
<div class="rounded-lg border border-border bg-bg-inset p-3 flex items-center justify-between">
<span class="demo-label">Lag behind source</span>
<span class="font-mono text-sm tabular-nums text-fg">+{{ lag }}</span>
</div>
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Throttle window: {{ delay }}ms
</span>
<input
@@ -82,16 +82,16 @@ onUnmounted(stop);
min="100"
max="1500"
step="100"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
<span class="text-xs text-(--fg-subtle)">Reset to apply a new window</span>
<span class="text-xs text-fg-subtle">Reset to apply a new window</span>
</label>
<div class="flex gap-2">
<button
type="button"
:disabled="running"
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="start"
>
Run (60ms)
@@ -99,14 +99,14 @@ onUnmounted(stop);
<button
type="button"
:disabled="!running"
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"
>
Pause
</button>
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="demo-btn"
@click="reset"
>
Reset
@@ -2,7 +2,7 @@ import { ref, toValue, watch } from 'vue';
import type { MaybeRefOrGetter, Ref } from 'vue';
import { createFilterWrapper, throttleFilter } from '@/utils/filters';
export type RefThrottledReturn<T = any> = Ref<T>;
export type RefThrottledReturn<T = unknown> = Ref<T>;
/**
* @name refThrottled
@@ -28,7 +28,7 @@ export type RefThrottledReturn<T = any> = Ref<T>;
*
* @since 0.0.15
*/
export function refThrottled<T = any>(
export function refThrottled<T = unknown>(
source: MaybeRefOrGetter<T>,
delay = 200,
trailing = true,
@@ -43,30 +43,30 @@ function silentBump() {
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-3">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col gap-3">
<div class="flex items-baseline justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Volume (tracked)</span>
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ volume }}</span>
<span class="demo-label">Volume (tracked)</span>
<span class="demo-stat text-3xl">{{ volume }}</span>
</div>
<div class="flex gap-2">
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--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="volume.value--"
>
- 1
</button>
<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="volume.value++"
>
+ 1
</button>
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="demo-btn flex-1"
@click="volume.value = 99"
>
Set 99
@@ -74,25 +74,25 @@ function silentBump() {
</div>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 flex items-center justify-between">
<div class="rounded-lg border border-border bg-bg-inset p-3 flex items-center justify-between">
<div class="flex flex-col">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">peek() snapshot</span>
<span class="text-xs text-(--fg-subtle)">untracked read; lay() writes silently</span>
<span class="demo-label">peek() snapshot</span>
<span class="text-xs text-fg-subtle">untracked read; lay() writes silently</span>
</div>
<span class="font-mono text-2xl font-bold tabular-nums text-(--fg)">{{ peeked }}</span>
<span class="demo-stat text-2xl">{{ peeked }}</span>
</div>
<div class="flex gap-2">
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--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="peek"
>
peek()
</button>
<button
type="button"
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
class="demo-btn flex-1"
@click="silentBump"
>
lay() +1 silent
@@ -100,16 +100,16 @@ function silentBump() {
</div>
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Hooks log</span>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 min-h-24 flex flex-col gap-1">
<p v-if="!log.length" class="text-xs text-(--fg-subtle)">
<span class="demo-label">Hooks log</span>
<div class="rounded-lg border border-border bg-bg-inset p-3 min-h-24 flex flex-col gap-1">
<p v-if="!log.length" class="text-xs text-fg-subtle">
Try setting volume to 99 to see onBeforeChange veto.
</p>
<p
v-for="entry in log"
:key="entry.id"
class="font-mono text-xs tabular-nums"
:class="entry.vetoed ? 'text-red-600 dark:text-red-400' : 'text-(--fg-muted)'"
:class="entry.vetoed ? 'text-red-600 dark:text-red-400' : 'text-fg-muted'"
>
{{ entry.vetoed ? '✗' : '✓' }} {{ entry.message }}
</p>
@@ -26,43 +26,43 @@ onUnmounted(stop);
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Two-way sync + transform</span>
<span class="demo-label">Two-way sync + transform</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
:class="synced
? '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="synced ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
:class="synced ? 'bg-emerald-500' : 'bg-fg-subtle'"
/>
{{ synced ? 'live' : 'stopped' }}
</span>
</div>
<div class="grid grid-cols-2 gap-3">
<label class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Celsius</span>
<label class="demo-card p-4 flex flex-col gap-2">
<span class="demo-label">Celsius</span>
<input
v-model.number="celsius"
type="number"
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-lg font-mono tabular-nums 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-lg font-mono tabular-nums text-fg transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
>
</label>
<label class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-2">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Fahrenheit</span>
<label class="demo-card p-4 flex flex-col gap-2">
<span class="demo-label">Fahrenheit</span>
<input
v-model.number="fahrenheit"
type="number"
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-lg font-mono tabular-nums 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-lg font-mono tabular-nums text-fg transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
>
</label>
</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">
{{ celsius }}°C {{ fahrenheit }}°F
</div>
@@ -72,19 +72,19 @@ onUnmounted(stop);
min="-20"
max="40"
step="1"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
aria-label="Celsius slider"
>
<button
type="button"
:disabled="!synced"
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="toggleSync"
>
Stop synchronization
</button>
<p v-if="!synced" class="text-xs text-(--fg-subtle) -mt-2">
<p v-if="!synced" class="text-xs text-fg-subtle -mt-2">
Watchers torn down the two refs now drift independently.
</p>
</div>
@@ -26,26 +26,26 @@ function loadPreset(p: Profile) {
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-3">
<div class="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col gap-3">
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Name</span>
<span class="demo-label">Name</span>
<input
v-model="profile.name"
type="text"
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
class="demo-input"
>
</label>
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Role</span>
<span class="demo-label">Role</span>
<input
v-model="profile.role"
type="text"
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
class="demo-input"
>
</label>
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
<span class="demo-label">
Level: {{ profile.level }}
</span>
<input
@@ -54,30 +54,30 @@ function loadPreset(p: Profile) {
min="1"
max="10"
step="1"
class="w-full accent-(--accent) cursor-pointer"
class="w-full accent-accent cursor-pointer"
>
</label>
</div>
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">source.value (the backing ref)</span>
<pre class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg) overflow-x-auto">{{ JSON.stringify(source, null, 2) }}</pre>
<span class="demo-label">source.value (the backing ref)</span>
<pre class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg overflow-x-auto">{{ JSON.stringify(source, null, 2) }}</pre>
</div>
<div class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Reassign the whole ref</span>
<span class="demo-label">Reassign the whole ref</span>
<div class="flex flex-wrap gap-2">
<button
v-for="preset in presets"
:key="preset.name"
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="loadPreset(preset)"
>
{{ preset.name.split(' ')[0] }}
</button>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
The proxy survives reassignment fields above update without re-binding.
</p>
</div>
@@ -19,14 +19,14 @@ const samples = ['Hello', 'hello', 'HELLO', 'World', 'world!'];
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Source ref</span>
<span class="demo-label">Source ref</span>
<input
v-model="source"
type="text"
placeholder="Type to change the source"
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>
@@ -35,32 +35,32 @@ const samples = ['Hello', 'hello', 'HELLO', 'World', 'world!'];
v-for="sample in samples"
:key="sample"
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="source = sample"
>
{{ sample }}
</button>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-3">
<div class="demo-card p-4 flex flex-col gap-3">
<div class="flex items-center justify-between gap-3">
<div class="flex flex-col">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Default cache</span>
<span class="text-xs text-(--fg-subtle)">a === b</span>
<span class="demo-label">Default cache</span>
<span class="text-xs text-fg-subtle">a === b</span>
</div>
<span class="font-mono text-sm tabular-nums text-(--fg) truncate">"{{ cachedDefault }}"</span>
<span class="font-mono text-sm tabular-nums text-fg truncate">"{{ cachedDefault }}"</span>
</div>
<div class="h-px bg-(--border)" />
<div class="h-px bg-border" />
<div class="flex items-center justify-between gap-3">
<div class="flex flex-col">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Case-insensitive cache</span>
<span class="text-xs text-(--fg-subtle)">toLowerCase() match</span>
<span class="demo-label">Case-insensitive cache</span>
<span class="text-xs text-fg-subtle">toLowerCase() match</span>
</div>
<span class="font-mono text-sm tabular-nums text-(--accent-text) truncate">"{{ cachedInsensitive }}"</span>
<span class="font-mono text-sm tabular-nums text-accent-text truncate">"{{ cachedInsensitive }}"</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Toggle between <span class="font-mono">Hello</span>, <span class="font-mono">hello</span> and
<span class="font-mono">HELLO</span>: the default cache follows every change, while the
case-insensitive cache keeps its first stored casing.
@@ -22,9 +22,9 @@ function addTag() {
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">useCloned</span>
<span class="demo-label">useCloned</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
:class="isModified
@@ -40,40 +40,40 @@ function addTag() {
</div>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Source</p>
<dl class="space-y-1.5 text-sm text-(--fg)">
<div class="demo-card p-4">
<p class="demo-label mb-2">Source</p>
<dl class="space-y-1.5 text-sm text-fg">
<div class="flex justify-between gap-2">
<dt class="text-(--fg-muted)">name</dt>
<dt class="text-fg-muted">name</dt>
<dd class="truncate font-medium">{{ original.name }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-(--fg-muted)">role</dt>
<dt class="text-fg-muted">role</dt>
<dd class="font-medium">{{ original.role }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-(--fg-muted)">tags</dt>
<dd class="font-mono text-xs text-(--fg-muted)">{{ original.tags.length }}</dd>
<dt class="text-fg-muted">tags</dt>
<dd class="font-mono text-xs text-fg-muted">{{ original.tags.length }}</dd>
</div>
</dl>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Cloned (editable)</p>
<dl class="space-y-1.5 text-sm text-(--fg)">
<div class="demo-card p-4">
<p class="demo-label mb-2">Cloned (editable)</p>
<dl class="space-y-1.5 text-sm text-fg">
<div class="flex justify-between gap-2">
<dt class="text-(--fg-muted)">name</dt>
<dt class="text-fg-muted">name</dt>
<dd class="truncate font-medium">{{ cloned.name }}</dd>
</div>
<div class="flex justify-between gap-2">
<dt class="text-(--fg-muted)">role</dt>
<dt class="text-fg-muted">role</dt>
<dd class="font-medium">{{ cloned.role }}</dd>
</div>
<div class="flex flex-wrap justify-end gap-1">
<span
v-for="tag in cloned.tags"
:key="tag"
class="inline-flex items-center rounded-md border border-(--border) bg-(--bg-inset) px-1.5 py-0.5 font-mono text-[0.65rem] text-(--fg-muted)"
class="inline-flex items-center rounded-md border border-border bg-bg-inset px-1.5 py-0.5 font-mono text-[0.65rem] text-fg-muted"
>
{{ tag }}
</span>
@@ -85,21 +85,21 @@ function addTag() {
<div class="flex flex-wrap gap-2">
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:border-(--border-strong) hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
class="demo-btn"
@click="bumpOriginal"
>
Cycle source role
</button>
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:border-(--border-strong) hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
class="demo-btn"
@click="addTag"
>
Edit clone
</button>
<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="!isModified"
@click="sync()"
>
@@ -107,7 +107,7 @@ function addTag() {
</button>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Editing the source auto-resyncs the clone. Editing the clone marks it modified without touching the source.
</p>
</div>
@@ -23,25 +23,25 @@ function onInput(event: Event) {
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<div class="demo-stack max-w-md">
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">useDebounceFn</span>
<span class="demo-label">useDebounceFn</span>
<span
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
:class="runSearch.isPending.value
? 'border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-400'
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
: 'border-border bg-bg-inset text-fg-muted'"
>
<span
class="size-1.5 rounded-full transition"
:class="runSearch.isPending.value ? 'animate-pulse bg-sky-500' : 'bg-(--fg-subtle)'"
:class="runSearch.isPending.value ? 'animate-pulse bg-sky-500' : 'bg-fg-subtle'"
/>
{{ runSearch.isPending.value ? 'Pending' : 'Settled' }}
</span>
</div>
<div class="flex flex-col gap-2">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="search">
<label class="demo-label" for="search">
Type to search
</label>
<input
@@ -49,13 +49,13 @@ function onInput(event: Event) {
:value="query"
type="text"
placeholder="Search the docs…"
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"
@input="onInput"
>
</div>
<div class="flex items-center gap-3">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="delay">
<label class="demo-label" for="delay">
Delay
</label>
<input
@@ -65,26 +65,26 @@ function onInput(event: Event) {
min="0"
max="1500"
step="100"
class="flex-1 accent-(--accent)"
class="flex-1 accent-accent"
>
<span class="w-16 text-right font-mono text-sm tabular-nums text-(--fg)">{{ delay }}ms</span>
<span class="w-16 text-right font-mono text-sm tabular-nums text-fg">{{ delay }}ms</span>
</div>
<div class="grid grid-cols-2 gap-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)">Invocations</p>
<p class="mt-1 font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ calls }}</p>
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label">Invocations</p>
<p class="demo-stat mt-1 text-3xl">{{ calls }}</p>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Last result</p>
<p class="mt-1 truncate font-mono text-sm text-(--fg)">{{ lastResult || '—' }}</p>
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label">Last result</p>
<p class="mt-1 truncate font-mono text-sm text-fg">{{ lastResult || '—' }}</p>
</div>
</div>
<div class="flex flex-wrap gap-2">
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:border-(--border-strong) hover:bg-(--bg-inset) 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="!runSearch.isPending.value"
@click="runSearch.flush()"
>
@@ -92,7 +92,7 @@ function onInput(event: Event) {
</button>
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:border-(--border-strong) hover:bg-(--bg-inset) 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="!runSearch.isPending.value"
@click="runSearch.cancel()"
>
@@ -100,9 +100,9 @@ function onInput(event: Event) {
</button>
</div>
<div v-if="log.length" 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)">Recent calls</p>
<ul class="space-y-1 font-mono text-xs text-(--fg-muted)">
<div v-if="log.length" class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label mb-2">Recent calls</p>
<ul class="space-y-1 font-mono text-xs text-fg-muted">
<li v-for="(entry, i) in log" :key="i" class="truncate">{{ entry }}</li>
</ul>
</div>
@@ -12,22 +12,22 @@ function pick(theme: string) {
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">usePrevious</span>
<div class="demo-stack max-w-sm">
<span class="demo-label">usePrevious</span>
<div class="grid grid-cols-2 gap-3">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Current</p>
<p class="mt-1 truncate text-2xl font-bold text-(--fg)">{{ selected }}</p>
<div class="demo-card p-4">
<p class="demo-label">Current</p>
<p class="mt-1 truncate text-2xl font-bold text-fg">{{ selected }}</p>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-inset) p-4">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Previous</p>
<p class="mt-1 truncate text-2xl font-bold text-(--fg-muted)">{{ previous }}</p>
<div class="rounded-xl border border-border bg-bg-inset p-4">
<p class="demo-label">Previous</p>
<p class="mt-1 truncate text-2xl font-bold text-fg-muted">{{ previous }}</p>
</div>
</div>
<div class="flex flex-col gap-2">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Select a theme</p>
<p class="demo-label">Select a theme</p>
<div class="flex flex-wrap gap-2">
<button
v-for="theme in themes"
@@ -35,8 +35,8 @@ function pick(theme: string) {
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="theme === selected
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:border-(--border-strong) hover:bg-(--bg-inset)'"
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
: 'border-border bg-bg-elevated text-fg hover:border-border-strong hover:bg-bg-inset'"
@click="pick(theme)"
>
{{ theme }}
@@ -44,8 +44,8 @@ function pick(theme: string) {
</div>
</div>
<p class="text-xs text-(--fg-subtle)">
Seeded with <span class="font-mono text-(--fg-muted)">"None"</span> as the initial previous value until the source first changes.
<p class="text-xs text-fg-subtle">
Seeded with <span class="font-mono text-fg-muted">"None"</span> as the initial previous value until the source first changes.
</p>
</div>
</template>
@@ -15,11 +15,11 @@ const presets = ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#0ea5e9'];
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">useSyncRefs</span>
<div class="demo-stack max-w-md">
<span class="demo-label">useSyncRefs</span>
<div class="flex flex-col gap-2">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="color">
<label class="demo-label" for="color">
Source ref
</label>
<div class="flex items-center gap-2">
@@ -27,33 +27,33 @@ const presets = ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#0ea5e9'];
id="color"
v-model="source"
type="color"
class="h-9 w-12 shrink-0 cursor-pointer rounded-lg border border-(--border) bg-(--bg)"
class="h-9 w-12 shrink-0 cursor-pointer rounded-lg border border-border bg-bg"
>
<input
v-model="source"
type="text"
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>
</div>
<p class="text-xs text-(--fg-subtle)">Three independent target refs stay synced to the source:</p>
<p class="text-xs text-fg-subtle">Three independent target refs stay synced to the source:</p>
<div class="grid grid-cols-3 gap-3">
<div class="flex flex-col items-center gap-2 rounded-xl border border-(--border) bg-(--bg-elevated) p-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">swatch</span>
<div class="demo-card flex flex-col items-center gap-2 p-3">
<span class="demo-label">swatch</span>
<span
class="size-10 rounded-lg border border-(--border-strong) shadow-sm transition"
class="size-10 rounded-lg border border-border-strong shadow-sm transition"
:style="{ backgroundColor: swatch }"
/>
</div>
<div class="flex flex-col items-center justify-center gap-2 rounded-xl border border-(--border) bg-(--bg-elevated) p-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">label</span>
<span class="font-mono text-xs text-(--fg)">{{ label }}</span>
<div class="demo-card flex flex-col items-center justify-center gap-2 p-3">
<span class="demo-label">label</span>
<span class="font-mono text-xs text-fg">{{ label }}</span>
</div>
<div class="flex flex-col items-center justify-center gap-2 rounded-xl border border-(--border) bg-(--bg-inset) p-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">hex</span>
<span class="font-mono text-xs uppercase text-(--fg)">{{ hex }}</span>
<div class="flex flex-col items-center justify-center gap-2 rounded-xl border border-border bg-bg-inset p-3">
<span class="demo-label">hex</span>
<span class="font-mono text-xs uppercase text-fg">{{ hex }}</span>
</div>
</div>
@@ -62,7 +62,7 @@ const presets = ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#0ea5e9'];
v-for="preset in presets"
:key="preset"
type="button"
class="size-7 rounded-md border border-(--border) transition hover:scale-110 active:scale-95 cursor-pointer"
class="size-7 rounded-md border border-border transition hover:scale-110 active:scale-95 cursor-pointer"
:style="{ backgroundColor: preset }"
:aria-label="`Set source to ${preset}`"
@click="source = preset"
@@ -24,24 +24,24 @@ const ratio = () => (moves.value ? Math.round((fires.value / moves.value) * 100)
</script>
<template>
<div class="flex w-full max-w-md flex-col gap-4">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">useThrottleFn</span>
<div class="demo-stack max-w-md">
<span class="demo-label">useThrottleFn</span>
<div
class="relative h-40 w-full overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset) touch-none"
class="relative h-40 w-full overflow-hidden rounded-xl border border-border bg-bg-inset touch-none"
@pointermove="handlePointer"
>
<span
class="pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-(--accent-fg) bg-(--accent) shadow-md transition-all duration-150 ease-out"
class="pointer-events-none absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-accent-fg bg-accent shadow-md transition-all duration-150 ease-out"
:style="{ left: `${position.x}%`, top: `${position.y}%` }"
/>
<span class="pointer-events-none absolute inset-x-0 bottom-2 text-center text-xs text-(--fg-subtle)">
<span class="pointer-events-none absolute inset-x-0 bottom-2 text-center text-xs text-fg-subtle">
Move your pointer over this area
</span>
</div>
<div class="flex items-center gap-3">
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="window">
<label class="demo-label" for="window">
Window
</label>
<input
@@ -51,40 +51,40 @@ const ratio = () => (moves.value ? Math.round((fires.value / moves.value) * 100)
min="0"
max="1000"
step="50"
class="flex-1 accent-(--accent)"
class="flex-1 accent-accent"
>
<span class="w-16 text-right font-mono text-sm tabular-nums text-(--fg)">{{ delay }}ms</span>
<span class="w-16 text-right font-mono text-sm tabular-nums text-fg">{{ delay }}ms</span>
</div>
<div class="grid grid-cols-3 gap-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)">Events</p>
<p class="mt-1 font-mono text-2xl font-bold tabular-nums text-(--fg)">{{ moves }}</p>
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label">Events</p>
<p class="demo-stat mt-1 text-2xl">{{ moves }}</p>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Fired</p>
<p class="mt-1 font-mono text-2xl font-bold tabular-nums text-(--accent-text)">{{ fires }}</p>
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label">Fired</p>
<p class="mt-1 font-mono text-2xl font-bold tabular-nums text-accent-text">{{ fires }}</p>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Ran</p>
<p class="mt-1 font-mono text-2xl font-bold tabular-nums text-(--fg)">{{ ratio() }}%</p>
<div class="rounded-lg border border-border bg-bg-inset p-3">
<p class="demo-label">Ran</p>
<p class="demo-stat mt-1 text-2xl">{{ ratio() }}%</p>
</div>
</div>
<div class="flex items-center justify-between gap-2">
<span class="rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 font-mono text-xs text-(--fg-muted)">
<span class="rounded-md border border-border bg-bg-inset px-2 py-0.5 font-mono text-xs text-fg-muted">
x: {{ position.x }} · y: {{ position.y }}
</span>
<button
type="button"
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:border-(--border-strong) hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
class="demo-btn"
@click="onMove.flush()"
>
Flush trailing
</button>
</div>
<p class="text-xs text-(--fg-subtle)">
<p class="text-xs text-fg-subtle">
Leading + trailing throttling caps the handler to once per window drag faster and watch the fired count lag behind events.
</p>
</div>
@@ -2,6 +2,7 @@ import { isRef, toValue } from 'vue';
import type { MaybeRefOrGetter } from 'vue';
import { isFunction, isObject, throttle } from '@robonen/stdlib';
import type { AnyFunction } from '@robonen/stdlib';
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';
export interface UseThrottleFnOptions {
/**
@@ -181,5 +182,9 @@ export function useThrottleFn<T extends AnyFunction>(
throttled.flush();
};
// Cancel any pending trailing invocation when the scope is disposed, so it
// can't fire after the component unmounts (mirrors useDebounceFn).
tryOnScopeDispose(wrapper.cancel);
return wrapper;
}
@@ -23,14 +23,14 @@ const presets = ['42.50', '3.14159', '255.9', 'abc', '-12'];
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="demo-stack max-w-sm">
<label class="flex flex-col gap-1.5">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Source value</span>
<span class="demo-label">Source value</span>
<input
v-model="source"
type="text"
placeholder="Type a number…"
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>
@@ -39,36 +39,36 @@ const presets = ['42.50', '3.14159', '255.9', 'abc', '-12'];
v-for="v in presets"
:key="v"
type="button"
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) transition hover:bg-(--bg-elevated) hover:border-(--border-strong) cursor-pointer"
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 transition hover:bg-bg-elevated hover:border-border-strong cursor-pointer"
@click="source = v"
>
{{ v }}
</button>
</div>
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) divide-y divide-(--border)">
<div class="demo-card divide-y divide-border">
<div
v-for="variant in variants"
:key="variant.label"
class="flex items-center justify-between gap-3 px-4 py-2.5"
>
<div class="flex flex-col">
<span class="font-mono text-sm text-(--fg)">{{ variant.label }}</span>
<span class="text-xs text-(--fg-subtle)">{{ variant.desc }}</span>
<span class="font-mono text-sm text-fg">{{ variant.label }}</span>
<span class="text-xs text-fg-subtle">{{ variant.desc }}</span>
</div>
<span
class="font-mono text-lg font-semibold tabular-nums"
:class="Number.isNaN(variant.value) ? 'text-amber-600 dark:text-amber-400' : 'text-(--fg)'"
:class="Number.isNaN(variant.value) ? 'text-amber-600 dark:text-amber-400' : 'text-fg'"
>
{{ Number.isNaN(variant.value) ? 'NaN' : variant.value }}
</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle) leading-relaxed">
One reactive source, four <span class="font-mono text-(--accent-text)">useToNumber</span>
instances. Try <span class="font-mono text-(--fg-muted)">abc</span> to see how
<span class="font-mono text-(--fg-muted)">nanToZero</span> and clamping tame invalid input.
<p class="text-xs text-fg-subtle leading-relaxed">
One reactive source, four <span class="font-mono text-accent-text">useToNumber</span>
instances. Try <span class="font-mono text-fg-muted">abc</span> to see how
<span class="font-mono text-fg-muted">nanToZero</span> and clamping tame invalid input.
</p>
</div>
</template>
@@ -26,54 +26,54 @@ const rows = [
</script>
<template>
<div class="w-full max-w-sm flex flex-col gap-4">
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-3">
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Live source values</span>
<div class="demo-stack max-w-sm">
<div class="demo-card p-4 flex flex-col gap-3">
<span class="demo-label">Live source values</span>
<div class="flex items-center justify-between gap-3">
<label class="text-sm text-(--fg)">count</label>
<label class="text-sm text-fg">count</label>
<div class="flex items-center gap-2">
<button
type="button"
class="inline-flex h-7 w-7 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-7 w-7 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="count--"
></button>
<span class="font-mono text-sm tabular-nums text-(--fg) w-6 text-center">{{ count }}</span>
<span class="font-mono text-sm tabular-nums text-fg w-6 text-center">{{ count }}</span>
<button
type="button"
class="inline-flex h-7 w-7 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-7 w-7 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="count++"
>+</button>
</div>
</div>
<div class="flex items-center justify-between gap-3">
<label class="text-sm text-(--fg)" for="ratio">ratio</label>
<input id="ratio" v-model.number="ratio" type="range" min="0" max="1" step="0.01" class="accent-(--accent) cursor-pointer">
<label class="text-sm text-fg" for="ratio">ratio</label>
<input id="ratio" v-model.number="ratio" type="range" min="0" max="1" step="0.01" class="accent-accent cursor-pointer">
</div>
<label class="flex items-center justify-between gap-3 cursor-pointer">
<span class="text-sm text-(--fg)">enabled</span>
<input v-model="enabled" type="checkbox" class="size-4 accent-(--accent) cursor-pointer">
<span class="text-sm text-fg">enabled</span>
<input v-model="enabled" type="checkbox" class="size-4 accent-accent cursor-pointer">
</label>
</div>
<div class="rounded-lg border border-(--border) bg-(--bg-inset) divide-y divide-(--border) font-mono text-sm">
<div class="rounded-lg border border-border bg-bg-inset divide-y divide-border font-mono text-sm">
<div
v-for="row in rows"
:key="row.type"
class="flex items-center gap-3 px-3 py-2"
>
<span 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) shrink-0">
<span 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 shrink-0">
{{ row.type }}
</span>
<span class="text-(--fg) truncate">"{{ row.value }}"</span>
<span class="text-fg truncate">"{{ row.value }}"</span>
</div>
</div>
<p class="text-xs text-(--fg-subtle) leading-relaxed">
<span class="font-mono text-(--accent-text)">useToString</span> is
<span class="font-mono text-(--fg-muted)">computed(() =&gt; String(toValue(v)))</span>
<p class="text-xs text-fg-subtle leading-relaxed">
<span class="font-mono text-accent-text">useToString</span> is
<span class="font-mono text-fg-muted">computed(() =&gt; String(toValue(v)))</span>
it stringifies refs, getters, and reactive objects alike.
</p>
</div>