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:
+17
-17
@@ -29,33 +29,33 @@ const { count, increment, decrement, reset } = useCounter(0, { min: 0, max: 10 }
|
||||
|
||||
<!-- Feature highlights -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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>
|
||||
<p class="text-sm text-(--fg-muted) leading-relaxed">
|
||||
<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>
|
||||
<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
|
||||
doesn't need — your bundle stays exactly as small as your usage.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p class="text-sm text-(--fg-muted) leading-relaxed">
|
||||
<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>
|
||||
<p class="text-sm text-fg-muted leading-relaxed">
|
||||
Browser-only access is guarded behind lifecycle hooks and configurable
|
||||
<code>window</code>/<code>document</code> targets, so Nuxt and SSR setups just work.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p class="text-sm text-(--fg-muted) leading-relaxed">
|
||||
<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>
|
||||
<p class="text-sm text-fg-muted leading-relaxed">
|
||||
Written in TypeScript with precise return types and generics. <code>MaybeRefOrGetter</code>
|
||||
arguments mean you can pass plain values, refs or getters interchangeably.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p class="text-sm text-(--fg-muted) leading-relaxed">
|
||||
<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>
|
||||
<p class="text-sm text-fg-muted leading-relaxed">
|
||||
From state and reactivity to sensors, elements, storage, math and form handling —
|
||||
one cohesive toolkit spanning the whole surface of a Vue app.
|
||||
</p>
|
||||
@@ -101,19 +101,19 @@ useEventListener('keydown', (e) => {
|
||||
<p>The same <code>useCounter</code> running live:</p>
|
||||
</div>
|
||||
<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
|
||||
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"
|
||||
@click="decrement()"
|
||||
>
|
||||
−
|
||||
</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
|
||||
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"
|
||||
@click="increment()"
|
||||
>
|
||||
@@ -121,7 +121,7 @@ useEventListener('keydown', (e) => {
|
||||
</button>
|
||||
<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()"
|
||||
>
|
||||
Reset
|
||||
|
||||
@@ -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 'paused': return 'bg-amber-500';
|
||||
case 'finished': return 'bg-sky-500';
|
||||
default: return 'bg-(--border-strong)';
|
||||
default: return 'bg-border-strong';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ const rates = [0.5, 1, 2] as const;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-600 dark:text-amber-400"
|
||||
@@ -57,28 +57,28 @@ const rates = [0.5, 1, 2] as const;
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex h-28 items-center justify-center overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset)">
|
||||
<div class="flex h-28 items-center justify-center overflow-hidden rounded-xl border border-border bg-bg-inset">
|
||||
<div
|
||||
ref="target"
|
||||
class="size-12 bg-(--accent) shadow-lg"
|
||||
class="size-12 bg-accent shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<div class="demo-label">
|
||||
State
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<span class="inline-block size-2 rounded-full transition" :class="stateColor" />
|
||||
<span class="font-mono text-sm text-(--fg)">{{ playState }}</span>
|
||||
<span class="font-mono text-sm text-fg">{{ playState }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<div class="demo-label">
|
||||
Current time
|
||||
</div>
|
||||
<div class="mt-1 font-mono text-sm tabular-nums text-(--fg)">
|
||||
<div class="mt-1 font-mono text-sm tabular-nums text-fg">
|
||||
{{ elapsed }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,31 +86,31 @@ const rates = [0.5, 1, 2] as const;
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary"
|
||||
@click="play"
|
||||
>
|
||||
Play
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="pause"
|
||||
>
|
||||
Pause
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="reverse"
|
||||
>
|
||||
Reverse
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="finish"
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
<button
|
||||
class="col-span-2 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn col-span-2"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
@@ -118,7 +118,7 @@ const rates = [0.5, 1, 2] as const;
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-label">
|
||||
Playback rate
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -127,8 +127,8 @@ const rates = [0.5, 1, 2] as const;
|
||||
:key="rate"
|
||||
class="flex-1 rounded-lg border px-3 py-1.5 text-sm font-medium tabular-nums transition active:scale-[0.98] cursor-pointer"
|
||||
:class="playbackRate === rate
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="playbackRate = rate"
|
||||
>
|
||||
{{ rate }}×
|
||||
|
||||
@@ -37,9 +37,9 @@ function toggle() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5 text-center">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-5 text-center">
|
||||
<div class="demo-label">
|
||||
Time remaining
|
||||
</div>
|
||||
<div
|
||||
@@ -48,22 +48,22 @@ function toggle() {
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: remaining <= 10 && remaining > 0
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
: 'text-(--fg)'"
|
||||
: 'text-fg'"
|
||||
>
|
||||
{{ minutes }}:{{ seconds }}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 h-1.5 w-full overflow-hidden rounded-full bg-(--bg-inset)">
|
||||
<div class="mt-4 h-1.5 w-full overflow-hidden rounded-full bg-bg-inset">
|
||||
<div
|
||||
class="h-full rounded-full bg-(--accent) transition-[width] duration-300 ease-linear"
|
||||
class="h-full rounded-full bg-accent transition-[width] duration-300 ease-linear"
|
||||
:style="{ width: `${progress * 100}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center justify-center gap-2 text-xs text-(--fg-subtle)">
|
||||
<div class="mt-3 flex items-center justify-center gap-2 text-xs text-fg-subtle">
|
||||
<span
|
||||
class="inline-block size-2 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : justFinished ? 'bg-sky-500' : 'bg-(--border-strong)'"
|
||||
:class="isActive ? 'bg-emerald-500' : justFinished ? 'bg-sky-500' : 'bg-border-strong'"
|
||||
/>
|
||||
{{ justFinished ? 'Completed' : isActive ? 'Counting down' : 'Paused' }}
|
||||
</div>
|
||||
@@ -73,7 +73,7 @@ function toggle() {
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
:key="preset"
|
||||
class="rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium tabular-nums text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="rounded-lg border border-border bg-bg-elevated px-3 py-1.5 text-sm font-medium tabular-nums text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.98] cursor-pointer"
|
||||
@click="setPreset(preset)"
|
||||
>
|
||||
{{ preset < 60 ? `${preset}s` : `${preset / 60}m` }}
|
||||
@@ -82,20 +82,20 @@ function toggle() {
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="remaining === 0 && isActive"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="start()"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="stop"
|
||||
>
|
||||
Stop
|
||||
|
||||
@@ -27,46 +27,46 @@ const isValid = computed(() => formatted.value !== 'Invalid Date');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="demo-card p-4">
|
||||
<div class="demo-label">
|
||||
Formatted output
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 font-mono text-lg font-semibold tabular-nums"
|
||||
:class="isValid ? 'text-(--fg)' : 'text-red-600 dark:text-red-400'"
|
||||
:class="isValid ? 'text-fg' : 'text-red-600 dark:text-red-400'"
|
||||
>
|
||||
{{ formatted }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<label class="demo-label">
|
||||
Date input
|
||||
</label>
|
||||
<input
|
||||
v-model="date"
|
||||
type="datetime-local"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<label class="demo-label">
|
||||
Format token string
|
||||
</label>
|
||||
<input
|
||||
v-model="format"
|
||||
type="text"
|
||||
spellcheck="false"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 font-mono text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input font-mono"
|
||||
>
|
||||
<div class="flex flex-wrap gap-1.5 pt-1">
|
||||
<button
|
||||
v-for="f in formats"
|
||||
:key="f"
|
||||
class="rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 font-mono text-xs text-(--fg-muted) transition hover:bg-(--bg-elevated) hover:text-(--fg) active:scale-[0.98] cursor-pointer"
|
||||
:class="{ 'border-(--accent) text-(--accent-text)': format === f }"
|
||||
class="rounded-md border border-border bg-bg-inset px-2 py-0.5 font-mono text-xs text-fg-muted transition hover:bg-bg-elevated hover:text-fg active:scale-[0.98] cursor-pointer"
|
||||
:class="{ 'border-accent text-accent-text': format === f }"
|
||||
@click="format = f"
|
||||
>
|
||||
{{ f }}
|
||||
@@ -75,7 +75,7 @@ const isValid = computed(() => formatted.value !== 'Invalid Date');
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<label class="demo-label">
|
||||
Locale
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@@ -84,8 +84,8 @@ const isValid = computed(() => formatted.value !== 'Invalid Date');
|
||||
:key="loc.value"
|
||||
class="rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="locale === loc.value
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="locale = loc.value"
|
||||
>
|
||||
{{ loc.label }}
|
||||
|
||||
@@ -52,6 +52,7 @@ const REGEX_FORMAT
|
||||
// `20240101`); JS lacks possessive quantifiers to disambiguate it.
|
||||
// eslint-disable-next-line regexp/no-misleading-capturing-group
|
||||
const REGEX_PARSE = /* #__PURE__ */ /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[T\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/i;
|
||||
const REGEX_ISO_SUFFIX = /* #__PURE__ */ /z$/i;
|
||||
|
||||
const ORDINAL_SUFFIXES = ['th', 'st', 'nd', 'rd'] as const;
|
||||
|
||||
@@ -82,7 +83,7 @@ function formatOrdinal(num: number): string {
|
||||
export function normalizeDate(date: DateLike): Date {
|
||||
if (date === null || date === undefined) return new Date();
|
||||
if (isDate(date)) return new Date(date.getTime());
|
||||
if (isString(date) && !/z$/i.test(date)) {
|
||||
if (isString(date) && !REGEX_ISO_SUFFIX.test(date)) {
|
||||
const d = REGEX_PARSE.exec(date);
|
||||
if (d) {
|
||||
const month = d[2] ? Number(d[2]) - 1 : 0;
|
||||
|
||||
@@ -27,12 +27,12 @@ function toggle() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5 text-center">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-5 text-center">
|
||||
<div class="demo-label">
|
||||
Ticks elapsed
|
||||
</div>
|
||||
<div class="mt-2 font-mono text-5xl font-bold tabular-nums text-(--fg)">
|
||||
<div class="demo-stat mt-2 text-5xl">
|
||||
{{ counter }}
|
||||
</div>
|
||||
|
||||
@@ -41,21 +41,21 @@ function toggle() {
|
||||
v-for="(on, i) in beats"
|
||||
:key="i"
|
||||
class="size-2.5 rounded-full transition-colors duration-200"
|
||||
:class="on ? 'bg-(--accent)' : 'bg-(--bg-inset)'"
|
||||
:class="on ? 'bg-accent' : 'bg-bg-inset'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-center gap-2 text-xs text-(--fg-subtle)">
|
||||
<div class="mt-4 flex items-center justify-center gap-2 text-xs text-fg-subtle">
|
||||
<span
|
||||
class="inline-block size-2 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-(--border-strong)'"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-border-strong'"
|
||||
/>
|
||||
{{ isActive ? `Ticking every ${interval}ms` : 'Paused' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-label">
|
||||
Interval speed
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -64,8 +64,8 @@ function toggle() {
|
||||
:key="speed.value"
|
||||
class="flex-1 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="interval === speed.value
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="interval = speed.value"
|
||||
>
|
||||
{{ speed.label }}
|
||||
@@ -75,13 +75,13 @@ function toggle() {
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary flex-1"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="counter === 0"
|
||||
@click="reset"
|
||||
>
|
||||
|
||||
@@ -31,22 +31,22 @@ function clear() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="flex items-center justify-between rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card flex items-center justify-between p-4">
|
||||
<div>
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-label">
|
||||
Interval callback
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2 text-sm text-(--fg-muted)">
|
||||
<div class="mt-1 flex items-center gap-2 text-sm text-fg-muted">
|
||||
<span
|
||||
class="inline-block size-2 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-(--border-strong)'"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-border-strong'"
|
||||
/>
|
||||
{{ isActive ? `Firing every ${interval}ms` : 'Stopped' }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Start' }}
|
||||
@@ -54,7 +54,7 @@ function clear() {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-label">
|
||||
Interval
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -63,24 +63,24 @@ function clear() {
|
||||
:key="speed.value"
|
||||
class="flex-1 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="interval === speed.value
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="interval = speed.value"
|
||||
>
|
||||
{{ speed.label }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Changing the interval while running restarts the timer with the new duration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-label">
|
||||
Tick log
|
||||
</div>
|
||||
<button
|
||||
class="text-xs text-(--accent-text) transition hover:underline disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"
|
||||
class="text-xs text-accent-text transition hover:underline disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"
|
||||
:disabled="logs.length === 0"
|
||||
@click="clear"
|
||||
>
|
||||
@@ -88,17 +88,17 @@ function clear() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="min-h-32 rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<p v-if="logs.length === 0" class="py-6 text-center text-sm text-(--fg-subtle)">
|
||||
<div class="min-h-32 rounded-lg border border-border bg-bg-inset p-3">
|
||||
<p v-if="logs.length === 0" class="py-6 text-center text-sm text-fg-subtle">
|
||||
No ticks yet — press Start.
|
||||
</p>
|
||||
<ul v-else class="flex flex-col gap-1.5">
|
||||
<li
|
||||
v-for="log in logs"
|
||||
:key="log.id"
|
||||
class="flex items-center gap-2 font-mono text-sm tabular-nums text-(--fg)"
|
||||
class="flex items-center gap-2 font-mono text-sm tabular-nums text-fg"
|
||||
>
|
||||
<span class="inline-block size-1.5 rounded-full bg-(--accent)" />
|
||||
<span class="inline-block size-1.5 rounded-full bg-accent" />
|
||||
{{ log.time }}
|
||||
</li>
|
||||
</ul>
|
||||
@@ -106,14 +106,14 @@ function clear() {
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="isActive"
|
||||
@click="resume"
|
||||
>
|
||||
Resume
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!isActive"
|
||||
@click="pause"
|
||||
>
|
||||
|
||||
@@ -20,23 +20,23 @@ const secondAngle = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col items-center gap-3">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Reactive now</div>
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4 flex flex-col items-center gap-3">
|
||||
<div class="demo-label">Reactive now</div>
|
||||
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ time }}</span>
|
||||
<span class="font-mono text-lg font-semibold tabular-nums text-(--fg-subtle)">.{{ millis }}</span>
|
||||
<span class="demo-stat text-3xl">{{ time }}</span>
|
||||
<span class="font-mono text-lg font-semibold tabular-nums text-fg-subtle">.{{ millis }}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-(--fg-muted)">{{ date }}</div>
|
||||
<div class="text-sm text-fg-muted">{{ date }}</div>
|
||||
|
||||
<div class="relative mt-1 size-24 rounded-full border-2 border-(--border-strong) bg-(--bg-inset)">
|
||||
<div class="relative mt-1 size-24 rounded-full border-2 border-border-strong bg-bg-inset">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="size-1.5 rounded-full bg-(--accent)" />
|
||||
<div class="size-1.5 rounded-full bg-accent" />
|
||||
</div>
|
||||
<div
|
||||
class="absolute bottom-1/2 left-1/2 h-9 w-0.5 origin-bottom rounded-full bg-(--accent)"
|
||||
class="absolute bottom-1/2 left-1/2 h-9 w-0.5 origin-bottom rounded-full bg-accent"
|
||||
:style="{ transform: `translateX(-50%) rotate(${secondAngle}deg)` }"
|
||||
/>
|
||||
</div>
|
||||
@@ -44,11 +44,11 @@ const secondAngle = computed(() => {
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
class="demo-badge"
|
||||
>
|
||||
<span
|
||||
class="size-1.5 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-fg-subtle'"
|
||||
/>
|
||||
{{ isActive ? 'Ticking (RAF)' : 'Paused' }}
|
||||
</span>
|
||||
@@ -56,7 +56,7 @@ const secondAngle = computed(() => {
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
@@ -64,7 +64,7 @@ const secondAngle = computed(() => {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isActive"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="resume"
|
||||
>
|
||||
Resume
|
||||
@@ -72,7 +72,7 @@ const secondAngle = computed(() => {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isActive"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="pause"
|
||||
>
|
||||
Pause
|
||||
|
||||
@@ -35,46 +35,46 @@ const limitLabel = computed(() => (fpsLimit.value === 0 ? 'Unlimited' : `${fpsLi
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">requestAnimationFrame</span>
|
||||
<span class="demo-label">requestAnimationFrame</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
class="demo-badge"
|
||||
>
|
||||
<span class="size-1.5 rounded-full transition" :class="isActive ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
<span class="size-1.5 rounded-full transition" :class="isActive ? 'bg-emerald-500' : 'bg-fg-subtle'" />
|
||||
{{ isActive ? 'Running' : 'Paused' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- The animated track: marker position is updated every frame -->
|
||||
<div class="relative mx-2.5 h-8 rounded-lg border border-(--border) bg-(--bg-inset)">
|
||||
<div class="relative mx-2.5 h-8 rounded-lg border border-border bg-bg-inset">
|
||||
<div
|
||||
class="absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-(--accent) shadow"
|
||||
class="absolute top-1/2 size-5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent shadow"
|
||||
:style="{ left: `${position}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-2 text-center">
|
||||
<div class="font-mono text-lg font-bold tabular-nums text-(--fg)">{{ fps }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">fps</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-2 text-center">
|
||||
<div class="demo-stat text-lg">{{ fps }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">fps</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-2 text-center">
|
||||
<div class="font-mono text-lg font-bold tabular-nums text-(--fg)">{{ delta.toFixed(1) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">delta ms</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-2 text-center">
|
||||
<div class="demo-stat text-lg">{{ delta.toFixed(1) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">delta ms</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-2 text-center">
|
||||
<div class="font-mono text-lg font-bold tabular-nums text-(--fg)">{{ frames }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">frames</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-2 text-center">
|
||||
<div class="demo-stat text-lg">{{ frames }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">frames</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="fps-limit">FPS limit</label>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ limitLabel }}</span>
|
||||
<label class="demo-label" for="fps-limit">FPS limit</label>
|
||||
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ limitLabel }}</span>
|
||||
</div>
|
||||
<input
|
||||
id="fps-limit"
|
||||
@@ -83,14 +83,14 @@ const limitLabel = computed(() => (fpsLimit.value === 0 ? 'Unlimited' : `${fpsLi
|
||||
min="0"
|
||||
max="60"
|
||||
step="5"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
class="w-full accent-accent cursor-pointer"
|
||||
>
|
||||
<p class="text-xs text-(--fg-subtle)">Changing the limit takes effect on the next mount; toggle below to see it live.</p>
|
||||
<p class="text-xs text-fg-subtle">Changing the limit takes effect on the next mount; toggle below to see it live.</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause loop' : 'Resume loop' }}
|
||||
|
||||
@@ -42,15 +42,15 @@ const absolute = computed(() =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col items-center gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Relative time</span>
|
||||
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg) text-center">{{ timeAgo }}</span>
|
||||
<span class="text-xs text-(--fg-muted)">{{ absolute }}</span>
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4 flex flex-col items-center gap-2">
|
||||
<span class="demo-label">Relative time</span>
|
||||
<span class="demo-stat text-3xl text-center">{{ timeAgo }}</span>
|
||||
<span class="text-xs text-fg-muted">{{ absolute }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Pick an instant</span>
|
||||
<span class="demo-label">Pick an instant</span>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
@@ -58,8 +58,8 @@ const absolute = computed(() =>
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="offset === preset.offset
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="offset = preset.offset"
|
||||
>
|
||||
{{ preset.label }}
|
||||
@@ -69,14 +69,14 @@ const absolute = computed(() =>
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
class="demo-badge"
|
||||
>
|
||||
<span class="size-1.5 rounded-full transition" :class="isActive ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
<span class="size-1.5 rounded-full transition" :class="isActive ? 'bg-emerald-500' : 'bg-fg-subtle'" />
|
||||
{{ isActive ? 'Updating every 1s' : 'Updates paused' }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
|
||||
@@ -165,10 +165,12 @@ const DEFAULT_UNITS: Array<UseTimeAgoUnit<UseTimeAgoUnitName>> = [
|
||||
{ max: Number.POSITIVE_INFINITY, value: 31536000000, name: 'year' },
|
||||
];
|
||||
|
||||
const REGEX_DIGIT = /* #__PURE__ */ /\d/;
|
||||
|
||||
const DEFAULT_MESSAGES: UseTimeAgoMessages<UseTimeAgoUnitName> = {
|
||||
justNow: 'just now',
|
||||
past: n => /\d/.test(n) ? `${n} ago` : n,
|
||||
future: n => /\d/.test(n) ? `in ${n}` : n,
|
||||
past: n => REGEX_DIGIT.test(n) ? `${n} ago` : n,
|
||||
future: n => REGEX_DIGIT.test(n) ? `in ${n}` : n,
|
||||
month: (n, past) => n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`,
|
||||
year: (n, past) => n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`,
|
||||
day: (n, past) => n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`,
|
||||
|
||||
@@ -24,21 +24,21 @@ function cancel() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col items-center gap-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Status</span>
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4 flex flex-col items-center gap-3">
|
||||
<span class="demo-label">Status</span>
|
||||
|
||||
<div
|
||||
class="flex size-20 items-center justify-center rounded-full border-2 transition"
|
||||
:class="ready
|
||||
? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'"
|
||||
: 'border-accent bg-accent-subtle text-accent-text'"
|
||||
>
|
||||
<span class="text-sm font-semibold">{{ ready ? 'Ready' : 'Pending' }}</span>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-sm text-(--fg-muted)">
|
||||
<template v-if="ready && firedAt">Fired at <span class="font-mono tabular-nums text-(--fg)">{{ firedAt }}</span></template>
|
||||
<p class="text-center text-sm text-fg-muted">
|
||||
<template v-if="ready && firedAt">Fired at <span class="font-mono tabular-nums text-fg">{{ firedAt }}</span></template>
|
||||
<template v-else-if="ready">Idle — start the timer below</template>
|
||||
<template v-else>Counting down… stays pending until the delay elapses</template>
|
||||
</p>
|
||||
@@ -46,8 +46,8 @@ function cancel() {
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="delay">Delay</label>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ (delay / 1000).toFixed(1) }}s</span>
|
||||
<label class="demo-label" for="delay">Delay</label>
|
||||
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ (delay / 1000).toFixed(1) }}s</span>
|
||||
</div>
|
||||
<input
|
||||
id="delay"
|
||||
@@ -56,14 +56,14 @@ function cancel() {
|
||||
min="500"
|
||||
max="5000"
|
||||
step="500"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
class="w-full accent-accent cursor-pointer"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary flex-1"
|
||||
@click="restart"
|
||||
>
|
||||
{{ ready ? 'Start' : 'Restart' }}
|
||||
@@ -71,7 +71,7 @@ function cancel() {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="ready"
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -42,23 +42,23 @@ function undo() {
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Inbox · undo with grace period</span>
|
||||
<span class="demo-label">Inbox · undo with grace period</span>
|
||||
|
||||
<ul v-if="inbox.length" class="flex flex-col gap-2">
|
||||
<li
|
||||
v-for="mail in inbox"
|
||||
:key="mail.id"
|
||||
class="flex items-center justify-between gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-3 transition"
|
||||
class="demo-card flex items-center justify-between gap-3 p-3 transition"
|
||||
:class="{ 'opacity-40': pendingDelete?.id === mail.id }"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-medium text-(--fg)">{{ mail.subject }}</div>
|
||||
<div class="truncate text-xs text-(--fg-muted)">{{ mail.from }}</div>
|
||||
<div class="truncate text-sm font-medium text-fg">{{ mail.subject }}</div>
|
||||
<div class="truncate text-xs text-fg-muted">{{ mail.from }}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isPending"
|
||||
class="shrink-0 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn shrink-0 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="archive(mail)"
|
||||
>
|
||||
Archive
|
||||
@@ -66,7 +66,7 @@ function undo() {
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-else class="rounded-xl border border-dashed border-(--border) bg-(--bg-inset) p-6 text-center text-sm text-(--fg-subtle)">
|
||||
<div v-else class="rounded-xl border border-dashed border-border bg-bg-inset p-6 text-center text-sm text-fg-subtle">
|
||||
Inbox zero — everything archived.
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface UseTimeoutFnOptions {
|
||||
immediateCallback?: boolean;
|
||||
}
|
||||
|
||||
export interface UseTimeoutFnReturn<Args extends any[]> {
|
||||
export interface UseTimeoutFnReturn<Args extends unknown[]> {
|
||||
/**
|
||||
* Whether the timeout is currently pending
|
||||
*/
|
||||
|
||||
@@ -45,33 +45,33 @@ function resetOffset() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 text-center">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4 text-center">
|
||||
<div class="demo-label">
|
||||
Reactive timestamp
|
||||
</div>
|
||||
<div class="mt-2 font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
<div class="demo-stat mt-2 text-3xl">
|
||||
{{ clockTime }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-(--fg-muted)">
|
||||
<div class="mt-1 text-sm text-fg-muted">
|
||||
{{ clockDate }}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-center gap-2 text-xs text-(--fg-subtle)">
|
||||
<div class="mt-3 flex items-center justify-center gap-2 text-xs text-fg-subtle">
|
||||
<span
|
||||
class="inline-block size-2 rounded-full transition"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-(--border-strong)'"
|
||||
:class="isActive ? 'bg-emerald-500' : 'bg-border-strong'"
|
||||
/>
|
||||
{{ isActive ? 'Updating every second' : 'Paused' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
{{ Math.round(timestamp) }} ms
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ isActive ? 'Pause' : 'Resume' }}
|
||||
@@ -79,13 +79,13 @@ function resetOffset() {
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="shift(-3600_000)"
|
||||
>
|
||||
-1h
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="shift(3600_000)"
|
||||
>
|
||||
+1h
|
||||
@@ -93,13 +93,13 @@ function resetOffset() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-sm text-(--fg-muted)">
|
||||
<div class="flex items-center justify-between text-sm text-fg-muted">
|
||||
<span>
|
||||
Offset:
|
||||
<span class="font-mono text-(--fg) tabular-nums">{{ (offset / 3600_000).toFixed(0) }}h</span>
|
||||
<span class="font-mono text-fg tabular-nums">{{ (offset / 3600_000).toFixed(0) }}h</span>
|
||||
</span>
|
||||
<button
|
||||
class="text-(--accent-text) transition hover:underline disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"
|
||||
class="text-accent-text transition hover:underline disabled:cursor-not-allowed disabled:opacity-40 cursor-pointer"
|
||||
:disabled="offset === 0"
|
||||
@click="resetOffset"
|
||||
>
|
||||
|
||||
@@ -40,19 +40,19 @@ function randomize() {
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-5">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card p-4">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Eased value
|
||||
</span>
|
||||
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
<span class="demo-stat text-3xl">
|
||||
{{ value.toFixed(1) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-(--bg-inset)">
|
||||
<div class="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-bg-inset">
|
||||
<div
|
||||
class="h-full rounded-full bg-(--accent)"
|
||||
class="h-full rounded-full bg-accent"
|
||||
:style="{ width: `${Math.max(0, Math.min(100, value))}%` }"
|
||||
/>
|
||||
</div>
|
||||
@@ -63,10 +63,10 @@ function randomize() {
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
class="h-1.5 flex-1 cursor-pointer accent-(--accent)"
|
||||
class="h-1.5 flex-1 cursor-pointer accent-accent"
|
||||
>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="randomize"
|
||||
>
|
||||
Random
|
||||
@@ -75,21 +75,21 @@ function randomize() {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<label class="demo-label">
|
||||
Easing preset
|
||||
</label>
|
||||
<select
|
||||
v-model="preset"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="w-full rounded-lg border border-border bg-bg px-3 py-2 text-sm text-fg transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option v-for="name in presetNames" :key="name" :value="name">
|
||||
{{ name }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<label class="mt-1 flex items-center justify-between text-sm text-(--fg-muted)">
|
||||
<label class="mt-1 flex items-center justify-between text-sm text-fg-muted">
|
||||
<span>Duration</span>
|
||||
<span class="font-mono text-(--fg) tabular-nums">{{ duration }}ms</span>
|
||||
<span class="font-mono text-fg tabular-nums">{{ duration }}ms</span>
|
||||
</label>
|
||||
<input
|
||||
v-model.number="duration"
|
||||
@@ -97,21 +97,21 @@ function randomize() {
|
||||
min="100"
|
||||
max="2000"
|
||||
step="100"
|
||||
class="h-1.5 w-full cursor-pointer accent-(--accent)"
|
||||
class="h-1.5 w-full cursor-pointer accent-accent"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="size-12 shrink-0 rounded-lg border border-(--border)"
|
||||
class="size-12 shrink-0 rounded-lg border border-border"
|
||||
:style="{ backgroundColor: colorCss }"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-label">
|
||||
Animated tuple
|
||||
</div>
|
||||
<div class="font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="font-mono text-sm text-fg tabular-nums">
|
||||
{{ colorCss }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,7 +121,7 @@ function randomize() {
|
||||
<button
|
||||
v-for="[label, rgb] in swatches"
|
||||
:key="label"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted) transition hover:border-(--border-strong) cursor-pointer"
|
||||
class="demo-badge transition hover:border-border-strong cursor-pointer"
|
||||
@click="colorTarget = [...rgb]"
|
||||
>
|
||||
<span class="size-2.5 rounded-full" :style="{ backgroundColor: `rgb(${rgb.join(',')})` }" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { clamp, isFunction, isNumber, lerp, noop } from '@robonen/stdlib';
|
||||
import { defaultWindow } from '@/types';
|
||||
import type { ConfigurableWindow } from '@/types';
|
||||
import { useRafFn } from '@/composables/animation/useRafFn';
|
||||
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';
|
||||
|
||||
/**
|
||||
* Cubic bezier control points `[x1, y1, x2, y2]` (the implied endpoints are
|
||||
@@ -356,5 +357,10 @@ export function useTransition<T extends TransitionValue>(
|
||||
},
|
||||
);
|
||||
|
||||
// The RAF loop is torn down by useRafFn on scope dispose, but a pending start
|
||||
// delay (window.setTimeout) is not — clear it so the timer can't fire into a
|
||||
// disposed scope.
|
||||
tryOnScopeDispose(clearDelay);
|
||||
|
||||
return computed(() => outputRef.value);
|
||||
}
|
||||
|
||||
@@ -40,16 +40,16 @@ function toggle(track: Track) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Library — tap to add / remove from playlist
|
||||
</span>
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-(--fg-muted)">
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-fg-muted">
|
||||
<input
|
||||
v-model="symmetric"
|
||||
type="checkbox"
|
||||
class="size-4 cursor-pointer accent-(--accent)"
|
||||
class="size-4 cursor-pointer accent-accent"
|
||||
>
|
||||
Symmetric
|
||||
</label>
|
||||
@@ -61,8 +61,8 @@ function toggle(track: Track) {
|
||||
:key="track.id"
|
||||
class="inline-flex items-center justify-between gap-2 rounded-lg border px-3 py-2 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="inPlaylist(track)
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="toggle(track)"
|
||||
>
|
||||
<span class="truncate">{{ track.title }}</span>
|
||||
@@ -70,12 +70,12 @@ function toggle(track: Track) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card p-4">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
{{ symmetric ? 'In exactly one (XOR)' : 'Not in playlist' }}
|
||||
</span>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg-muted)">
|
||||
<span class="font-mono text-sm tabular-nums text-fg-muted">
|
||||
{{ diff.length }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -84,12 +84,12 @@ function toggle(track: Track) {
|
||||
<li
|
||||
v-for="track in diff"
|
||||
:key="track.id"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
class="demo-badge"
|
||||
>
|
||||
{{ track.title }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="mt-3 text-sm text-(--fg-subtle)">
|
||||
<p v-else class="mt-3 text-sm text-fg-subtle">
|
||||
No difference — every track matches.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed, toValue } from 'vue';
|
||||
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
|
||||
import { isObject, isString } from '@robonen/stdlib';
|
||||
import { isFunction, isNumber, isObject, isString, isSymbol } from '@robonen/stdlib';
|
||||
|
||||
/**
|
||||
* Comparator deciding whether two array elements are considered equal.
|
||||
@@ -24,7 +24,7 @@ export interface UseArrayDifferenceOptions<T> {
|
||||
comparator?: UseArrayDifferenceComparatorFn<T> | keyof T;
|
||||
}
|
||||
|
||||
export type UseArrayDifferenceReturn<T = any>
|
||||
export type UseArrayDifferenceReturn<T = unknown>
|
||||
= ComputedRef<T[]>;
|
||||
|
||||
function isArrayDifferenceOptions<T>(value: unknown): value is UseArrayDifferenceOptions<T> {
|
||||
@@ -101,11 +101,11 @@ export function useArrayDifference<T>(
|
||||
// Resolve the comparator once instead of rebuilding it on every recompute.
|
||||
let compare: UseArrayDifferenceComparatorFn<T>;
|
||||
|
||||
if (isString(resolved) || typeof resolved === 'symbol' || typeof resolved === 'number') {
|
||||
if (isString(resolved) || isSymbol(resolved) || isNumber(resolved)) {
|
||||
const key = resolved as keyof T;
|
||||
compare = (value, othVal) => value[key] === othVal[key];
|
||||
}
|
||||
else if (typeof resolved === 'function') {
|
||||
else if (isFunction(resolved)) {
|
||||
compare = resolved;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -30,22 +30,22 @@ function reset() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
class="rounded-xl border p-4 transition"
|
||||
:class="allDone
|
||||
? 'border-emerald-500/30 bg-emerald-500/10'
|
||||
: 'border-(--border) bg-(--bg-elevated)'"
|
||||
: 'border-border bg-bg-elevated'"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Release readiness
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="allDone
|
||||
? 'border-emerald-500/30 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
<span
|
||||
class="size-2 rounded-full"
|
||||
@@ -54,7 +54,7 @@ function reset() {
|
||||
{{ allDone ? 'Ready to ship' : 'Blocked' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 font-mono text-sm tabular-nums text-(--fg-muted)">
|
||||
<div class="mt-2 font-mono text-sm tabular-nums text-fg-muted">
|
||||
{{ completed }} / {{ checklist.length }} complete
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,18 +62,18 @@ function reset() {
|
||||
<ul class="flex flex-col gap-2">
|
||||
<li v-for="item in checklist" :key="item.id">
|
||||
<button
|
||||
class="flex w-full items-center gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2 text-left text-sm text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.99] cursor-pointer"
|
||||
class="flex w-full items-center gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2 text-left text-sm text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.99] cursor-pointer"
|
||||
@click="toggle(item)"
|
||||
>
|
||||
<span
|
||||
class="flex size-5 shrink-0 items-center justify-center rounded-md border text-xs transition"
|
||||
:class="item.done
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border-strong) text-transparent'"
|
||||
? 'border-transparent bg-accent text-accent-fg'
|
||||
: 'border-border-strong text-transparent'"
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
<span :class="item.done ? 'line-through text-(--fg-subtle)' : ''">
|
||||
<span :class="item.done ? 'line-through text-fg-subtle' : ''">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</button>
|
||||
@@ -81,7 +81,7 @@ function reset() {
|
||||
</ul>
|
||||
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 self-start rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn self-start"
|
||||
@click="reset"
|
||||
>
|
||||
Reset
|
||||
|
||||
@@ -35,18 +35,18 @@ const formatted = computed(() =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<input
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="Search products…"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
|
||||
<div class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<label class="flex items-center justify-between text-sm text-(--fg-muted)">
|
||||
<div class="demo-card flex flex-col gap-3 p-4">
|
||||
<label class="flex items-center justify-between text-sm text-fg-muted">
|
||||
<span>Max price</span>
|
||||
<span class="font-mono text-(--fg) tabular-nums">${{ maxPrice }}</span>
|
||||
<span class="font-mono text-fg tabular-nums">${{ maxPrice }}</span>
|
||||
</label>
|
||||
<input
|
||||
v-model.number="maxPrice"
|
||||
@@ -54,21 +54,21 @@ const formatted = computed(() =>
|
||||
min="25"
|
||||
max="400"
|
||||
step="5"
|
||||
class="h-1.5 w-full cursor-pointer accent-(--accent)"
|
||||
class="h-1.5 w-full cursor-pointer accent-accent"
|
||||
>
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-(--fg-muted)">
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-fg-muted">
|
||||
<input
|
||||
v-model="inStockOnly"
|
||||
type="checkbox"
|
||||
class="size-4 cursor-pointer accent-(--accent)"
|
||||
class="size-4 cursor-pointer accent-accent"
|
||||
>
|
||||
In stock only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="font-medium uppercase tracking-wide text-(--fg-subtle)">Results</span>
|
||||
<span class="font-mono tabular-nums text-(--fg-muted)">
|
||||
<span class="font-medium uppercase tracking-wide text-fg-subtle">Results</span>
|
||||
<span class="font-mono tabular-nums text-fg-muted">
|
||||
{{ formatted.length }} / {{ products.length }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -77,10 +77,10 @@ const formatted = computed(() =>
|
||||
<li
|
||||
v-for="product in formatted"
|
||||
:key="product.name"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-(--fg)">{{ product.name }}</span>
|
||||
<span class="text-sm font-medium text-fg">{{ product.name }}</span>
|
||||
<span
|
||||
v-if="!product.inStock"
|
||||
class="inline-flex items-center rounded-md border border-amber-500/30 bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium uppercase text-amber-600 dark:text-amber-400"
|
||||
@@ -88,12 +88,12 @@ const formatted = computed(() =>
|
||||
Out
|
||||
</span>
|
||||
</div>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg-muted)">{{ product.priceLabel }}</span>
|
||||
<span class="font-mono text-sm tabular-nums text-fg-muted">{{ product.priceLabel }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-else
|
||||
class="rounded-lg border border-dashed border-(--border) bg-(--bg-inset) px-3 py-6 text-center text-sm text-(--fg-subtle)"
|
||||
class="rounded-lg border border-dashed border-border bg-bg-inset px-3 py-6 text-center text-sm text-fg-subtle"
|
||||
>
|
||||
No products match your filters.
|
||||
</div>
|
||||
|
||||
@@ -35,13 +35,13 @@ const matchIndex = computed(() =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="maxPrice">
|
||||
<label class="demo-label" for="maxPrice">
|
||||
Max price
|
||||
</label>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg)">${{ maxPrice }}</span>
|
||||
<span class="font-mono text-sm tabular-nums text-fg">${{ maxPrice }}</span>
|
||||
</div>
|
||||
<input
|
||||
id="maxPrice"
|
||||
@@ -50,28 +50,28 @@ const matchIndex = computed(() =>
|
||||
min="20"
|
||||
max="400"
|
||||
step="5"
|
||||
class="w-full accent-(--accent)"
|
||||
class="w-full accent-accent"
|
||||
>
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm text-(--fg-muted)">
|
||||
<input v-model="inStockOnly" type="checkbox" class="accent-(--accent)">
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm text-fg-muted">
|
||||
<input v-model="inStockOnly" type="checkbox" class="accent-accent">
|
||||
In stock only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<p class="mb-1 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<p class="demo-label mb-1">
|
||||
First match
|
||||
</p>
|
||||
<template v-if="firstMatch">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<span class="text-sm font-medium text-(--fg)">{{ firstMatch.name }}</span>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg)">${{ firstMatch.price }}</span>
|
||||
<span class="text-sm font-medium text-fg">{{ firstMatch.name }}</span>
|
||||
<span class="font-mono text-sm tabular-nums text-fg">${{ firstMatch.price }}</span>
|
||||
</div>
|
||||
<p class="mt-1 font-mono text-xs text-(--fg-subtle)">
|
||||
<p class="mt-1 font-mono text-xs text-fg-subtle">
|
||||
index {{ matchIndex }} · id {{ firstMatch.id }}
|
||||
</p>
|
||||
</template>
|
||||
<p v-else class="text-sm text-(--fg-subtle)">
|
||||
<p v-else class="text-sm text-fg-subtle">
|
||||
No product matches the filters
|
||||
</p>
|
||||
</div>
|
||||
@@ -82,8 +82,8 @@ const matchIndex = computed(() =>
|
||||
:key="product.id"
|
||||
class="flex items-center justify-between rounded-lg border px-3 py-2 text-sm transition"
|
||||
:class="product.id === firstMatch?.id
|
||||
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg-muted)'"
|
||||
? 'border-accent bg-accent-subtle text-accent-text'
|
||||
: 'border-border bg-bg-elevated text-fg-muted'"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
{{ product.name }}
|
||||
|
||||
@@ -24,15 +24,15 @@ function toggle(index: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<p class="demo-label">
|
||||
Next pending index
|
||||
</p>
|
||||
<p class="mt-1 font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
<p class="demo-stat mt-1 text-3xl">
|
||||
{{ nextIndex }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-(--fg-subtle)">
|
||||
<p class="mt-1 text-sm text-fg-subtle">
|
||||
{{ nextIndex === -1 ? 'All steps complete' : `“${steps[nextIndex]!.label}”` }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -43,16 +43,16 @@ function toggle(index: number) {
|
||||
:key="step.label"
|
||||
class="flex items-center justify-between rounded-lg border px-3 py-2 text-sm transition"
|
||||
:class="index === nextIndex
|
||||
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg-muted)'"
|
||||
? 'border-accent bg-accent-subtle text-accent-text'
|
||||
: 'border-border bg-bg-elevated text-fg-muted'"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-subtle)">{{ index }}</span>
|
||||
<span class="font-mono text-xs tabular-nums text-fg-subtle">{{ index }}</span>
|
||||
<span :class="step.done ? 'line-through opacity-60' : ''">{{ step.label }}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="toggle(index)"
|
||||
>
|
||||
{{ step.done ? 'Undo' : 'Done' }}
|
||||
|
||||
@@ -42,7 +42,7 @@ const tone: Record<LogEntry['level'], string> = {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex gap-1.5">
|
||||
<button
|
||||
v-for="level in levels"
|
||||
@@ -50,38 +50,38 @@ const tone: Record<LogEntry['level'], string> = {
|
||||
type="button"
|
||||
class="flex-1 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="filter === level
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset)'"
|
||||
? 'border-transparent bg-accent text-accent-fg'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset'"
|
||||
@click="filter = level"
|
||||
>
|
||||
{{ level }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<p class="demo-label">
|
||||
Latest “{{ filter }}” entry
|
||||
</p>
|
||||
<template v-if="latest">
|
||||
<p class="mt-1 font-mono text-sm text-(--fg)">{{ latest.message }}</p>
|
||||
<p class="mt-1 font-mono text-xs text-(--fg-subtle)">#{{ latest.id }}</p>
|
||||
<p class="mt-1 font-mono text-sm text-fg">{{ latest.message }}</p>
|
||||
<p class="mt-1 font-mono text-xs text-fg-subtle">#{{ latest.id }}</p>
|
||||
</template>
|
||||
<p v-else class="mt-1 text-sm text-(--fg-subtle)">
|
||||
<p v-else class="mt-1 text-sm text-fg-subtle">
|
||||
No “{{ filter }}” entries yet
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul class="flex max-h-44 flex-col gap-1 overflow-y-auto rounded-lg border border-(--border) bg-(--bg-elevated) p-2">
|
||||
<ul class="flex max-h-44 flex-col gap-1 overflow-y-auto rounded-lg border border-border bg-bg-elevated p-2">
|
||||
<li
|
||||
v-for="entry in log"
|
||||
:key="entry.id"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-1 font-mono text-xs transition"
|
||||
:class="entry.id === latest?.id ? 'bg-(--accent-subtle)' : ''"
|
||||
:class="entry.id === latest?.id ? 'bg-accent-subtle' : ''"
|
||||
>
|
||||
<span class="w-10 shrink-0 font-semibold uppercase" :class="tone[entry.level]">
|
||||
{{ entry.level }}
|
||||
</span>
|
||||
<span class="truncate text-(--fg-muted)">{{ entry.message }}</span>
|
||||
<span class="truncate text-fg-muted">{{ entry.message }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -90,7 +90,7 @@ const tone: Record<LogEntry['level'], string> = {
|
||||
v-for="level in levels"
|
||||
:key="level"
|
||||
type="button"
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn flex-1"
|
||||
@click="append(level)"
|
||||
>
|
||||
+ {{ level }}
|
||||
|
||||
@@ -26,9 +26,9 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4">
|
||||
<p class="demo-label mb-2">
|
||||
Member by key
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@@ -37,11 +37,11 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
|
||||
:key="user.id"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
|
||||
:class="user.id === searchId
|
||||
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
? 'border-accent bg-accent-subtle text-accent-text'
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
{{ user.name }}
|
||||
<span class="font-mono text-(--fg-subtle)">#{{ user.id }}</span>
|
||||
<span class="font-mono text-fg-subtle">#{{ user.id }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -49,21 +49,21 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
|
||||
<input
|
||||
v-model.number="searchId"
|
||||
type="number"
|
||||
class="w-24 rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="w-24 rounded-lg border border-border bg-bg px-3 py-2 text-sm text-fg transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-sm font-medium"
|
||||
:class="isMember
|
||||
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-(--bg-inset) text-(--fg-subtle)'"
|
||||
: 'bg-bg-inset text-fg-subtle'"
|
||||
>
|
||||
{{ isMember ? 'includes id' : 'not found' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-card p-4">
|
||||
<p class="demo-label mb-2">
|
||||
Primitive search (fromIndex 2)
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@@ -72,8 +72,8 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
|
||||
:key="i"
|
||||
class="inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="i < 2
|
||||
? 'border-(--border) bg-(--bg-inset) text-(--fg-subtle) opacity-50 line-through'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
? 'border-border bg-bg-inset text-fg-subtle opacity-50 line-through'
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
@@ -83,13 +83,13 @@ const hasTag = useArrayIncludes(tags, query, { fromIndex: fromIndex.value });
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="search a tag…"
|
||||
class="mt-3 w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input mt-3"
|
||||
>
|
||||
<p class="mt-2 text-sm text-(--fg-muted)">
|
||||
<p class="mt-2 text-sm text-fg-muted">
|
||||
Searching from index 2 →
|
||||
<span
|
||||
class="font-mono font-semibold"
|
||||
:class="hasTag ? 'text-emerald-600 dark:text-emerald-400' : 'text-(--fg-subtle)'"
|
||||
:class="hasTag ? 'text-emerald-600 dark:text-emerald-400' : 'text-fg-subtle'"
|
||||
>{{ hasTag }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed, toValue } from 'vue';
|
||||
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
|
||||
import { isObject, isString } from '@robonen/stdlib';
|
||||
import { isFunction, isNumber, isObject, isString, isSymbol } from '@robonen/stdlib';
|
||||
|
||||
/**
|
||||
* Comparator deciding whether an array element equals the searched value.
|
||||
@@ -83,11 +83,11 @@ export function useArrayIncludes<T, V = T>(
|
||||
// Resolve the comparator once instead of on every recompute.
|
||||
let compare: UseArrayIncludesComparatorFn<T, V>;
|
||||
|
||||
if (isString(resolved) || typeof resolved === 'symbol' || typeof resolved === 'number') {
|
||||
if (isString(resolved) || isSymbol(resolved) || isNumber(resolved)) {
|
||||
const key = resolved as keyof T;
|
||||
compare = (element, searched) => element[key] === (searched as unknown);
|
||||
}
|
||||
else if (typeof resolved === 'function') {
|
||||
else if (isFunction(resolved)) {
|
||||
compare = resolved;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -30,19 +30,19 @@ function remove(index: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<p class="mb-1 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<p class="demo-label mb-1">
|
||||
Joined result
|
||||
</p>
|
||||
<p class="break-all font-mono text-sm text-(--fg) tabular-nums">
|
||||
<p class="break-all font-mono text-sm text-fg tabular-nums">
|
||||
<span v-if="joined">{{ joined }}</span>
|
||||
<span v-else class="text-(--fg-subtle)">empty</span>
|
||||
<span v-else class="text-fg-subtle">empty</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Separator</span>
|
||||
<span class="demo-label">Separator</span>
|
||||
<div class="flex gap-1.5">
|
||||
<button
|
||||
v-for="sep in separators"
|
||||
@@ -50,8 +50,8 @@ function remove(index: number) {
|
||||
type="button"
|
||||
class="flex-1 rounded-lg border px-2 py-1.5 text-xs font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="separator === sep.value
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset)'"
|
||||
? 'border-transparent bg-accent text-accent-fg'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset'"
|
||||
@click="separator = sep.value"
|
||||
>
|
||||
{{ sep.label }}
|
||||
@@ -63,16 +63,16 @@ function remove(index: number) {
|
||||
<li
|
||||
v-for="(segment, index) in segments"
|
||||
:key="index"
|
||||
class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm text-(--fg)"
|
||||
class="flex items-center justify-between rounded-lg border border-border bg-bg-elevated px-3 py-1.5 text-sm text-fg"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="font-mono text-xs text-(--fg-subtle)">{{ index }}</span>
|
||||
<span class="font-mono text-xs text-fg-subtle">{{ index }}</span>
|
||||
{{ segment }}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove segment"
|
||||
class="rounded-md px-2 py-0.5 text-xs font-medium text-(--fg-subtle) transition hover:bg-(--bg-inset) hover:text-(--fg) cursor-pointer"
|
||||
class="rounded-md px-2 py-0.5 text-xs font-medium text-fg-subtle transition hover:bg-bg-inset hover:text-fg cursor-pointer"
|
||||
@click="remove(index)"
|
||||
>
|
||||
✕
|
||||
@@ -85,11 +85,11 @@ function remove(index: number) {
|
||||
v-model="draft"
|
||||
type="text"
|
||||
placeholder="add a segment…"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
|
||||
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:disabled="!draft.trim()"
|
||||
>
|
||||
Add
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed, toValue } from 'vue';
|
||||
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
|
||||
import { isFunction } from '@robonen/stdlib';
|
||||
|
||||
export type UseArrayJoinReturn = ComputedRef<string>;
|
||||
|
||||
@@ -30,7 +31,7 @@ export function useArrayJoin(
|
||||
// reactive items first lets the computed track per-item ref dependencies.
|
||||
let needsUnwrap = false;
|
||||
for (const item of resolved) {
|
||||
if (typeof item === 'function' || (typeof item === 'object' && item !== null && 'value' in item)) {
|
||||
if (isFunction(item) || (typeof item === 'object' && item !== null && 'value' in item)) {
|
||||
needsUnwrap = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -34,17 +34,17 @@ function bump(index: number, delta: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Cart</span>
|
||||
<label class="flex items-center gap-2 text-sm text-(--fg-muted)">
|
||||
<span class="demo-label">Cart</span>
|
||||
<label class="flex items-center gap-2 text-sm text-fg-muted">
|
||||
Tax {{ taxRate }}%
|
||||
<input
|
||||
v-model.number="taxRate"
|
||||
type="range"
|
||||
min="0"
|
||||
max="25"
|
||||
class="accent-(--accent)"
|
||||
class="accent-accent"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
@@ -53,41 +53,41 @@ function bump(index: number, delta: number) {
|
||||
<li
|
||||
v-for="(item, index) in priced"
|
||||
:key="item.name"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-medium text-(--fg)">
|
||||
<p class="truncate text-sm font-medium text-fg">
|
||||
{{ item.name }}
|
||||
</p>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
base {{ formatter.format(item.price) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
class="inline-flex size-7 items-center justify-center rounded-md border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex size-7 items-center justify-center rounded-md border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset active:scale-[0.98] cursor-pointer"
|
||||
aria-label="Decrease price"
|
||||
@click="bump(index, -10)"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex size-7 items-center justify-center rounded-md border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex size-7 items-center justify-center rounded-md border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset active:scale-[0.98] cursor-pointer"
|
||||
aria-label="Increase price"
|
||||
@click="bump(index, 10)"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<span class="w-20 text-right font-mono text-sm tabular-nums text-(--fg)">
|
||||
<span class="w-20 text-right font-mono text-sm tabular-nums text-fg">
|
||||
{{ formatter.format(item.gross) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Total with tax</span>
|
||||
<span class="font-mono text-xl font-bold tabular-nums text-(--fg)">
|
||||
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3">
|
||||
<span class="demo-label">Total with tax</span>
|
||||
<span class="demo-stat text-xl">
|
||||
{{ formatter.format(total) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -36,14 +36,14 @@ function removeAt(index: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<label class="flex items-center justify-between gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Starting budget</span>
|
||||
<div class="demo-stack max-w-md">
|
||||
<label class="demo-card flex items-center justify-between gap-3 p-4">
|
||||
<span class="demo-label">Starting budget</span>
|
||||
<input
|
||||
v-model.number="startingBudget"
|
||||
type="number"
|
||||
step="50"
|
||||
class="w-28 rounded-lg border border-(--border) bg-(--bg) px-3 py-1.5 text-right font-mono text-sm tabular-nums text-(--fg) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="w-28 rounded-lg border border-border bg-bg px-3 py-1.5 text-right font-mono text-sm tabular-nums text-fg transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
</label>
|
||||
|
||||
@@ -51,34 +51,34 @@ function removeAt(index: number) {
|
||||
<li
|
||||
v-for="(expense, index) in expenses"
|
||||
:key="index"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
|
||||
>
|
||||
<span class="flex-1 truncate text-sm text-(--fg)">{{ expense.label }}</span>
|
||||
<span class="flex-1 truncate text-sm text-fg">{{ expense.label }}</span>
|
||||
<span class="font-mono text-sm tabular-nums text-rose-600 dark:text-rose-400">
|
||||
−{{ formatter.format(expense.amount) }}
|
||||
</span>
|
||||
<button
|
||||
class="inline-flex size-6 items-center justify-center rounded-md text-(--fg-subtle) transition hover:bg-(--bg-inset) hover:text-(--fg) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex size-6 items-center justify-center rounded-md text-fg-subtle transition hover:bg-bg-inset hover:text-fg active:scale-[0.98] cursor-pointer"
|
||||
aria-label="Remove expense"
|
||||
@click="removeAt(index)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="expenses.length === 0" class="rounded-lg border border-dashed border-(--border) px-3 py-4 text-center text-sm text-(--fg-subtle)">
|
||||
<li v-if="expenses.length === 0" class="rounded-lg border border-dashed border-border px-3 py-4 text-center text-sm text-fg-subtle">
|
||||
No expenses — full budget remains.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="add"
|
||||
>
|
||||
+ Add charge
|
||||
</button>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Remaining</span>
|
||||
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3">
|
||||
<span class="demo-label">Remaining</span>
|
||||
<span
|
||||
class="font-mono text-2xl font-bold tabular-nums"
|
||||
:class="remaining < 0 ? 'text-rose-600 dark:text-rose-400' : 'text-emerald-600 dark:text-emerald-400'"
|
||||
|
||||
@@ -27,7 +27,7 @@ function load(index: number, delta: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div
|
||||
class="flex items-center gap-3 rounded-xl border p-4 transition"
|
||||
:class="hasOverloaded
|
||||
@@ -43,7 +43,7 @@ function load(index: number, delta: number) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label class="flex items-center justify-between gap-3 text-sm text-(--fg-muted)">
|
||||
<label class="flex items-center justify-between gap-3 text-sm text-fg-muted">
|
||||
<span>Alert threshold</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<input
|
||||
@@ -51,9 +51,9 @@ function load(index: number, delta: number) {
|
||||
type="range"
|
||||
min="40"
|
||||
max="100"
|
||||
class="accent-(--accent)"
|
||||
class="accent-accent"
|
||||
>
|
||||
<span class="w-10 text-right font-mono tabular-nums text-(--fg)">{{ threshold }}%</span>
|
||||
<span class="w-10 text-right font-mono tabular-nums text-fg">{{ threshold }}%</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -61,31 +61,31 @@ function load(index: number, delta: number) {
|
||||
<li
|
||||
v-for="(server, index) in servers"
|
||||
:key="server.name"
|
||||
class="rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2.5"
|
||||
class="rounded-lg border border-border bg-bg-elevated px-3 py-2.5"
|
||||
>
|
||||
<div class="mb-1.5 flex items-center justify-between">
|
||||
<span class="font-mono text-sm text-(--fg)">{{ server.name }}</span>
|
||||
<span class="font-mono text-sm text-fg">{{ server.name }}</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span
|
||||
class="font-mono text-sm tabular-nums"
|
||||
:class="server.cpu > threshold ? 'text-amber-600 dark:text-amber-400' : 'text-(--fg-muted)'"
|
||||
:class="server.cpu > threshold ? 'text-amber-600 dark:text-amber-400' : 'text-fg-muted'"
|
||||
>{{ server.cpu }}%</span>
|
||||
<button
|
||||
class="inline-flex size-6 items-center justify-center rounded-md border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex size-6 items-center justify-center rounded-md border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset active:scale-[0.98] cursor-pointer"
|
||||
aria-label="Decrease load"
|
||||
@click="load(index, -10)"
|
||||
>−</button>
|
||||
<button
|
||||
class="inline-flex size-6 items-center justify-center rounded-md border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex size-6 items-center justify-center rounded-md border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset active:scale-[0.98] cursor-pointer"
|
||||
aria-label="Increase load"
|
||||
@click="load(index, 10)"
|
||||
>+</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-1.5 w-full overflow-hidden rounded-full bg-(--bg-inset)">
|
||||
<div class="h-1.5 w-full overflow-hidden rounded-full bg-bg-inset">
|
||||
<div
|
||||
class="h-full rounded-full transition-all"
|
||||
:class="server.cpu > threshold ? 'bg-amber-500' : 'bg-(--accent)'"
|
||||
:class="server.cpu > threshold ? 'bg-amber-500' : 'bg-accent'"
|
||||
:style="{ width: `${server.cpu}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -35,40 +35,40 @@ function addTag() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<form class="flex gap-2" @submit.prevent="addTag">
|
||||
<input
|
||||
v-model="draft"
|
||||
type="text"
|
||||
placeholder="Add a tag, e.g. TypeScript"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<label class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2.5 text-sm text-(--fg)">
|
||||
<label class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2.5 text-sm text-fg">
|
||||
<span>Case-insensitive comparator</span>
|
||||
<input
|
||||
v-model="caseInsensitive"
|
||||
type="checkbox"
|
||||
class="size-4 accent-(--accent) cursor-pointer"
|
||||
class="size-4 accent-accent cursor-pointer"
|
||||
>
|
||||
</label>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Source ({{ raw.length }})
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-1.5 rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<div class="flex flex-wrap gap-1.5 rounded-lg border border-border bg-bg-inset p-3">
|
||||
<span
|
||||
v-for="(tag, index) in raw"
|
||||
:key="`${tag}-${index}`"
|
||||
class="inline-flex items-center rounded-md border border-(--border) bg-(--bg-elevated) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
class="inline-flex items-center rounded-md border border-border bg-bg-elevated px-2 py-0.5 text-xs font-medium text-fg-muted"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
@@ -76,18 +76,18 @@ function addTag() {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Unique ({{ unique.length }})
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) p-3">
|
||||
<div class="flex flex-wrap gap-1.5 rounded-lg border border-border bg-bg-elevated p-3">
|
||||
<span
|
||||
v-for="tag in unique"
|
||||
:key="tag"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--accent) bg-(--accent-subtle) px-2 py-0.5 text-xs font-medium text-(--accent-text)"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-accent bg-accent-subtle px-2 py-0.5 text-xs font-medium text-accent-text"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
<span v-if="unique.length === 0" class="text-xs text-(--fg-subtle)">No tags yet.</span>
|
||||
<span v-if="unique.length === 0" class="text-xs text-fg-subtle">No tags yet.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed, toValue } from 'vue';
|
||||
import type { ComputedRef, MaybeRefOrGetter } from 'vue';
|
||||
import { isString, unique } from '@robonen/stdlib';
|
||||
import { isFunction, isNumber, isString, isSymbol, unique } from '@robonen/stdlib';
|
||||
|
||||
/**
|
||||
* Equality comparator deciding whether two array elements are duplicates.
|
||||
@@ -66,12 +66,12 @@ export function useArrayUnique<T>(
|
||||
// Resolve the comparison strategy once, not on every recompute.
|
||||
|
||||
// Key of T (string | number | symbol) -> O(n) first-seen-wins key de-dup.
|
||||
if (isString(comparator) || typeof comparator === 'symbol' || typeof comparator === 'number') {
|
||||
if (isString(comparator) || isSymbol(comparator) || isNumber(comparator)) {
|
||||
const key = comparator as keyof T;
|
||||
return computed<T[]>(() => uniqueByKey(resolve(list), element => element[key] as PropertyKey));
|
||||
}
|
||||
|
||||
if (typeof comparator === 'function') {
|
||||
if (isFunction(comparator)) {
|
||||
// A unary key extractor stays O(n); a binary comparator falls back to O(n²)
|
||||
// pairwise comparison (unavoidable for arbitrary equality). Branch on arity.
|
||||
if (comparator.length <= 1) {
|
||||
|
||||
@@ -38,23 +38,23 @@ const keys: { id: SortKey; label: string }[] = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="inline-flex rounded-lg border border-(--border) bg-(--bg-elevated) p-0.5">
|
||||
<div class="inline-flex rounded-lg border border-border bg-bg-elevated p-0.5">
|
||||
<button
|
||||
v-for="key in keys"
|
||||
:key="key.id"
|
||||
class="rounded-md px-3 py-1 text-sm font-medium transition cursor-pointer"
|
||||
:class="sortKey === key.id
|
||||
? 'bg-(--accent) text-(--accent-fg)'
|
||||
: 'text-(--fg-muted) hover:text-(--fg)'"
|
||||
? 'bg-accent text-accent-fg'
|
||||
: 'text-fg-muted hover:text-fg'"
|
||||
@click="sortKey = key.id"
|
||||
>
|
||||
{{ key.label }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="descending = !descending"
|
||||
>
|
||||
{{ descending ? 'Desc ↓' : 'Asc ↑' }}
|
||||
@@ -65,22 +65,22 @@ const keys: { id: SortKey; label: string }[] = [
|
||||
<li
|
||||
v-for="(player, index) in sorted"
|
||||
:key="player.name"
|
||||
class="flex items-center gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2.5"
|
||||
class="flex items-center gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2.5"
|
||||
>
|
||||
<span class="w-6 text-center font-mono text-sm tabular-nums text-(--fg-subtle)">
|
||||
<span class="w-6 text-center font-mono text-sm tabular-nums text-fg-subtle">
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<span class="flex-1 text-sm font-medium text-(--fg)">{{ player.name }}</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="flex-1 text-sm font-medium text-fg">{{ player.name }}</span>
|
||||
<span class="demo-badge">
|
||||
Lv {{ player.level }}
|
||||
</span>
|
||||
<span class="w-16 text-right font-mono text-sm font-semibold tabular-nums text-(--fg)">
|
||||
<span class="w-16 text-right font-mono text-sm font-semibold tabular-nums text-fg">
|
||||
{{ player.score.toLocaleString() }}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Stable sort — players with an equal {{ sortKey }} keep their original order. The source array is left untouched.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { computed, isRef, toValue, watchEffect } from 'vue';
|
||||
import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
|
||||
import { isFunction } from '@robonen/stdlib';
|
||||
|
||||
export type UseSortedCompareFn<T = any>
|
||||
export type UseSortedCompareFn<T = unknown>
|
||||
= (a: T, b: T) => number;
|
||||
|
||||
export type UseSortedFn<T = any>
|
||||
export type UseSortedFn<T = unknown>
|
||||
= (arr: T[], compareFn: UseSortedCompareFn<T>) => T[];
|
||||
|
||||
export interface UseSortedOptions<T = any> {
|
||||
export interface UseSortedOptions<T = unknown> {
|
||||
/**
|
||||
* The sort algorithm to apply. Receives a copy of the array (or the source
|
||||
* itself in `dirty` mode) and the resolved compare function.
|
||||
@@ -101,13 +101,13 @@ const defaultSortFn: UseSortedFn = <T>(source: T[], compareFn: UseSortedCompareF
|
||||
*
|
||||
* @since 0.0.15
|
||||
*/
|
||||
export function useSorted<T = any>(source: Ref<T[]>, compareFn?: UseSortedCompareFn<T>): Ref<T[]>;
|
||||
export function useSorted<T = any>(source: MaybeRefOrGetter<T[]>, compareFn?: UseSortedCompareFn<T>): ComputedRef<T[]>;
|
||||
export function useSorted<T = any>(source: Ref<T[]>, options?: UseSortedOptions<T>): Ref<T[]>;
|
||||
export function useSorted<T = any>(source: MaybeRefOrGetter<T[]>, options?: UseSortedOptions<T>): ComputedRef<T[]>;
|
||||
export function useSorted<T = any>(source: Ref<T[]>, compareFn?: UseSortedCompareFn<T>, options?: Omit<UseSortedOptions<T>, 'compareFn'>): Ref<T[]>;
|
||||
export function useSorted<T = any>(source: MaybeRefOrGetter<T[]>, compareFn?: UseSortedCompareFn<T>, options?: Omit<UseSortedOptions<T>, 'compareFn'>): ComputedRef<T[]>;
|
||||
export function useSorted<T = any>(
|
||||
export function useSorted<T = unknown>(source: Ref<T[]>, compareFn?: UseSortedCompareFn<T>): Ref<T[]>;
|
||||
export function useSorted<T = unknown>(source: MaybeRefOrGetter<T[]>, compareFn?: UseSortedCompareFn<T>): ComputedRef<T[]>;
|
||||
export function useSorted<T = unknown>(source: Ref<T[]>, options?: UseSortedOptions<T>): Ref<T[]>;
|
||||
export function useSorted<T = unknown>(source: MaybeRefOrGetter<T[]>, options?: UseSortedOptions<T>): ComputedRef<T[]>;
|
||||
export function useSorted<T = unknown>(source: Ref<T[]>, compareFn?: UseSortedCompareFn<T>, options?: Omit<UseSortedOptions<T>, 'compareFn'>): Ref<T[]>;
|
||||
export function useSorted<T = unknown>(source: MaybeRefOrGetter<T[]>, compareFn?: UseSortedCompareFn<T>, options?: Omit<UseSortedOptions<T>, 'compareFn'>): ComputedRef<T[]>;
|
||||
export function useSorted<T = unknown>(
|
||||
source: MaybeRefOrGetter<T[]>,
|
||||
compareFnOrOptions?: UseSortedCompareFn<T> | UseSortedOptions<T>,
|
||||
maybeOptions?: Omit<UseSortedOptions<T>, 'compareFn'>,
|
||||
|
||||
@@ -27,9 +27,9 @@ function setQuantity(delta: number): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Shared cart</span>
|
||||
<span class="demo-label">Shared cart</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="supported
|
||||
@@ -41,15 +41,15 @@ function setQuantity(delta: number): void {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card p-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="product in products"
|
||||
:key="product"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="cart.item === product
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="pick(product)"
|
||||
>
|
||||
{{ product }}
|
||||
@@ -57,18 +57,18 @@ function setQuantity(delta: number): void {
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<span class="text-sm text-(--fg-muted)">Quantity</span>
|
||||
<span class="text-sm text-fg-muted">Quantity</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:disabled="cart.quantity <= 1"
|
||||
@click="setQuantity(-1)"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span class="w-8 text-center font-mono text-lg font-bold tabular-nums text-(--fg)">{{ cart.quantity }}</span>
|
||||
<span class="demo-stat w-8 text-center text-lg">{{ cart.quantity }}</span>
|
||||
<button
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-(--border) bg-(--bg-elevated) text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-border bg-bg-elevated text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.98] cursor-pointer"
|
||||
@click="setQuantity(1)"
|
||||
>
|
||||
+
|
||||
@@ -77,21 +77,21 @@ function setQuantity(delta: number): void {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-(--fg-muted)">{{ cart.item }} × {{ cart.quantity }}</span>
|
||||
<span class="text-fg-muted">{{ cart.item }} × {{ cart.quantity }}</span>
|
||||
<span class="font-bold">${{ subtotal }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="theme = theme === 'light' ? 'dark' : 'light'"
|
||||
>
|
||||
Toggle shared theme: <span class="font-mono">{{ theme }}</span>
|
||||
</button>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Open this page in a second tab. Every change you make here is broadcast and mirrored instantly in the other tab.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -21,42 +21,42 @@ const device = (): string => (isDesktop.value ? 'Desktop' : isMobile.value ? 'Mo
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Active breakpoint</span>
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4">
|
||||
<span class="demo-label">Active breakpoint</span>
|
||||
<div class="mt-1 flex items-baseline gap-2">
|
||||
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ active || 'none' }}</span>
|
||||
<span class="rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">{{ device() }}</span>
|
||||
<span class="demo-stat text-3xl">{{ active || 'none' }}</span>
|
||||
<span class="rounded-md border border-border bg-bg-inset px-2 py-0.5 text-xs font-medium text-fg-muted">{{ device() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Tailwind breakpoints</span>
|
||||
<span class="demo-label">Tailwind breakpoints</span>
|
||||
<div
|
||||
v-for="row in rows"
|
||||
:key="row.key"
|
||||
class="flex items-center justify-between rounded-lg border px-3 py-2 text-sm transition"
|
||||
:class="bp[row.key].value
|
||||
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
? 'border-accent bg-accent-subtle text-accent-text'
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
<span class="font-mono font-medium">{{ row.key }}</span>
|
||||
<span class="font-mono tabular-nums text-(--fg-subtle)">≥ {{ row.width }}</span>
|
||||
<span class="font-mono tabular-nums text-fg-subtle">≥ {{ row.width }}</span>
|
||||
<span
|
||||
class="h-2 w-2 rounded-full transition"
|
||||
:class="bp[row.key].value ? 'bg-(--accent)' : 'bg-(--border-strong)'"
|
||||
:class="bp[row.key].value ? 'bg-accent' : 'bg-border-strong'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-(--fg-muted)">current()</span>
|
||||
<span class="text-fg-muted">current()</span>
|
||||
<span>[{{ current.length ? current.join(', ') : '—' }}]</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Resize your browser window — the matched breakpoints update live.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { defaultWindow } from '@/types';
|
||||
import type { ConfigurableWindow } from '@/types';
|
||||
import { useMediaQuery } from '@/composables/browser/useMediaQuery';
|
||||
import type { UseMediaQueryOptions } from '@/composables/browser/useMediaQuery';
|
||||
import { pxValue } from '@robonen/platform/browsers';
|
||||
|
||||
/**
|
||||
* A breakpoints map: name → viewport width. Numbers are treated as pixels;
|
||||
@@ -61,22 +62,6 @@ export type UseBreakpointsReturn<K extends string = string>
|
||||
active: () => ComputedRef<K | ''>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a CSS length token (`"1024px"`, `"48em"`, `"30rem"`, `"50%"`) into a
|
||||
* pixel number. `em`/`rem` use the conventional 16px root size.
|
||||
*/
|
||||
function pxValue(value: string): number {
|
||||
const number = Number.parseFloat(value);
|
||||
|
||||
if (Number.isNaN(number))
|
||||
return Number.NaN;
|
||||
|
||||
if (/(?:em|rem)\s*$/i.test(value))
|
||||
return number * 16;
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add `delta` to the numeric portion of a CSS length, preserving its unit.
|
||||
* Used to build the strict (`> / <`) variants from inclusive media queries via
|
||||
|
||||
@@ -14,9 +14,9 @@ const snippets = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Clipboard API</span>
|
||||
<span class="demo-label">Clipboard API</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="isSupported
|
||||
@@ -32,11 +32,11 @@ const snippets = [
|
||||
<input
|
||||
v-model="draft"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
placeholder="Type something to copy…"
|
||||
>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="copyPending || !draft"
|
||||
@click="copy(draft)"
|
||||
>
|
||||
@@ -45,21 +45,21 @@ const snippets = [
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Quick copy</span>
|
||||
<span class="demo-label">Quick copy</span>
|
||||
<button
|
||||
v-for="snippet in snippets"
|
||||
:key="snippet"
|
||||
class="inline-flex items-center justify-between gap-2 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2 text-left text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.99] cursor-pointer"
|
||||
class="inline-flex items-center justify-between gap-2 rounded-lg border border-border bg-bg-elevated px-3 py-2 text-left text-sm font-medium text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.99] cursor-pointer"
|
||||
@click="copy(snippet)"
|
||||
>
|
||||
<span class="truncate font-mono text-xs text-(--fg-muted)">{{ snippet }}</span>
|
||||
<span class="shrink-0 text-xs text-(--fg-subtle)">Copy</span>
|
||||
<span class="truncate font-mono text-xs text-fg-muted">{{ snippet }}</span>
|
||||
<span class="shrink-0 text-xs text-fg-subtle">Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Last copied</span>
|
||||
<p class="mt-1 break-all font-mono text-sm text-(--fg)">{{ text || '—' }}</p>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<span class="demo-label">Last copied</span>
|
||||
<p class="mt-1 break-all font-mono text-sm text-fg">{{ text || '—' }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -34,9 +34,9 @@ function typesOf(item: ClipboardItem): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">ClipboardItem API</span>
|
||||
<span class="demo-label">ClipboardItem API</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="isSupported
|
||||
@@ -48,42 +48,42 @@ function typesOf(item: ClipboardItem): string {
|
||||
</div>
|
||||
|
||||
<template v-if="isSupported">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Rich payload</span>
|
||||
<p class="mt-1 text-sm text-(--fg)" v-html="html" />
|
||||
<p class="mt-1 font-mono text-xs text-(--fg-subtle)">text/plain · text/html</p>
|
||||
<div class="demo-card p-4">
|
||||
<span class="demo-label">Rich payload</span>
|
||||
<p class="mt-1 text-sm text-fg" v-html="html" />
|
||||
<p class="mt-1 font-mono text-xs text-fg-subtle">text/plain · text/html</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="copyPending"
|
||||
@click="copyRich"
|
||||
>
|
||||
{{ copyPending ? 'Copying…' : copied ? 'Copied!' : 'Copy rich content' }}
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="readClipboard"
|
||||
>
|
||||
Read clipboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<span class="demo-label">
|
||||
content ({{ content.length }} {{ content.length === 1 ? 'item' : 'items' }})
|
||||
</span>
|
||||
<ul v-if="content.length" class="mt-2 flex flex-col gap-1">
|
||||
<li
|
||||
v-for="(item, i) in content"
|
||||
:key="i"
|
||||
class="font-mono text-xs text-(--fg)"
|
||||
class="font-mono text-xs text-fg"
|
||||
>
|
||||
#{{ i + 1 }}: {{ typesOf(item) }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="mt-2 font-mono text-xs text-(--fg-subtle)">No items read yet</p>
|
||||
<p v-else class="mt-2 font-mono text-xs text-fg-subtle">No items read yet</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -20,9 +20,9 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">CloseWatcher API</span>
|
||||
<span class="demo-label">CloseWatcher API</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="isSupported
|
||||
@@ -34,7 +34,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
|
||||
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:disabled="open"
|
||||
@click="open = true"
|
||||
>
|
||||
@@ -47,19 +47,19 @@ onMounted(() => {
|
||||
leave-active-class="transition duration-100 ease-in"
|
||||
leave-to-class="opacity-0 translate-y-1"
|
||||
>
|
||||
<div v-if="open" class="rounded-xl border border-(--border-strong) bg-(--bg-elevated) p-4 shadow-lg">
|
||||
<div v-if="open" class="rounded-xl border border-border-strong bg-bg-elevated p-4 shadow-lg">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-(--fg)">Unsaved changes</p>
|
||||
<p class="mt-1 text-sm text-(--fg-muted)">
|
||||
Press <kbd class="rounded border border-(--border) bg-(--bg-inset) px-1.5 py-0.5 font-mono text-xs text-(--fg)">Esc</kbd>
|
||||
<p class="text-sm font-semibold text-fg">Unsaved changes</p>
|
||||
<p class="mt-1 text-sm text-fg-muted">
|
||||
Press <kbd class="rounded border border-border bg-bg-inset px-1.5 py-0.5 font-mono text-xs text-fg">Esc</kbd>
|
||||
(or the Android back gesture) to dismiss.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="close()"
|
||||
>
|
||||
Dismiss via close()
|
||||
@@ -68,18 +68,18 @@ onMounted(() => {
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-(--fg-muted)">closes</span>
|
||||
<span class="text-fg-muted">closes</span>
|
||||
<span class="font-bold">{{ closeCount }}</span>
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between">
|
||||
<span class="text-(--fg-muted)">last</span>
|
||||
<span class="text-fg-muted">last</span>
|
||||
<span>{{ lastClosedAt ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Open the dialog, then dismiss it with Esc, the system back gesture, or the programmatic <code class="font-mono">close()</code> call.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -30,11 +30,11 @@ const options = [
|
||||
<template>
|
||||
<div
|
||||
ref="target"
|
||||
class="flex w-full max-w-sm flex-col gap-4"
|
||||
class="demo-stack max-w-sm"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Color mode</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-label">Color mode</span>
|
||||
<span class="demo-badge">
|
||||
system: {{ mode.system.value }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -46,8 +46,8 @@ const options = [
|
||||
type="button"
|
||||
class="inline-flex flex-col items-center justify-center gap-1 rounded-lg border px-2 py-3 text-xs font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="mode === opt.value
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="mode = opt.value"
|
||||
>
|
||||
<span class="text-base leading-none">{{ opt.icon }}</span>
|
||||
@@ -55,26 +55,26 @@ const options = [
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<p class="mb-3 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Reactive state</p>
|
||||
<div class="demo-card p-4">
|
||||
<p class="demo-label mb-3">Reactive state</p>
|
||||
<dl class="space-y-2 text-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<dt class="text-(--fg-muted)">selected (emitAuto)</dt>
|
||||
<dd class="font-mono tabular-nums text-(--fg)">{{ mode }}</dd>
|
||||
<dt class="text-fg-muted">selected (emitAuto)</dt>
|
||||
<dd class="font-mono tabular-nums text-fg">{{ mode }}</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<dt class="text-(--fg-muted)">resolved state</dt>
|
||||
<dd class="font-mono tabular-nums text-(--fg)">{{ mode.state.value }}</dd>
|
||||
<dt class="text-fg-muted">resolved state</dt>
|
||||
<dd class="font-mono tabular-nums text-fg">{{ mode.state.value }}</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<dt class="text-(--fg-muted)">store</dt>
|
||||
<dd class="font-mono tabular-nums text-(--fg)">{{ mode.store.value }}</dd>
|
||||
<dt class="text-fg-muted">store</dt>
|
||||
<dd class="font-mono tabular-nums text-fg">{{ mode.store.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
The chosen mode is applied as <code class="font-mono text-(--fg-muted)">data-demo-theme</code> on this card.
|
||||
<p class="text-xs text-fg-subtle">
|
||||
The chosen mode is applied as <code class="font-mono text-fg-muted">data-demo-theme</code> on this card.
|
||||
Pick "Auto" to follow your OS preference.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -15,10 +15,10 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
ref="target"
|
||||
class="flex items-center justify-center rounded-xl border border-(--border) bg-(--bg-inset) p-6"
|
||||
class="flex items-center justify-center rounded-xl border border-border bg-bg-inset p-6"
|
||||
>
|
||||
<div
|
||||
class="shadow-lg transition-all duration-300 ease-out"
|
||||
@@ -31,11 +31,11 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card flex flex-col gap-4 p-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="hue">Hue</label>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg)">--demo-hue: {{ hue }}</span>
|
||||
<label class="demo-label" for="hue">Hue</label>
|
||||
<span class="font-mono text-sm tabular-nums text-fg">--demo-hue: {{ hue }}</span>
|
||||
</div>
|
||||
<input
|
||||
id="hue"
|
||||
@@ -43,14 +43,14 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
class="w-full accent-accent cursor-pointer"
|
||||
>
|
||||
<div class="flex gap-1.5">
|
||||
<button
|
||||
v-for="s in swatches"
|
||||
:key="s"
|
||||
type="button"
|
||||
class="h-6 w-6 rounded-md border border-(--border) transition hover:scale-110 active:scale-95 cursor-pointer"
|
||||
class="h-6 w-6 rounded-md border border-border transition hover:scale-110 active:scale-95 cursor-pointer"
|
||||
:style="{ background: `hsl(${s} 80% 55%)` }"
|
||||
:aria-label="`Set hue ${s}`"
|
||||
@click="hue = s"
|
||||
@@ -60,8 +60,8 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="radius">Radius</label>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg)">--demo-radius: {{ radius }}</span>
|
||||
<label class="demo-label" for="radius">Radius</label>
|
||||
<span class="font-mono text-sm tabular-nums text-fg">--demo-radius: {{ radius }}</span>
|
||||
</div>
|
||||
<input
|
||||
id="radius"
|
||||
@@ -69,14 +69,14 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
|
||||
type="range"
|
||||
min="0"
|
||||
max="48"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
class="w-full accent-accent cursor-pointer"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="size">Size</label>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg)">--demo-size: {{ size }}</span>
|
||||
<label class="demo-label" for="size">Size</label>
|
||||
<span class="font-mono text-sm tabular-nums text-fg">--demo-size: {{ size }}</span>
|
||||
</div>
|
||||
<input
|
||||
id="size"
|
||||
@@ -84,12 +84,12 @@ const accent = computed(() => `hsl(${hue.value} 80% 55%)`);
|
||||
type="range"
|
||||
min="48"
|
||||
max="140"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
class="w-full accent-accent cursor-pointer"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
background: {{ accent }};
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,9 +23,9 @@ function toggle() {
|
||||
<div
|
||||
ref="target"
|
||||
data-demo-mode
|
||||
class="flex w-full max-w-sm flex-col gap-4"
|
||||
class="demo-stack max-w-sm"
|
||||
>
|
||||
<div class="flex items-center justify-between rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card flex items-center justify-between p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg text-lg transition-colors"
|
||||
@@ -36,8 +36,8 @@ function toggle() {
|
||||
{{ isDark ? '☾' : '☀' }}
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-(--fg)">{{ isDark ? 'Dark mode' : 'Light mode' }}</p>
|
||||
<p class="text-xs text-(--fg-subtle)">isDark = {{ isDark }}</p>
|
||||
<p class="text-sm font-medium text-fg">{{ isDark ? 'Dark mode' : 'Light mode' }}</p>
|
||||
<p class="text-xs text-fg-subtle">isDark = {{ isDark }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,30 +45,30 @@ function toggle() {
|
||||
type="button"
|
||||
role="switch"
|
||||
:aria-checked="isDark"
|
||||
class="relative inline-flex h-7 w-12 items-center rounded-full border border-(--border) transition focus:outline-none focus:ring-2 focus:ring-(--ring) cursor-pointer"
|
||||
:class="isDark ? 'bg-(--accent)' : 'bg-(--bg-inset)'"
|
||||
class="relative inline-flex h-7 w-12 items-center rounded-full border border-border transition focus:outline-none focus:ring-2 focus:ring-ring cursor-pointer"
|
||||
:class="isDark ? 'bg-accent' : 'bg-bg-inset'"
|
||||
@click="toggle"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-5 w-5 transform rounded-full bg-(--bg) shadow transition-transform"
|
||||
class="inline-block h-5 w-5 transform rounded-full bg-bg shadow transition-transform"
|
||||
:class="isDark ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<p class="mb-2 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Preview surface</p>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<p class="demo-label mb-2">Preview surface</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg px-2 py-0.5 text-xs font-medium text-fg-muted">
|
||||
data-demo-mode = "{{ isDark ? 'dark' : 'light' }}"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Writing the boolean toggles the attribute on this card. When the requested state
|
||||
matches your OS preference, <code class="font-mono text-(--fg-muted)">useDark</code>
|
||||
falls back to <code class="font-mono text-(--fg-muted)">auto</code> to keep tracking it.
|
||||
matches your OS preference, <code class="font-mono text-fg-muted">useDark</code>
|
||||
falls back to <code class="font-mono text-fg-muted">auto</code> to keep tracking it.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -51,7 +51,7 @@ watch(isOpen, (openNow) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-400"
|
||||
@@ -62,14 +62,14 @@ watch(isOpen, (openNow) => {
|
||||
<template v-else>
|
||||
<div
|
||||
ref="host"
|
||||
class="min-h-[7rem] rounded-xl border border-(--border) bg-(--bg-inset) p-1"
|
||||
class="min-h-[7rem] rounded-xl border border-border bg-bg-inset p-1"
|
||||
>
|
||||
<div
|
||||
ref="player"
|
||||
class="flex h-full flex-col items-center justify-center gap-1 rounded-lg bg-(--bg-elevated) p-6"
|
||||
class="flex h-full flex-col items-center justify-center gap-1 rounded-lg bg-bg-elevated p-6"
|
||||
>
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Live timer</span>
|
||||
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
<span class="demo-label">Live timer</span>
|
||||
<span class="demo-stat text-3xl">
|
||||
{{ String(Math.floor(elapsed / 60)).padStart(2, '0') }}:{{ String(elapsed % 60).padStart(2, '0') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@ watch(isOpen, (openNow) => {
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="isOpen"
|
||||
@click="popOut"
|
||||
>
|
||||
@@ -86,7 +86,7 @@ watch(isOpen, (openNow) => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!isOpen"
|
||||
@click="close"
|
||||
>
|
||||
@@ -94,17 +94,17 @@ watch(isOpen, (openNow) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-sm">
|
||||
<span class="text-(--fg-muted)">isOpen</span>
|
||||
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3 text-sm">
|
||||
<span class="text-fg-muted">isOpen</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="isOpen
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg) text-(--fg-muted)'"
|
||||
: 'border-border bg-bg text-fg-muted'"
|
||||
>
|
||||
<span
|
||||
class="h-1.5 w-1.5 rounded-full"
|
||||
:class="isOpen ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
|
||||
:class="isOpen ? 'bg-emerald-500' : 'bg-fg-subtle'"
|
||||
/>
|
||||
{{ isOpen ? 'floating' : 'docked' }}
|
||||
</span>
|
||||
@@ -118,7 +118,7 @@ watch(isOpen, (openNow) => {
|
||||
</p>
|
||||
<p
|
||||
v-else
|
||||
class="text-xs text-(--fg-subtle)"
|
||||
class="text-xs text-fg-subtle"
|
||||
>
|
||||
"Pop out" moves the live timer into an always-on-top window. Closing it returns the element to the page.
|
||||
</p>
|
||||
|
||||
@@ -46,33 +46,33 @@ function toggleListening() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">pointermove on element</span>
|
||||
<span class="demo-label">pointermove on element</span>
|
||||
<div
|
||||
ref="pad"
|
||||
class="relative h-32 overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset) touch-none"
|
||||
class="relative h-32 overflow-hidden rounded-xl border border-border bg-bg-inset touch-none"
|
||||
>
|
||||
<div
|
||||
class="pointer-events-none absolute h-5 w-5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-(--accent) transition-opacity"
|
||||
class="pointer-events-none absolute h-5 w-5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-accent transition-opacity"
|
||||
:class="inside ? 'opacity-100' : 'opacity-0'"
|
||||
:style="{ left: `${pos.x}px`, top: `${pos.y}px` }"
|
||||
/>
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<span class="text-xs text-(--fg-subtle)">{{ inside ? '' : 'Hover here' }}</span>
|
||||
<span class="text-xs text-fg-subtle">{{ inside ? '' : 'Hover here' }}</span>
|
||||
</div>
|
||||
<div class="pointer-events-none absolute bottom-2 left-2 rounded-md border border-(--border) bg-(--bg) px-2 py-0.5 font-mono text-xs tabular-nums text-(--fg-muted)">
|
||||
<div class="pointer-events-none absolute bottom-2 left-2 rounded-md border border-border bg-bg px-2 py-0.5 font-mono text-xs tabular-nums text-fg-muted">
|
||||
x: {{ pos.x }} · y: {{ pos.y }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card flex flex-col gap-2 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">keydown on window</span>
|
||||
<span class="demo-label">keydown on window</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-xs font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-border bg-bg-elevated px-3 py-1.5 text-xs font-medium text-fg transition hover:bg-bg-inset hover:border-border-strong active:scale-[0.98] cursor-pointer"
|
||||
@click="toggleListening"
|
||||
>
|
||||
{{ listening ? 'Stop listening' : 'Start listening' }}
|
||||
@@ -81,30 +81,30 @@ function toggleListening() {
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<kbd
|
||||
class="flex min-w-[3.5rem] items-center justify-center rounded-lg border border-(--border-strong) bg-(--bg-inset) px-3 py-2 font-mono text-sm font-medium text-(--fg)"
|
||||
class="flex min-w-[3.5rem] items-center justify-center rounded-lg border border-border-strong bg-bg-inset px-3 py-2 font-mono text-sm font-medium text-fg"
|
||||
>
|
||||
{{ lastKey || '—' }}
|
||||
</kbd>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs text-(--fg-subtle)">presses captured</span>
|
||||
<span class="font-mono text-lg font-bold tabular-nums text-(--fg)">{{ keyCount }}</span>
|
||||
<span class="text-xs text-fg-subtle">presses captured</span>
|
||||
<span class="demo-stat text-lg">{{ keyCount }}</span>
|
||||
</div>
|
||||
<span
|
||||
class="ml-auto inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="listening
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
<span
|
||||
class="h-1.5 w-1.5 rounded-full"
|
||||
:class="listening ? 'bg-emerald-500 animate-pulse' : 'bg-(--fg-subtle)'"
|
||||
:class="listening ? 'bg-emerald-500 animate-pulse' : 'bg-fg-subtle'"
|
||||
/>
|
||||
{{ listening ? 'active' : 'stopped' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Listeners auto-detach on unmount. The returned stop function lets you detach early — press any key, then toggle listening.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { MaybeRefOrGetter } from 'vue';
|
||||
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';
|
||||
|
||||
interface InferEventTarget<Events> {
|
||||
addEventListener: (event: Events, listener?: any, options?: any) => any;
|
||||
removeEventListener: (event: Events, listener?: any, options?: any) => any;
|
||||
addEventListener: (event: Events, listener?: GeneralEventListener, options?: boolean | AddEventListenerOptions) => void;
|
||||
removeEventListener: (event: Events, listener?: GeneralEventListener, options?: boolean | EventListenerOptions) => void;
|
||||
}
|
||||
|
||||
export type GeneralEventListener<E = Event> = (evt: E) => void;
|
||||
@@ -27,7 +27,7 @@ type ListenerOptions = boolean | AddEventListenerOptions;
|
||||
*/
|
||||
export function useEventListener<E extends WindowEventName>(
|
||||
event: Arrayable<E>,
|
||||
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
|
||||
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => void>,
|
||||
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
|
||||
): VoidFunction;
|
||||
|
||||
@@ -41,7 +41,7 @@ export function useEventListener<E extends WindowEventName>(
|
||||
export function useEventListener<E extends WindowEventName>(
|
||||
target: Window,
|
||||
event: Arrayable<E>,
|
||||
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
|
||||
listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => void>,
|
||||
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
|
||||
): VoidFunction;
|
||||
|
||||
@@ -55,7 +55,7 @@ export function useEventListener<E extends WindowEventName>(
|
||||
export function useEventListener<E extends DocumentEventName>(
|
||||
target: Document,
|
||||
event: Arrayable<E>,
|
||||
listener: Arrayable<(this: Document, ev: DocumentEventMap[E]) => any>,
|
||||
listener: Arrayable<(this: Document, ev: DocumentEventMap[E]) => void>,
|
||||
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
|
||||
): VoidFunction;
|
||||
|
||||
@@ -69,7 +69,7 @@ export function useEventListener<E extends DocumentEventName>(
|
||||
export function useEventListener<E extends ElementEventName>(
|
||||
target: MaybeRefOrGetter<HTMLElement | null | undefined>,
|
||||
event: Arrayable<E>,
|
||||
listener: Arrayable<(this: HTMLElement, ev: HTMLElementEventMap[E]) => any>,
|
||||
listener: Arrayable<(this: HTMLElement, ev: HTMLElementEventMap[E]) => void>,
|
||||
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
|
||||
): VoidFunction;
|
||||
|
||||
@@ -101,6 +101,7 @@ export function useEventListener<EventType = Event>(
|
||||
options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>,
|
||||
): VoidFunction;
|
||||
|
||||
// Variadic implementation signature behind the typed overloads above; args are narrowed at runtime.
|
||||
export function useEventListener(...args: any[]) {
|
||||
let target: MaybeRefOrGetter<EventTarget> | undefined = defaultWindow;
|
||||
let _events: Arrayable<string>;
|
||||
|
||||
@@ -34,7 +34,7 @@ async function pick() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-center text-sm text-amber-600 dark:text-amber-400"
|
||||
@@ -44,33 +44,33 @@ async function pick() {
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
class="flex h-32 items-center justify-center rounded-xl border border-(--border) transition-colors duration-300"
|
||||
class="flex h-32 items-center justify-center rounded-xl border border-border transition-colors duration-300"
|
||||
:style="{ backgroundColor: hex, color: readableText }"
|
||||
>
|
||||
<span class="font-mono text-2xl font-bold tabular-nums">{{ hex }}</span>
|
||||
</div>
|
||||
|
||||
<button class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer" @click="pick">
|
||||
<button class="demo-btn-primary" @click="pick">
|
||||
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m2 22 1-1h3l9-9" /><path d="M3 21v-3l9-9" /><path d="m15 6 3.4-3.4a2.1 2.1 0 1 1 3 3L21 6l3 3-3 3-3-3-9 9" />
|
||||
</svg>
|
||||
Pick a color from screen
|
||||
</button>
|
||||
|
||||
<p v-if="error" class="text-center text-xs text-(--fg-subtle)">
|
||||
<p v-if="error" class="text-center text-xs text-fg-subtle">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<div v-if="history.length" class="flex flex-col gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Recent</span>
|
||||
<span class="demo-label">Recent</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="color in history"
|
||||
:key="color"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted) transition hover:border-(--border-strong) cursor-pointer"
|
||||
class="demo-badge transition hover:border-border-strong cursor-pointer"
|
||||
@click="sRGBHex = color"
|
||||
>
|
||||
<span class="size-3 rounded-full border border-(--border)" :style="{ backgroundColor: color }" />
|
||||
<span class="size-3 rounded-full border border-border" :style="{ backgroundColor: color }" />
|
||||
{{ color }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -31,34 +31,34 @@ function select(preset: Preset) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="flex items-center gap-2 rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4">
|
||||
<div class="flex items-center gap-2 rounded-lg border border-border bg-bg-inset px-3 py-2">
|
||||
<div class="flex gap-1.5">
|
||||
<span class="size-2.5 rounded-full bg-red-500/70" />
|
||||
<span class="size-2.5 rounded-full bg-amber-500/70" />
|
||||
<span class="size-2.5 rounded-full bg-emerald-500/70" />
|
||||
</div>
|
||||
<div class="ml-2 flex flex-1 items-center gap-2 rounded-md bg-(--bg) px-2 py-1">
|
||||
<div class="ml-2 flex flex-1 items-center gap-2 rounded-md bg-bg px-2 py-1">
|
||||
<span class="text-base leading-none">{{ presets.find(p => p.label === active)?.emoji }}</span>
|
||||
<span class="truncate text-xs text-(--fg-muted)">My Awesome App</span>
|
||||
<span class="truncate text-xs text-fg-muted">My Awesome App</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-center text-xs text-(--fg-subtle)">
|
||||
<p class="mt-2 text-center text-xs text-fg-subtle">
|
||||
Look at the real browser tab — its favicon updates live.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Choose a favicon</span>
|
||||
<span class="demo-label">Choose a favicon</span>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
:key="preset.label"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="active === preset.label
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="select(preset)"
|
||||
>
|
||||
<span class="text-base leading-none">{{ preset.emoji }}</span>
|
||||
@@ -67,7 +67,7 @@ function select(preset: Preset) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg) break-all">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg break-all">
|
||||
favicon.value = "{{ presets.find(p => p.label === active)?.emoji }} svg"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,26 +35,26 @@ function pick() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-(--fg-muted)">
|
||||
<input v-model="multiple" type="checkbox" class="size-4 rounded border-(--border) accent-(--accent)">
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 text-sm text-fg-muted">
|
||||
<input v-model="multiple" type="checkbox" class="size-4 rounded border-border accent-accent">
|
||||
Allow multiple
|
||||
</label>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-badge">
|
||||
{{ status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer" @click="pick">
|
||||
<button class="demo-btn-primary flex-1" @click="pick">
|
||||
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" y1="3" x2="12" y2="15" />
|
||||
</svg>
|
||||
Choose images
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!selected.length"
|
||||
@click="reset"
|
||||
>
|
||||
@@ -64,25 +64,25 @@ function pick() {
|
||||
|
||||
<div
|
||||
v-if="!selected.length"
|
||||
class="flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-(--border) bg-(--bg-inset) p-6 text-center"
|
||||
class="flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-border bg-bg-inset p-6 text-center"
|
||||
>
|
||||
<span class="text-sm text-(--fg-muted)">No files selected</span>
|
||||
<span class="text-xs text-(--fg-subtle)">Click “Choose images” to open the native dialog</span>
|
||||
<span class="text-sm text-fg-muted">No files selected</span>
|
||||
<span class="text-xs text-fg-subtle">Click “Choose images” to open the native dialog</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">{{ selected.length }} file(s)</span>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ formatBytes(totalBytes) }} total</span>
|
||||
<span class="demo-label">{{ selected.length }} file(s)</span>
|
||||
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ formatBytes(totalBytes) }} total</span>
|
||||
</div>
|
||||
<ul class="flex max-h-44 flex-col gap-1.5 overflow-auto">
|
||||
<li
|
||||
v-for="file in selected"
|
||||
:key="file.name + file.lastModified"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
|
||||
class="flex items-center justify-between gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
|
||||
>
|
||||
<span class="truncate text-sm text-(--fg)">{{ file.name }}</span>
|
||||
<span class="shrink-0 font-mono text-xs tabular-nums text-(--fg-subtle)">{{ formatBytes(file.size) }}</span>
|
||||
<span class="truncate text-sm text-fg">{{ file.name }}</span>
|
||||
<span class="shrink-0 font-mono text-xs tabular-nums text-fg-subtle">{{ formatBytes(file.size) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ async function newFile() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-center text-sm text-amber-600 dark:text-amber-400"
|
||||
@@ -57,28 +57,28 @@ async function newFile() {
|
||||
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer" @click="open()">
|
||||
<button class="demo-btn" @click="open()">
|
||||
Open…
|
||||
</button>
|
||||
<button class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer" @click="newFile">
|
||||
<button class="demo-btn" @click="newFile">
|
||||
New…
|
||||
</button>
|
||||
<button class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100" :disabled="data === undefined" @click="save()">
|
||||
<button class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100" :disabled="data === undefined" @click="save()">
|
||||
Save
|
||||
</button>
|
||||
<button class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100" :disabled="data === undefined" @click="saveAs()">
|
||||
<button class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100" :disabled="data === undefined" @click="saveAs()">
|
||||
Save As…
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="fileName" class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-badge">
|
||||
{{ fileName }}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-badge">
|
||||
{{ fileMIME || 'text/plain' }}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-mono tabular-nums text-(--fg-muted)">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg-inset px-2 py-0.5 text-xs font-mono tabular-nums text-fg-muted">
|
||||
{{ formatBytes(fileSize) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -88,19 +88,19 @@ async function newFile() {
|
||||
v-model="text"
|
||||
rows="6"
|
||||
spellcheck="false"
|
||||
class="w-full resize-none rounded-lg border border-(--border) bg-(--bg) px-3 py-2 font-mono text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input resize-none font-mono"
|
||||
placeholder="File contents…"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-(--border) bg-(--bg-inset) p-6 text-center"
|
||||
class="flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-border bg-bg-inset p-6 text-center"
|
||||
>
|
||||
<span class="text-sm text-(--fg-muted)">No file open</span>
|
||||
<span class="text-xs text-(--fg-subtle)">Open an existing file or create a new one, edit it, then save back to disk.</span>
|
||||
<span class="text-sm text-fg-muted">No file open</span>
|
||||
<span class="text-xs text-fg-subtle">Open an existing file or create a new one, edit it, then save back to disk.</span>
|
||||
</div>
|
||||
|
||||
<p v-if="lastError" class="text-center text-xs text-(--fg-subtle)">
|
||||
<p v-if="lastError" class="text-center text-xs text-fg-subtle">
|
||||
{{ lastError }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,7 @@ const { isSupported, isFullscreen, enter, exit, toggle } = useFullscreen(target)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-center text-sm text-amber-600 dark:text-amber-400"
|
||||
@@ -17,27 +17,27 @@ const { isSupported, isFullscreen, enter, exit, toggle } = useFullscreen(target)
|
||||
|
||||
<div
|
||||
ref="target"
|
||||
class="relative flex h-44 flex-col items-center justify-center gap-3 overflow-hidden rounded-xl border border-(--border) bg-(--bg-elevated) transition-colors"
|
||||
:class="isFullscreen && 'bg-(--bg-inset)'"
|
||||
class="demo-card relative flex h-44 flex-col items-center justify-center gap-3 overflow-hidden transition-colors"
|
||||
:class="isFullscreen && 'bg-bg-inset'"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="isFullscreen
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
<span class="size-1.5 rounded-full" :class="isFullscreen ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
<span class="size-1.5 rounded-full" :class="isFullscreen ? 'bg-emerald-500' : 'bg-fg-subtle'" />
|
||||
{{ isFullscreen ? 'Fullscreen' : 'Windowed' }}
|
||||
</span>
|
||||
|
||||
<p class="px-6 text-center text-sm text-(--fg-muted)">
|
||||
<p class="px-6 text-center text-sm text-fg-muted">
|
||||
This panel becomes the fullscreen target. Press
|
||||
<kbd class="rounded border border-(--border) bg-(--bg) px-1.5 py-0.5 font-mono text-xs text-(--fg)">Esc</kbd>
|
||||
<kbd class="rounded border border-border bg-bg px-1.5 py-0.5 font-mono text-xs text-fg">Esc</kbd>
|
||||
to leave.
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!isSupported"
|
||||
@click="toggle"
|
||||
>
|
||||
@@ -55,14 +55,14 @@ const { isSupported, isFullscreen, enter, exit, toggle } = useFullscreen(target)
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!isSupported || isFullscreen"
|
||||
@click="enter"
|
||||
>
|
||||
Enter
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!isSupported || !isFullscreen"
|
||||
@click="exit"
|
||||
>
|
||||
|
||||
@@ -146,7 +146,7 @@ export function useFullscreen(
|
||||
|
||||
const isCurrentElementFullScreen = (): boolean => {
|
||||
if (fullscreenElementMethod)
|
||||
return (document as any)?.[fullscreenElementMethod] === targetRef.value;
|
||||
return (document as Record<string, unknown> | undefined)?.[fullscreenElementMethod] === targetRef.value;
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -155,12 +155,12 @@ export function useFullscreen(
|
||||
if (!flag)
|
||||
return false;
|
||||
|
||||
const docFlag = document && (document as any)[flag];
|
||||
const docFlag = document && (document as unknown as Record<string, unknown>)[flag];
|
||||
if (docFlag !== null && docFlag !== undefined)
|
||||
return Boolean(docFlag);
|
||||
|
||||
// Fallback for WebKit / iOS Safari, where the flag lives on the element itself.
|
||||
const elFlag = (targetRef.value as any)?.[flag];
|
||||
const elFlag = (targetRef.value as unknown as Record<string, unknown> | null | undefined)?.[flag];
|
||||
if (elFlag !== null && elFlag !== undefined)
|
||||
return Boolean(elFlag);
|
||||
|
||||
@@ -173,13 +173,15 @@ export function useFullscreen(
|
||||
|
||||
const method = exitMethod.value;
|
||||
if (method) {
|
||||
if (typeof (document as any)?.[method] === 'function')
|
||||
await (document as any)[method]();
|
||||
const docMethod = (document as unknown as Record<string, unknown> | undefined)?.[method];
|
||||
if (isFunction(docMethod))
|
||||
await docMethod.call(document);
|
||||
else {
|
||||
// Fallback for Safari iOS, where exit lives on the element.
|
||||
const el = targetRef.value as any;
|
||||
if (isFunction(el?.[method]))
|
||||
await el[method]();
|
||||
const el = targetRef.value as unknown as Record<string, unknown> | null | undefined;
|
||||
const elMethod = el?.[method];
|
||||
if (isFunction(elMethod))
|
||||
await elMethod.call(targetRef.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,10 +195,11 @@ export function useFullscreen(
|
||||
if (isElementFullScreen())
|
||||
await exit();
|
||||
|
||||
const el = targetRef.value as any;
|
||||
const el = targetRef.value as unknown as Record<string, unknown> | null | undefined;
|
||||
const method = requestMethod.value;
|
||||
if (method && isFunction(el?.[method])) {
|
||||
await el[method]();
|
||||
const elMethod = method ? el?.[method] : undefined;
|
||||
if (isFunction(elMethod)) {
|
||||
await elMethod.call(targetRef.value);
|
||||
isFullscreen.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const { isLoading, isReady, error, state, execute } = useImage(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="(sample, index) in samples"
|
||||
@@ -26,15 +26,15 @@ const { isLoading, isReady, error, state, execute } = useImage(
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="current === index
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="current = index"
|
||||
>
|
||||
{{ sample.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative aspect-[8/5] w-full overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset)">
|
||||
<div class="relative aspect-[8/5] w-full overflow-hidden rounded-xl border border-border bg-bg-inset">
|
||||
<Transition
|
||||
enter-active-class="transition duration-300"
|
||||
enter-from-class="opacity-0 scale-[1.02]"
|
||||
@@ -61,8 +61,8 @@ const { isLoading, isReady, error, state, execute } = useImage(
|
||||
v-else
|
||||
class="absolute inset-0 flex flex-col items-center justify-center gap-3"
|
||||
>
|
||||
<span class="h-7 w-7 animate-spin rounded-full border-2 border-(--border-strong) border-t-(--accent)" />
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="h-7 w-7 animate-spin rounded-full border-2 border-border-strong border-t-accent" />
|
||||
<p class="demo-label">
|
||||
Loading…
|
||||
</p>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@ const { isLoading, isReady, error, state, execute } = useImage(
|
||||
</span>
|
||||
<span
|
||||
v-if="isReady && state"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted) tabular-nums"
|
||||
class="demo-badge tabular-nums"
|
||||
>
|
||||
{{ state.naturalWidth }}×{{ state.naturalHeight }}
|
||||
</span>
|
||||
@@ -91,7 +91,7 @@ const { isLoading, isReady, error, state, execute } = useImage(
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="isLoading"
|
||||
@click="execute()"
|
||||
>
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface UseImageOptions {
|
||||
export interface UseImageAsyncStateOptions
|
||||
extends UseAsyncStateOptions<true, HTMLImageElement | undefined>, ConfigurableWindow {}
|
||||
|
||||
export type UseImageReturn = UseAsyncStateReturn<HTMLImageElement | undefined, any[], true>;
|
||||
export type UseImageReturn = UseAsyncStateReturn<HTMLImageElement | undefined, [], true>;
|
||||
|
||||
interface LoadImageContext {
|
||||
window?: Window;
|
||||
|
||||
@@ -34,14 +34,14 @@ function familyStyle(name: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<p class="demo-label">
|
||||
Local Font Access
|
||||
</p>
|
||||
<span
|
||||
v-if="fonts.length"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted) tabular-nums"
|
||||
class="demo-badge tabular-nums"
|
||||
>
|
||||
{{ fonts.length }} faces · {{ familyCount }} families
|
||||
</span>
|
||||
@@ -57,7 +57,7 @@ function familyStyle(name: string) {
|
||||
<template v-else>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="loading"
|
||||
@click="pickFonts"
|
||||
>
|
||||
@@ -76,28 +76,28 @@ function familyStyle(name: string) {
|
||||
v-model="filter"
|
||||
type="search"
|
||||
placeholder="Filter by name…"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
|
||||
<ul class="max-h-56 divide-y divide-(--border) overflow-y-auto rounded-xl border border-(--border) bg-(--bg-elevated)">
|
||||
<ul class="demo-card max-h-56 divide-y divide-border overflow-y-auto">
|
||||
<li
|
||||
v-for="font in filtered"
|
||||
:key="font.postscriptName"
|
||||
class="flex items-baseline justify-between gap-3 px-3 py-2"
|
||||
>
|
||||
<span
|
||||
class="truncate text-base text-(--fg)"
|
||||
class="truncate text-base text-fg"
|
||||
:style="familyStyle(font.fullName)"
|
||||
>
|
||||
{{ font.fullName }}
|
||||
</span>
|
||||
<span class="shrink-0 font-mono text-xs text-(--fg-subtle)">
|
||||
<span class="shrink-0 font-mono text-xs text-fg-subtle">
|
||||
{{ font.style }}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
v-if="!filtered.length"
|
||||
class="px-3 py-6 text-center text-sm text-(--fg-subtle)"
|
||||
class="px-3 py-6 text-center text-sm text-fg-subtle"
|
||||
>
|
||||
No fonts match "{{ filter }}"
|
||||
</li>
|
||||
@@ -106,7 +106,7 @@ function familyStyle(name: string) {
|
||||
|
||||
<p
|
||||
v-else-if="!error && !loading"
|
||||
class="rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-6 text-center text-sm text-(--fg-subtle)"
|
||||
class="rounded-lg border border-border bg-bg-inset px-3 py-6 text-center text-sm text-fg-subtle"
|
||||
>
|
||||
Click above to grant the <code class="font-mono">local-fonts</code> permission and list your fonts.
|
||||
</p>
|
||||
|
||||
@@ -22,35 +22,35 @@ const queries = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 text-center">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="demo-card p-4 text-center">
|
||||
<p class="demo-label">
|
||||
Current layout
|
||||
</p>
|
||||
<p class="mt-1 font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
<p class="demo-stat mt-1 text-3xl">
|
||||
{{ breakpoint ? 'desktop' : isMedium ? 'tablet' : 'mobile' }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-(--fg-muted)">
|
||||
<p class="mt-1 text-sm text-fg-muted">
|
||||
Resize the window to watch these queries flip live.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-(--border) rounded-xl border border-(--border) bg-(--bg-elevated)">
|
||||
<ul class="demo-card divide-y divide-border">
|
||||
<li
|
||||
v-for="query in queries"
|
||||
:key="query.label"
|
||||
class="flex items-center justify-between gap-3 px-3 py-2.5"
|
||||
>
|
||||
<code class="font-mono text-sm text-(--fg)">{{ query.label }}</code>
|
||||
<code class="font-mono text-sm text-fg">{{ query.label }}</code>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
|
||||
:class="query.match.value
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-subtle)'"
|
||||
: 'border-border bg-bg-inset text-fg-subtle'"
|
||||
>
|
||||
<span
|
||||
class="h-1.5 w-1.5 rounded-full"
|
||||
:class="query.match.value ? 'bg-emerald-500' : 'bg-(--border-strong)'"
|
||||
:class="query.match.value ? 'bg-emerald-500' : 'bg-border-strong'"
|
||||
/>
|
||||
{{ query.match.value ? 'matches' : 'no match' }}
|
||||
</span>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { defaultWindow } from '@/types';
|
||||
import type { ConfigurableWindow } from '@/types';
|
||||
import { useSupported } from '@/composables/utilities/useSupported';
|
||||
import { useEventListener } from '@/composables/browser/useEventListener';
|
||||
import { pxValue } from '@robonen/platform/browsers';
|
||||
|
||||
export interface UseMediaQueryOptions extends ConfigurableWindow {
|
||||
/**
|
||||
@@ -20,22 +21,6 @@ export interface UseMediaQueryOptions extends ConfigurableWindow {
|
||||
ssrWidth?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a CSS length token (e.g. `"1024px"`, `"48em"`, `"30rem"`) to pixels.
|
||||
* Falls back to treating `em`/`rem` as the conventional 16px root size.
|
||||
*/
|
||||
function pxValue(value: string): number {
|
||||
const number = Number.parseFloat(value);
|
||||
|
||||
if (Number.isNaN(number))
|
||||
return Number.NaN;
|
||||
|
||||
if (/(?:em|rem)\s*$/i.test(value))
|
||||
return number * 16;
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort evaluation of `min-width` / `max-width` media queries against a
|
||||
* known viewport width, for SSR. Comma-separated queries are OR-combined and
|
||||
|
||||
@@ -44,19 +44,19 @@ function generateSample() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<label
|
||||
class="flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed p-6 text-center transition"
|
||||
:class="dragging
|
||||
? 'border-(--accent) bg-(--accent-subtle)'
|
||||
: 'border-(--border) bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-accent bg-accent-subtle'
|
||||
: 'border-border bg-bg-inset hover:border-border-strong'"
|
||||
@dragover.prevent="dragging = true"
|
||||
@dragleave.prevent="dragging = false"
|
||||
@drop.prevent="onDrop"
|
||||
>
|
||||
<span class="text-2xl">📎</span>
|
||||
<span class="text-sm font-medium text-(--fg)">Drop a file or click to choose</span>
|
||||
<span class="text-xs text-(--fg-subtle)">An object URL is created instantly</span>
|
||||
<span class="text-sm font-medium text-fg">Drop a file or click to choose</span>
|
||||
<span class="text-xs text-fg-subtle">An object URL is created instantly</span>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
@@ -67,14 +67,14 @@ function generateSample() {
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn flex-1"
|
||||
@click="generateSample"
|
||||
>
|
||||
Use sample image
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!file"
|
||||
@click="clear"
|
||||
>
|
||||
@@ -84,26 +84,26 @@ function generateSample() {
|
||||
|
||||
<div
|
||||
v-if="file"
|
||||
class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4"
|
||||
class="demo-card flex flex-col gap-3 p-4"
|
||||
>
|
||||
<img
|
||||
v-if="isImage && url"
|
||||
:src="url"
|
||||
alt="Selected file preview"
|
||||
class="mx-auto max-h-40 rounded-lg border border-(--border) object-contain"
|
||||
class="mx-auto max-h-40 rounded-lg border border-border object-contain"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3 text-sm">
|
||||
<span class="truncate font-medium text-(--fg)">{{ file.name }}</span>
|
||||
<span class="shrink-0 font-mono text-xs text-(--fg-muted) tabular-nums">{{ sizeLabel }}</span>
|
||||
<span class="truncate font-medium text-fg">{{ file.name }}</span>
|
||||
<span class="shrink-0 font-mono text-xs text-fg-muted tabular-nums">{{ sizeLabel }}</span>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg) break-all">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg break-all">
|
||||
{{ url }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-else
|
||||
class="rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-6 text-center text-sm text-(--fg-subtle)"
|
||||
class="rounded-lg border border-border bg-bg-inset px-3 py-6 text-center text-sm text-fg-subtle"
|
||||
>
|
||||
No source — the URL ref is <code class="font-mono">undefined</code>
|
||||
</p>
|
||||
|
||||
@@ -41,9 +41,9 @@ function simulate() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">WebOTP</span>
|
||||
<span class="demo-label">WebOTP</span>
|
||||
<span
|
||||
class="rounded-full px-2 py-0.5 text-xs font-medium"
|
||||
:class="isSupported
|
||||
@@ -61,27 +61,27 @@ function simulate() {
|
||||
autocomplete="one-time-code"
|
||||
maxlength="6"
|
||||
placeholder="••••••"
|
||||
class="w-full rounded-xl border border-(--border) bg-(--bg-inset) px-4 py-3 text-center font-mono text-2xl tracking-[0.4em] tabular-nums text-(--fg) outline-none transition focus:border-(--accent)"
|
||||
class="w-full rounded-xl border border-border bg-bg-inset px-4 py-3 text-center font-mono text-2xl tracking-[0.4em] tabular-nums text-fg outline-none transition focus:border-accent"
|
||||
@input="onInput"
|
||||
>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-if="!isReceiving"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg bg-accent px-3 py-1.5 text-sm font-medium text-accent-fg transition hover:bg-accent-hover active:scale-[0.98] cursor-pointer"
|
||||
@click="listen"
|
||||
>
|
||||
Listen for code
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) px-3 py-1.5 text-sm font-medium text-(--fg-muted) transition hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-fg-muted transition hover:border-border-strong active:scale-[0.98] cursor-pointer"
|
||||
@click="abort()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center rounded-lg border border-(--border) px-3 py-1.5 text-sm font-medium text-(--fg-muted) transition hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex items-center justify-center rounded-lg border border-border px-3 py-1.5 text-sm font-medium text-fg-muted transition hover:border-border-strong active:scale-[0.98] cursor-pointer"
|
||||
@click="simulate"
|
||||
>
|
||||
Simulate
|
||||
@@ -92,13 +92,13 @@ function simulate() {
|
||||
<span
|
||||
class="size-2 rounded-full"
|
||||
:class="{
|
||||
'bg-(--fg-subtle)': status.tone === 'idle',
|
||||
'bg-fg-subtle': status.tone === 'idle',
|
||||
'animate-pulse bg-amber-500': status.tone === 'pending',
|
||||
'bg-emerald-500': status.tone === 'ok',
|
||||
'bg-red-500': status.tone === 'error',
|
||||
}"
|
||||
/>
|
||||
<span class="text-(--fg-muted)">{{ status.label }}</span>
|
||||
<span class="text-fg-muted">{{ status.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,13 +32,13 @@ const meta = computed(() => {
|
||||
case 'prompt':
|
||||
return { label: 'prompt', dot: 'bg-amber-500', text: 'text-amber-600 dark:text-amber-400', ring: 'border-amber-500/30 bg-amber-500/10' };
|
||||
default:
|
||||
return { label: 'unknown', dot: 'bg-(--border-strong)', text: 'text-(--fg-subtle)', ring: 'border-(--border) bg-(--bg-inset)' };
|
||||
return { label: 'unknown', dot: 'bg-border-strong', text: 'text-fg-subtle', ring: 'border-border bg-bg-inset' };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-300"
|
||||
@@ -48,12 +48,12 @@ const meta = computed(() => {
|
||||
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<label class="demo-label">
|
||||
Permission
|
||||
</label>
|
||||
<select
|
||||
v-model="selected"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="w-full rounded-lg border border-border bg-bg px-3 py-2 text-sm text-fg transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option v-for="name in names" :key="name" :value="name">
|
||||
{{ name }}
|
||||
@@ -61,19 +61,19 @@ const meta = computed(() => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<ul class="divide-y divide-(--border) rounded-xl border border-(--border) bg-(--bg-elevated)">
|
||||
<ul class="demo-card divide-y divide-border">
|
||||
<li
|
||||
v-for="perm in permissions"
|
||||
:key="perm.name"
|
||||
class="flex items-center justify-between gap-3 px-3 py-2.5"
|
||||
>
|
||||
<code class="font-mono text-sm text-(--fg)">{{ perm.name }}</code>
|
||||
<span class="font-mono text-xs text-(--fg-muted)">{{ perm.state.value ?? 'unknown' }}</span>
|
||||
<code class="font-mono text-sm text-fg">{{ perm.name }}</code>
|
||||
<span class="font-mono text-xs text-fg-muted">{{ perm.state.value ?? 'unknown' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="flex flex-col items-center gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-card flex flex-col items-center gap-3 p-5">
|
||||
<span class="demo-label">
|
||||
{{ selected }}
|
||||
</span>
|
||||
<span
|
||||
@@ -87,13 +87,13 @@ const meta = computed(() => {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary"
|
||||
@click="active.query()"
|
||||
>
|
||||
Re-check "{{ selected }}"
|
||||
</button>
|
||||
|
||||
<p class="text-center text-xs text-(--fg-subtle)">
|
||||
<p class="text-center text-xs text-fg-subtle">
|
||||
Status updates live if you change a permission in your browser settings.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -22,12 +22,12 @@ const previewClass = computed(() =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Preferred color scheme
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-badge">
|
||||
<span class="size-1.5 rounded-full bg-emerald-500" />
|
||||
live
|
||||
</span>
|
||||
@@ -39,22 +39,22 @@ const previewClass = computed(() =>
|
||||
:key="option.value"
|
||||
class="flex items-center gap-3 rounded-lg border px-3 py-2.5 transition"
|
||||
:class="scheme === option.value
|
||||
? 'border-(--accent) bg-(--accent-subtle)'
|
||||
: 'border-(--border) bg-(--bg-elevated)'"
|
||||
? 'border-accent bg-accent-subtle'
|
||||
: 'border-border bg-bg-elevated'"
|
||||
>
|
||||
<span class="text-lg leading-none">{{ option.icon }}</span>
|
||||
<span class="flex flex-1 flex-col">
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="scheme === option.value ? 'text-(--accent-text)' : 'text-(--fg)'"
|
||||
:class="scheme === option.value ? 'text-accent-text' : 'text-fg'"
|
||||
>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="font-mono text-xs text-(--fg-subtle)">{{ option.hint }}</span>
|
||||
<span class="font-mono text-xs text-fg-subtle">{{ option.hint }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="scheme === option.value"
|
||||
class="text-(--accent-text)"
|
||||
class="text-accent-text"
|
||||
aria-hidden="true"
|
||||
>✓</span>
|
||||
</li>
|
||||
@@ -73,7 +73,7 @@ const previewClass = computed(() =>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Read-only: change your OS appearance setting to see this update instantly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -17,22 +17,22 @@ const active = computed(() => levels.find(l => l.value === contrast.value));
|
||||
const cardClass = computed(() => {
|
||||
switch (contrast.value) {
|
||||
case 'more':
|
||||
return 'border-(--border-strong) bg-(--bg-inset)';
|
||||
return 'border-border-strong bg-bg-inset';
|
||||
case 'less':
|
||||
return 'border-(--border) bg-(--bg-subtle) opacity-90';
|
||||
return 'border-border bg-bg-subtle opacity-90';
|
||||
default:
|
||||
return 'border-(--border) bg-(--bg-elevated)';
|
||||
return 'border-border bg-bg-elevated';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Preferred contrast
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-badge">
|
||||
{{ contrast }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -43,30 +43,30 @@ const cardClass = computed(() => {
|
||||
:key="level.value"
|
||||
class="flex flex-col gap-1 rounded-lg border px-3 py-2.5 transition"
|
||||
:class="contrast === level.value
|
||||
? 'border-(--accent) bg-(--accent-subtle)'
|
||||
: 'border-(--border) bg-(--bg-elevated)'"
|
||||
? 'border-accent bg-accent-subtle'
|
||||
: 'border-border bg-bg-elevated'"
|
||||
>
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="contrast === level.value ? 'text-(--accent-text)' : 'text-(--fg)'"
|
||||
:class="contrast === level.value ? 'text-accent-text' : 'text-fg'"
|
||||
>
|
||||
{{ level.label }}
|
||||
</span>
|
||||
<span class="text-xs leading-snug text-(--fg-muted)">{{ level.desc }}</span>
|
||||
<span class="text-xs leading-snug text-fg-muted">{{ level.desc }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border p-4 transition-colors" :class="cardClass">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Adaptive surface
|
||||
</span>
|
||||
<p class="mt-1 text-sm text-(--fg)">
|
||||
<p class="mt-1 text-sm text-fg">
|
||||
This card adjusts its borders and fill to match the
|
||||
<span class="font-mono text-(--fg-muted)">{{ active?.query }}</span> level.
|
||||
<span class="font-mono text-fg-muted">{{ active?.query }}</span> level.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Read-only: toggle your OS accessibility "increase / reduce contrast" setting to update.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,9 @@ const label = computed(() => (isDark.value ? 'Dark' : 'Light'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
prefers-color-scheme: dark
|
||||
</span>
|
||||
<span
|
||||
@@ -25,7 +25,7 @@ const label = computed(() => (isDark.value ? 'Dark' : 'Light'));
|
||||
|
||||
<!-- A miniature sky scene that flips between day and night. -->
|
||||
<div
|
||||
class="relative flex h-40 items-end overflow-hidden rounded-xl border border-(--border) p-4 transition-colors duration-500"
|
||||
class="relative flex h-40 items-end overflow-hidden rounded-xl border border-border p-4 transition-colors duration-500"
|
||||
:class="isDark
|
||||
? 'bg-gradient-to-b from-slate-900 to-slate-700'
|
||||
: 'bg-gradient-to-b from-sky-300 to-sky-100'"
|
||||
@@ -60,12 +60,12 @@ const label = computed(() => (isDark.value ? 'Dark' : 'Light'));
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
const isDark = usePreferredDark()
|
||||
<span class="text-(--fg-subtle)"> // </span>{{ isDark }}
|
||||
<span class="text-fg-subtle"> // </span>{{ isDark }}
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Read-only: switch your OS to dark/light mode to watch the scene change.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -30,25 +30,25 @@ const primary = computed(() => languages.value[0] ?? 'en');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
navigator.languages
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-badge">
|
||||
{{ languages.length }} {{ languages.length === 1 ? 'locale' : 'locales' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-card p-4">
|
||||
<span class="demo-label">
|
||||
Primary language
|
||||
</span>
|
||||
<div class="mt-1 flex items-center gap-2.5">
|
||||
<span class="text-2xl leading-none">{{ flagOf(primary) }}</span>
|
||||
<span class="flex flex-col">
|
||||
<span class="text-base font-semibold text-(--fg)">{{ nameOf(primary) }}</span>
|
||||
<span class="font-mono text-xs text-(--fg-subtle)">{{ primary }}</span>
|
||||
<span class="text-base font-semibold text-fg">{{ nameOf(primary) }}</span>
|
||||
<span class="font-mono text-xs text-fg-subtle">{{ primary }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,26 +57,26 @@ const primary = computed(() => languages.value[0] ?? 'en');
|
||||
<li
|
||||
v-for="(lang, index) in languages"
|
||||
:key="`${lang}-${index}`"
|
||||
class="flex items-center gap-3 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-2"
|
||||
class="flex items-center gap-3 rounded-lg border border-border bg-bg-elevated px-3 py-2"
|
||||
>
|
||||
<span class="w-5 text-center font-mono text-xs text-(--fg-subtle) tabular-nums">
|
||||
<span class="w-5 text-center font-mono text-xs text-fg-subtle tabular-nums">
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
<span class="text-lg leading-none">{{ flagOf(lang) }}</span>
|
||||
<span class="flex flex-1 flex-col">
|
||||
<span class="text-sm font-medium text-(--fg)">{{ nameOf(lang) }}</span>
|
||||
<span class="font-mono text-xs text-(--fg-subtle)">{{ lang }}</span>
|
||||
<span class="text-sm font-medium text-fg">{{ nameOf(lang) }}</span>
|
||||
<span class="font-mono text-xs text-fg-subtle">{{ lang }}</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="index === 0"
|
||||
class="inline-flex items-center rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
class="inline-flex items-center rounded-md border border-border bg-bg-inset px-2 py-0.5 text-xs font-medium text-fg-muted"
|
||||
>
|
||||
preferred
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Read-only: updates automatically on the browser's <span class="font-mono">languagechange</span> event.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,9 @@ const duration = computed(() => (reduced.value ? 0 : 1.2));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
prefers-reduced-motion
|
||||
</span>
|
||||
<span
|
||||
@@ -28,14 +28,14 @@ const duration = computed(() => (reduced.value ? 0 : 1.2));
|
||||
</div>
|
||||
|
||||
<!-- Animated demo box: the orbiting dot pauses when motion is reduced. -->
|
||||
<div class="flex h-40 items-center justify-center rounded-xl border border-(--border) bg-(--bg-inset)">
|
||||
<div class="flex h-40 items-center justify-center rounded-xl border border-border bg-bg-inset">
|
||||
<div class="relative size-24">
|
||||
<div class="absolute inset-0 rounded-full border-2 border-dashed border-(--border-strong)" />
|
||||
<div class="absolute inset-0 rounded-full border-2 border-dashed border-border-strong" />
|
||||
<div
|
||||
class="orbit absolute left-1/2 top-1/2 size-24"
|
||||
:style="{ animationDuration: `${duration}s`, animationPlayState: reduced ? 'paused' : 'running' }"
|
||||
>
|
||||
<span class="absolute -left-2 -top-2 size-4 rounded-full bg-(--accent) shadow-lg" />
|
||||
<span class="absolute -left-2 -top-2 size-4 rounded-full bg-accent shadow-lg" />
|
||||
</div>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<span class="text-2xl">{{ reduced ? '⏸' : '🎞️' }}</span>
|
||||
@@ -43,24 +43,24 @@ const duration = computed(() => (reduced.value ? 0 : 1.2));
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-card p-4">
|
||||
<span class="demo-label">
|
||||
Derived setting
|
||||
</span>
|
||||
<div class="mt-1 flex items-baseline gap-2">
|
||||
<span class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
<span class="demo-stat text-3xl">
|
||||
{{ reduced ? 0 : 1200 }}
|
||||
</span>
|
||||
<span class="text-sm text-(--fg-muted)">ms transition</span>
|
||||
<span class="text-sm text-fg-muted">ms transition</span>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-(--fg-muted)">
|
||||
<p class="mt-2 text-sm text-fg-muted">
|
||||
{{ reduced
|
||||
? 'Reduced motion requested — animations are disabled.'
|
||||
: 'Full motion — animations run at normal speed.' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Read-only: enable "Reduce motion" in your OS accessibility settings to pause the orbit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,9 @@ const isReduced = computed(() => transparency.value === 'reduce');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
prefers-reduced-transparency
|
||||
</span>
|
||||
<span
|
||||
@@ -28,23 +28,23 @@ const isReduced = computed(() => transparency.value === 'reduce');
|
||||
</div>
|
||||
|
||||
<!-- Live preview: a frosted glass panel that flattens when reduce is preferred -->
|
||||
<div class="relative overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset) p-4">
|
||||
<div class="relative overflow-hidden rounded-xl border border-border bg-bg-inset p-4">
|
||||
<div
|
||||
class="pointer-events-none absolute -left-6 -top-8 size-28 rounded-full bg-(--accent) opacity-60 blur-xl"
|
||||
class="pointer-events-none absolute -left-6 -top-8 size-28 rounded-full bg-accent opacity-60 blur-xl"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute -bottom-10 right-2 size-24 rounded-full bg-sky-500 opacity-50 blur-xl"
|
||||
/>
|
||||
<div
|
||||
class="relative rounded-lg border border-(--border) p-4 transition"
|
||||
class="relative rounded-lg border border-border p-4 transition"
|
||||
:class="isReduced
|
||||
? 'bg-(--bg-elevated)'
|
||||
: 'bg-(--bg-elevated)/60 backdrop-blur-md'"
|
||||
? 'bg-bg-elevated'
|
||||
: 'bg-bg-elevated/60 backdrop-blur-md'"
|
||||
>
|
||||
<p class="text-sm font-medium text-(--fg)">
|
||||
<p class="text-sm font-medium text-fg">
|
||||
Glass card
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-(--fg-muted)">
|
||||
<p class="mt-1 text-sm text-fg-muted">
|
||||
{{ isReduced
|
||||
? 'Translucency removed for clarity.'
|
||||
: 'Background blurs through the panel.' }}
|
||||
@@ -52,8 +52,8 @@ const isReduced = computed(() => transparency.value === 'reduce');
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs leading-relaxed text-(--fg-subtle)">
|
||||
Toggle <span class="font-mono text-(--fg-muted)">Reduce transparency</span> in your OS
|
||||
<p class="text-xs leading-relaxed text-fg-subtle">
|
||||
Toggle <span class="font-mono text-fg-muted">Reduce transparency</span> in your OS
|
||||
accessibility settings to see this update live.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ function onUnload() {
|
||||
}
|
||||
|
||||
const statusStyles = {
|
||||
idle: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)',
|
||||
idle: 'border-border bg-bg-inset text-fg-muted',
|
||||
loading: 'border-sky-500/30 bg-sky-500/10 text-sky-600 dark:text-sky-400',
|
||||
loaded: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
|
||||
error: 'border-red-500/30 bg-red-500/10 text-red-600 dark:text-red-400',
|
||||
@@ -43,10 +43,10 @@ const statusStyles = {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Script status
|
||||
</span>
|
||||
<span
|
||||
@@ -56,7 +56,7 @@ const statusStyles = {
|
||||
<span
|
||||
class="size-1.5 rounded-full"
|
||||
:class="{
|
||||
'bg-(--fg-subtle)': status === 'idle',
|
||||
'bg-fg-subtle': status === 'idle',
|
||||
'bg-sky-500 animate-pulse': status === 'loading',
|
||||
'bg-emerald-500': status === 'loaded',
|
||||
'bg-red-500': status === 'error',
|
||||
@@ -66,24 +66,24 @@ const statusStyles = {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 truncate rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-xs text-(--fg-muted)">
|
||||
<div class="mt-3 truncate rounded-lg border border-border bg-bg-inset p-3 font-mono text-xs text-fg-muted">
|
||||
{{ src }}
|
||||
</div>
|
||||
|
||||
<dl class="mt-3 grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<dt class="text-xs text-(--fg-subtle)">
|
||||
<dt class="text-xs text-fg-subtle">
|
||||
<script> element
|
||||
</dt>
|
||||
<dd class="font-mono text-(--fg)">
|
||||
<dd class="font-mono text-fg">
|
||||
{{ scriptTag ? 'present' : 'null' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-(--fg-subtle)">
|
||||
<dt class="text-xs text-fg-subtle">
|
||||
Loaded at
|
||||
</dt>
|
||||
<dd class="font-mono text-(--fg)">
|
||||
<dd class="font-mono text-fg">
|
||||
{{ loadedAt ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@ const statusStyles = {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="status === 'loading' || status === 'loaded'"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="onLoad"
|
||||
>
|
||||
{{ status === 'loading' ? 'Loading…' : 'Load script' }}
|
||||
@@ -102,15 +102,15 @@ const statusStyles = {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="status !== 'loaded'"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="onUnload"
|
||||
>
|
||||
Unload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs leading-relaxed text-(--fg-subtle)">
|
||||
Manual mode — the tag is injected into <span class="font-mono text-(--fg-muted)"><head></span>
|
||||
<p class="text-xs leading-relaxed text-fg-subtle">
|
||||
Manual mode — the tag is injected into <span class="font-mono text-fg-muted"><head></span>
|
||||
only when you click, and removed on unload.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -26,48 +26,48 @@ async function onShare() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Web Share API
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="isSupported
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
<span
|
||||
class="size-1.5 rounded-full"
|
||||
:class="isSupported ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
|
||||
:class="isSupported ? 'bg-emerald-500' : 'bg-fg-subtle'"
|
||||
/>
|
||||
{{ isSupported ? 'Supported' : 'Unsupported' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card flex flex-col gap-3 p-4">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Title</span>
|
||||
<span class="demo-label">Title</span>
|
||||
<input
|
||||
v-model="payload.title"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Text</span>
|
||||
<span class="demo-label">Text</span>
|
||||
<input
|
||||
v-model="payload.text"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">URL</span>
|
||||
<span class="demo-label">URL</span>
|
||||
<input
|
||||
v-model="payload.url"
|
||||
type="url"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@ async function onShare() {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isSupported"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="onShare"
|
||||
>
|
||||
<svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
@@ -88,7 +88,7 @@ async function onShare() {
|
||||
Share
|
||||
</button>
|
||||
|
||||
<p v-if="!isSupported" class="text-xs leading-relaxed text-(--fg-subtle)">
|
||||
<p v-if="!isSupported" class="text-xs leading-relaxed text-fg-subtle">
|
||||
The Web Share API is not available in this browser. It works on most mobile
|
||||
browsers and Safari.
|
||||
</p>
|
||||
@@ -97,7 +97,7 @@ async function onShare() {
|
||||
class="text-xs font-medium"
|
||||
:class="lastResult === 'shared'
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: 'text-(--fg-muted)'"
|
||||
: 'text-fg-muted'"
|
||||
>
|
||||
{{ lastResult === 'shared' ? 'Content shared.' : 'Share sheet dismissed.' }}
|
||||
</p>
|
||||
|
||||
@@ -11,41 +11,41 @@ const { id, css, isLoaded, load, unload } = useStyleTag(initialCss, { immediate:
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Injected style tag
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="isLoaded
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
<span
|
||||
class="size-1.5 rounded-full"
|
||||
:class="isLoaded ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
|
||||
:class="isLoaded ? 'bg-emerald-500' : 'bg-fg-subtle'"
|
||||
/>
|
||||
{{ isLoaded ? 'Loaded' : 'Unloaded' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Live target affected by the injected stylesheet -->
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-inset) p-4">
|
||||
<div class="rounded-xl border border-border bg-bg-inset p-4">
|
||||
<div
|
||||
class="styletag-demo-box rounded-lg border border-(--border) bg-(--bg-elevated) px-4 py-6 text-center text-sm font-semibold text-(--fg) transition-all"
|
||||
class="styletag-demo-box rounded-lg border border-border bg-bg-elevated px-4 py-6 text-center text-sm font-semibold text-fg transition-all"
|
||||
>
|
||||
Styled by <style id="{{ id }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">CSS source</span>
|
||||
<span class="demo-label">CSS source</span>
|
||||
<textarea
|
||||
v-model="css"
|
||||
rows="5"
|
||||
spellcheck="false"
|
||||
class="w-full resize-none rounded-lg border border-(--border) bg-(--bg) px-3 py-2 font-mono text-xs leading-relaxed text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="w-full resize-none rounded-lg border border-border bg-bg px-3 py-2 font-mono text-xs leading-relaxed text-fg placeholder:text-fg-subtle transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -53,7 +53,7 @@ const { id, css, isLoaded, load, unload } = useStyleTag(initialCss, { immediate:
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isLoaded"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="load"
|
||||
>
|
||||
Load
|
||||
@@ -61,14 +61,14 @@ const { id, css, isLoaded, load, unload } = useStyleTag(initialCss, { immediate:
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isLoaded"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="unload"
|
||||
>
|
||||
Unload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs leading-relaxed text-(--fg-subtle)">
|
||||
<p class="text-xs leading-relaxed text-fg-subtle">
|
||||
Editing the CSS updates the live stylesheet instantly while loaded.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,20 +5,20 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Web Locks election
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="isSupported
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
<span
|
||||
class="size-1.5 rounded-full"
|
||||
:class="isSupported ? 'bg-emerald-500' : 'bg-(--fg-subtle)'"
|
||||
:class="isSupported ? 'bg-emerald-500' : 'bg-fg-subtle'"
|
||||
/>
|
||||
{{ isSupported ? 'Supported' : 'Unsupported' }}
|
||||
</span>
|
||||
@@ -28,14 +28,14 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
|
||||
<div
|
||||
class="flex flex-col items-center gap-2 rounded-xl border p-6 transition-colors"
|
||||
:class="isLeader
|
||||
? 'border-(--accent) bg-(--accent-subtle)'
|
||||
: 'border-(--border) bg-(--bg-elevated)'"
|
||||
? 'border-accent bg-accent-subtle'
|
||||
: 'border-border bg-bg-elevated'"
|
||||
>
|
||||
<div
|
||||
class="flex size-12 items-center justify-center rounded-full transition-colors"
|
||||
:class="isLeader
|
||||
? 'bg-(--accent) text-(--accent-fg)'
|
||||
: 'bg-(--bg-inset) text-(--fg-subtle)'"
|
||||
? 'bg-accent text-accent-fg'
|
||||
: 'bg-bg-inset text-fg-subtle'"
|
||||
>
|
||||
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m12 2 3 7h7l-5.5 4.5L18.5 21 12 16.5 5.5 21 7.5 13.5 2 9h7z" />
|
||||
@@ -43,11 +43,11 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
|
||||
</div>
|
||||
<p
|
||||
class="text-lg font-bold"
|
||||
:class="isLeader ? 'text-(--accent-text)' : 'text-(--fg)'"
|
||||
:class="isLeader ? 'text-accent-text' : 'text-fg'"
|
||||
>
|
||||
{{ isLeader ? 'Leader tab' : 'Follower tab' }}
|
||||
</p>
|
||||
<p class="text-center text-xs text-(--fg-muted)">
|
||||
<p class="text-center text-xs text-fg-muted">
|
||||
{{ isLeader
|
||||
? 'This tab holds the lock and would run exclusive work.'
|
||||
: 'Another tab is the leader, or leadership was released.' }}
|
||||
@@ -58,7 +58,7 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isSupported || isLeader"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="acquire"
|
||||
>
|
||||
Acquire
|
||||
@@ -66,17 +66,17 @@ const { isLeader, isSupported, acquire, release } = useTabLeader('vue-toolkit-de
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isSupported || !isLeader"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="release"
|
||||
>
|
||||
Release
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="!isSupported" class="text-xs leading-relaxed text-(--fg-subtle)">
|
||||
<p v-if="!isSupported" class="text-xs leading-relaxed text-fg-subtle">
|
||||
The Web Locks API is not available in this browser.
|
||||
</p>
|
||||
<p v-else class="text-xs leading-relaxed text-(--fg-subtle)">
|
||||
<p v-else class="text-xs leading-relaxed text-fg-subtle">
|
||||
Open this page in a second tab — only one tab is the leader at a time. Release
|
||||
here and watch another tab take over.
|
||||
</p>
|
||||
|
||||
@@ -28,9 +28,9 @@ function clear(): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<label class="demo-label">
|
||||
Auto-growing textarea
|
||||
</label>
|
||||
<textarea
|
||||
@@ -38,16 +38,16 @@ function clear(): void {
|
||||
v-model="input"
|
||||
placeholder="Start typing…"
|
||||
rows="1"
|
||||
class="w-full resize-none overflow-y-auto rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm leading-relaxed text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input resize-none overflow-y-auto leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card flex flex-col gap-2 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Max height
|
||||
</span>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg)">{{ maxHeight }}px</span>
|
||||
<span class="font-mono text-sm tabular-nums text-fg">{{ maxHeight }}px</span>
|
||||
</div>
|
||||
<input
|
||||
v-model.number="maxHeight"
|
||||
@@ -55,11 +55,11 @@ function clear(): void {
|
||||
min="80"
|
||||
max="400"
|
||||
step="20"
|
||||
class="w-full accent-(--accent)"
|
||||
class="w-full accent-accent"
|
||||
>
|
||||
<div class="flex items-center justify-between border-t border-(--border) pt-2 text-xs">
|
||||
<span class="text-(--fg-muted)">{{ input.length }} chars</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 font-medium text-(--fg-muted)">
|
||||
<div class="flex items-center justify-between border-t border-border pt-2 text-xs">
|
||||
<span class="text-fg-muted">{{ input.length }} chars</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg-inset px-2 py-0.5 font-medium text-fg-muted">
|
||||
{{ resizes }} resizes
|
||||
</span>
|
||||
</div>
|
||||
@@ -68,21 +68,21 @@ function clear(): void {
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary"
|
||||
@click="loadSample"
|
||||
>
|
||||
Load sample
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="triggerResize"
|
||||
>
|
||||
Trigger resize
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!input"
|
||||
@click="clear"
|
||||
>
|
||||
|
||||
@@ -19,42 +19,42 @@ watch(appName, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4">
|
||||
<span class="demo-label">
|
||||
Live document title
|
||||
</span>
|
||||
<div class="mt-2 flex items-center gap-2 rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2.5">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="size-4 shrink-0 text-(--fg-subtle)">
|
||||
<div class="mt-2 flex items-center gap-2 rounded-lg border border-border bg-bg-inset px-3 py-2.5">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="size-4 shrink-0 text-fg-subtle">
|
||||
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
<span class="truncate font-mono text-sm text-(--fg)">
|
||||
<span class="truncate font-mono text-sm text-fg">
|
||||
{{ title || 'Untitled' }} · {{ appName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<label class="demo-label">
|
||||
Page title
|
||||
</label>
|
||||
<input
|
||||
v-model="title"
|
||||
type="text"
|
||||
placeholder="Enter a page title"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<label class="demo-label">
|
||||
App name (template suffix)
|
||||
</label>
|
||||
<input
|
||||
v-model="appName"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -63,15 +63,15 @@ watch(appName, () => {
|
||||
v-for="preset in presets"
|
||||
:key="preset"
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
:class="title === preset ? 'border-(--accent) text-(--accent-text)' : ''"
|
||||
class="demo-btn"
|
||||
:class="title === preset ? 'border-accent text-accent-text' : ''"
|
||||
@click="title = preset"
|
||||
>
|
||||
{{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Check your browser tab — it updates in real time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -44,21 +44,21 @@ const queryString = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-md flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<label class="demo-label">
|
||||
Search query
|
||||
</label>
|
||||
<input
|
||||
v-model="params.q"
|
||||
type="text"
|
||||
placeholder="Search…"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Sort by</span>
|
||||
<span class="demo-label">Sort by</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="s in sorts"
|
||||
@@ -66,8 +66,8 @@ const queryString = computed(() => {
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="params.sort === s
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="params.sort = s"
|
||||
>
|
||||
{{ s }}
|
||||
@@ -76,7 +76,7 @@ const queryString = computed(() => {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Tags (repeated keys → array)
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@@ -86,8 +86,8 @@ const queryString = computed(() => {
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition cursor-pointer"
|
||||
:class="activeTags.includes(tag)
|
||||
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted) hover:border-(--border-strong)'"
|
||||
? 'border-accent bg-accent-subtle text-accent-text'
|
||||
: 'border-border bg-bg-inset text-fg-muted hover:border-border-strong'"
|
||||
@click="toggleTag(tag)"
|
||||
>
|
||||
#{{ tag }}
|
||||
@@ -96,13 +96,13 @@ const queryString = computed(() => {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Live URL query
|
||||
</span>
|
||||
<div class="overflow-x-auto rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="overflow-x-auto rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
<span class="whitespace-nowrap">{{ queryString }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
The browser address bar updates as you edit. Falsy values are dropped.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -54,6 +54,9 @@ export interface UseUrlSearchParamsOptions<T> extends ConfigurableWindow {
|
||||
stringify?: (params: URLSearchParams) => string;
|
||||
}
|
||||
|
||||
// `Record<string, any>` is the idiomatic "any object shape" constraint here: `T` is
|
||||
// caller-supplied and flows straight back out, so an interface (no implicit index
|
||||
// signature) must still satisfy the bound — `Record<string, unknown>` would reject those.
|
||||
export type UseUrlSearchParamsReturn<T extends Record<string, any> = UrlParams>
|
||||
= T;
|
||||
|
||||
@@ -87,6 +90,8 @@ export type UseUrlSearchParamsReturn<T extends Record<string, any> = UrlParams>
|
||||
*
|
||||
* @since 0.0.15
|
||||
*/
|
||||
// `Record<string, any>` constraint mirrors `UseUrlSearchParamsReturn`: caller-supplied
|
||||
// `T` flows back out, so interface types must satisfy the bound (see note above).
|
||||
export function useUrlSearchParams<T extends Record<string, any> = UrlParams>(
|
||||
mode: UrlSearchParamsMode = 'history',
|
||||
options: UseUrlSearchParamsOptions<T> = {},
|
||||
@@ -104,7 +109,7 @@ export function useUrlSearchParams<T extends Record<string, any> = UrlParams>(
|
||||
if (!window)
|
||||
return reactive({ ...initialValue }) as UseUrlSearchParamsReturn<T>;
|
||||
|
||||
const state = reactive<Record<string, any>>({});
|
||||
const state = reactive<Record<string, string | string[] | null | undefined>>({});
|
||||
|
||||
const getRawParams = (): string => {
|
||||
if (mode === 'history')
|
||||
@@ -160,7 +165,9 @@ export function useUrlSearchParams<T extends Record<string, any> = UrlParams>(
|
||||
else if (removeFalsyValues && !value)
|
||||
params.delete(key);
|
||||
else
|
||||
params.set(key, value);
|
||||
// `set` coerces to string at runtime; do it explicitly so null/undefined (when
|
||||
// not stripped above) match the WHATWG ToString behaviour the old `any` masked.
|
||||
params.set(key, String(value));
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ function toggleLoop(): void {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-300"
|
||||
@@ -40,27 +40,27 @@ function toggleLoop(): void {
|
||||
The Vibration API is not supported in this browser. Try a mobile device.
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Current pattern (ms)
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium"
|
||||
:class="isSupported
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
{{ isSupported ? 'supported' : 'unsupported' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 overflow-x-auto rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="mt-2 overflow-x-auto rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
[{{ Array.isArray(pattern) ? pattern.join(', ') : pattern }}]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Presets</span>
|
||||
<span class="demo-label">Presets</span>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="(_, name) in presets"
|
||||
@@ -69,8 +69,8 @@ function toggleLoop(): void {
|
||||
:disabled="!isSupported"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:class="activePreset === name
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="applyPreset(name)"
|
||||
>
|
||||
{{ name }}
|
||||
@@ -82,7 +82,7 @@ function toggleLoop(): void {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isSupported"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="vibrate()"
|
||||
>
|
||||
Vibrate now
|
||||
@@ -90,7 +90,7 @@ function toggleLoop(): void {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isSupported"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="toggleLoop"
|
||||
>
|
||||
{{ looping ? 'Stop loop' : 'Loop every 1.5s' }}
|
||||
@@ -98,7 +98,7 @@ function toggleLoop(): void {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isSupported"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="stop"
|
||||
>
|
||||
Stop
|
||||
|
||||
@@ -21,7 +21,7 @@ async function toggle(): Promise<void> {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-300"
|
||||
@@ -29,12 +29,12 @@ async function toggle(): Promise<void> {
|
||||
The Screen Wake Lock API is not supported in this browser.
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-6">
|
||||
<div class="demo-card flex flex-col items-center gap-3 p-6">
|
||||
<div
|
||||
class="flex size-16 items-center justify-center rounded-full border transition"
|
||||
:class="isActive
|
||||
? 'border-emerald-500/40 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-subtle)'"
|
||||
: 'border-border bg-bg-inset text-fg-subtle'"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" class="size-7">
|
||||
<rect x="4" y="3" width="16" height="14" rx="2" stroke="currentColor" stroke-width="1.5" />
|
||||
@@ -43,18 +43,18 @@ async function toggle(): Promise<void> {
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
<p class="demo-stat text-3xl">
|
||||
{{ isActive ? 'AWAKE' : 'IDLE' }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-(--fg-muted)">
|
||||
<p class="mt-1 text-xs text-fg-muted">
|
||||
{{ isActive ? 'Screen will stay on' : 'Screen may sleep normally' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2.5 text-sm">
|
||||
<span class="text-(--fg-muted)">Lock held</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-elevated) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset px-3 py-2.5 text-sm">
|
||||
<span class="text-fg-muted">Lock held</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg-elevated px-2 py-0.5 text-xs font-medium text-fg-muted">
|
||||
{{ sentinel ? 'yes' : 'none' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -62,7 +62,7 @@ async function toggle(): Promise<void> {
|
||||
<button
|
||||
type="button"
|
||||
:disabled="!isSupported"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ sentinel ? 'Release wake lock' : 'Request wake lock' }}
|
||||
@@ -71,7 +71,7 @@ async function toggle(): Promise<void> {
|
||||
<p v-if="error" class="text-xs text-red-600 dark:text-red-400">
|
||||
{{ error }}
|
||||
</p>
|
||||
<p v-else class="text-xs text-(--fg-subtle)">
|
||||
<p v-else class="text-xs text-fg-subtle">
|
||||
The lock auto-releases when the tab is hidden and re-acquires when visible again.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ function notify() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-sm text-amber-700 dark:text-amber-400"
|
||||
@@ -48,21 +48,21 @@ function notify() {
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="demo-card flex items-center justify-between p-4">
|
||||
<div>
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-label">
|
||||
Permission
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2 text-sm text-(--fg-muted)">
|
||||
<div class="mt-1 flex items-center gap-2 text-sm text-fg-muted">
|
||||
<span
|
||||
class="inline-block size-2 rounded-full transition"
|
||||
:class="permissionGranted ? 'bg-emerald-500' : 'bg-(--border-strong)'"
|
||||
:class="permissionGranted ? 'bg-emerald-500' : 'bg-border-strong'"
|
||||
/>
|
||||
{{ permissionGranted ? 'Granted' : 'Not granted' }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="permissionGranted"
|
||||
@click="requestPermission"
|
||||
>
|
||||
@@ -72,33 +72,33 @@ function notify() {
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Title</span>
|
||||
<span class="demo-label">Title</span>
|
||||
<input
|
||||
v-model="title"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Body</span>
|
||||
<span class="demo-label">Body</span>
|
||||
<textarea
|
||||
v-model="body"
|
||||
rows="2"
|
||||
class="w-full resize-none rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input resize-none"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn-primary flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!permissionGranted"
|
||||
@click="notify"
|
||||
>
|
||||
Show notification
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!notification"
|
||||
@click="close"
|
||||
>
|
||||
@@ -106,12 +106,12 @@ function notify() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-sm">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Last event</span>
|
||||
<span class="font-mono text-(--fg)">{{ lastEvent || '—' }}</span>
|
||||
<div class="flex items-center justify-between rounded-lg border border-border bg-bg-inset p-3 text-sm">
|
||||
<span class="demo-label">Last event</span>
|
||||
<span class="font-mono text-fg">{{ lastEvent || '—' }}</span>
|
||||
</div>
|
||||
|
||||
<p v-if="!permissionGranted" class="text-xs text-(--fg-subtle)">
|
||||
<p v-if="!permissionGranted" class="text-xs text-fg-subtle">
|
||||
Grant access first, then trigger a notification. Switch back to this tab and it auto-closes.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -26,14 +26,14 @@ function toggle(member: Member) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<!-- Templates are captured here, rendered wherever Reuse* appears -->
|
||||
<DefineStat v-slot="{ label, value }">
|
||||
<div class="flex-1 rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="flex-1 rounded-lg border border-border bg-bg-inset p-3">
|
||||
<div class="demo-label">
|
||||
{{ label }}
|
||||
</div>
|
||||
<div class="mt-1 font-mono text-2xl font-bold tabular-nums text-(--fg)">
|
||||
<div class="demo-stat mt-1 text-2xl">
|
||||
{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,14 +43,14 @@ function toggle(member: Member) {
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="inline-block size-2 shrink-0 rounded-full transition"
|
||||
:class="online ? 'bg-emerald-500' : 'bg-(--border-strong)'"
|
||||
:class="online ? 'bg-emerald-500' : 'bg-border-strong'"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-medium text-(--fg)">{{ name }}</div>
|
||||
<div class="text-xs text-(--fg-subtle)">{{ role }}</div>
|
||||
<div class="truncate text-sm font-medium text-fg">{{ name }}</div>
|
||||
<div class="text-xs text-fg-subtle">{{ role }}</div>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
class="demo-badge"
|
||||
>
|
||||
{{ online ? 'Online' : 'Away' }}
|
||||
</span>
|
||||
@@ -62,14 +62,14 @@ function toggle(member: Member) {
|
||||
<ReuseStat label="Online" :value="String(team.filter(m => m.online).length)" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-card flex flex-col gap-3 p-4">
|
||||
<div class="demo-label">
|
||||
Team — click a row to toggle status
|
||||
</div>
|
||||
<button
|
||||
v-for="member in team"
|
||||
:key="member.name"
|
||||
class="rounded-lg p-2 text-left transition hover:bg-(--bg-inset) active:scale-[0.99] cursor-pointer"
|
||||
class="rounded-lg p-2 text-left transition hover:bg-bg-inset active:scale-[0.99] cursor-pointer"
|
||||
@click="toggle(member)"
|
||||
>
|
||||
<ReuseMember
|
||||
@@ -80,7 +80,7 @@ function toggle(member: Member) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Both card and row markup are declared once via <code class="font-mono">DefineTemplate</code> and
|
||||
rendered from multiple <code class="font-mono">ReuseTemplate</code> call sites.
|
||||
</p>
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import type { ComponentObjectPropsOptions, DefineComponent, Slot } from 'vue';
|
||||
import { camelize, defineComponent, shallowRef } from 'vue';
|
||||
|
||||
/** Map of slot name -> slot props object (or `undefined` for prop-less slots) */
|
||||
/**
|
||||
* Map of slot name -> slot props object (or `undefined` for prop-less slots).
|
||||
* The inner `Record<string, any>` is the idiomatic "any slot-props shape" bound:
|
||||
* interface-typed slot props (which lack an implicit index signature) must satisfy
|
||||
* it, so `Record<string, unknown>` would wrongly reject legitimate callers.
|
||||
*/
|
||||
type SlotPropsMap = Record<string, Record<string, any> | undefined>;
|
||||
|
||||
/** Turn a {@link SlotPropsMap} into a record of typed `Slot`s */
|
||||
type GenerateSlotsFromSlotMap<T extends SlotPropsMap>
|
||||
= { [K in keyof T]: Slot<T[K]> };
|
||||
|
||||
// `Bindings extends Record<string, any>` is the idiomatic "any object shape" bound,
|
||||
// matching Vue/Reka's own component-binding generics: an interface-typed `Bindings`
|
||||
// must satisfy it, which `Record<string, unknown>` would reject. Applies to every
|
||||
// `extends Record<string, any>` constraint in this file.
|
||||
export type DefineTemplateComponent<Bindings extends Record<string, any>, Slots extends SlotPropsMap>
|
||||
= DefineComponent & (new () => {
|
||||
$slots: {
|
||||
// Slot render fn: returns `any` to match Vue's own `Slot` return type.
|
||||
default: (_: Bindings & { $slots: GenerateSlotsFromSlotMap<Slots> }) => any;
|
||||
};
|
||||
});
|
||||
@@ -49,8 +59,8 @@ export interface CreateReusableTemplateOptions<Props extends Record<string, any>
|
||||
}
|
||||
|
||||
/** Re-key an attrs object so every key is camelCased */
|
||||
function keysToCamelCase(obj: Record<string, any>): Record<string, any> {
|
||||
const result: Record<string, any> = {};
|
||||
function keysToCamelCase(obj: Record<string, unknown>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const key in obj)
|
||||
result[camelize(key)] = obj[key];
|
||||
|
||||
@@ -21,10 +21,10 @@ function measure() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
ref="box"
|
||||
class="flex items-center justify-center rounded-xl border border-dashed border-(--border-strong) bg-(--bg-inset) py-8 text-sm font-medium text-(--fg-muted) transition-[width] duration-300 ease-out"
|
||||
class="flex items-center justify-center rounded-xl border border-dashed border-border-strong bg-bg-inset py-8 text-sm font-medium text-fg-muted transition-[width] duration-300 ease-out"
|
||||
:style="{ width: `${width}px` }"
|
||||
>
|
||||
Target element
|
||||
@@ -32,37 +32,37 @@ function measure() {
|
||||
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Width</span>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg-muted)">{{ width }}px</span>
|
||||
<span class="demo-label">Width</span>
|
||||
<span class="font-mono text-sm tabular-nums text-fg-muted">{{ width }}px</span>
|
||||
</div>
|
||||
<input
|
||||
v-model.number="width"
|
||||
type="range"
|
||||
min="120"
|
||||
max="340"
|
||||
class="w-full accent-(--accent)"
|
||||
class="w-full accent-accent"
|
||||
>
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary"
|
||||
@click="measure"
|
||||
>
|
||||
Read element via unrefElement
|
||||
</button>
|
||||
|
||||
<div class="flex flex-col gap-2 rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="flex flex-col gap-2 rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-(--fg-subtle)">tagName</span>
|
||||
<span class="text-fg-subtle">tagName</span>
|
||||
<span>{{ tag }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-(--fg-subtle)">boundingRect</span>
|
||||
<span class="text-fg-subtle">boundingRect</span>
|
||||
<span>{{ rect ? `${rect.w} × ${rect.h}` : '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Resize the box, then measure. <code class="font-mono">unrefElement</code> unwraps the template
|
||||
ref to the real DOM node — it also resolves a component ref to its <code class="font-mono">$el</code>.
|
||||
</p>
|
||||
|
||||
@@ -29,10 +29,10 @@ const chips = ['vue', 'reactivity', 'composables', 'ssr', 'typescript', 'dom'];
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex w-full max-w-sm flex-col gap-4 rounded-xl border border-(--border) bg-(--bg-elevated)"
|
||||
class="demo-stack demo-card max-w-sm"
|
||||
:style="{ padding: `${padding}px` }"
|
||||
>
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-label">
|
||||
Live measurement of this component's root
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ const chips = ['vue', 'reactivity', 'composables', 'ssr', 'typescript', 'dom'];
|
||||
v-for="chip in chips.slice(0, childCount)"
|
||||
:key="chip"
|
||||
data-chip
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
class="demo-badge"
|
||||
>
|
||||
{{ chip }}
|
||||
</span>
|
||||
@@ -49,28 +49,28 @@ const chips = ['vue', 'reactivity', 'composables', 'ssr', 'typescript', 'dom'];
|
||||
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Root padding</span>
|
||||
<span class="font-mono text-sm tabular-nums text-(--fg-muted)">{{ padding }}px</span>
|
||||
<span class="demo-label">Root padding</span>
|
||||
<span class="font-mono text-sm tabular-nums text-fg-muted">{{ padding }}px</span>
|
||||
</div>
|
||||
<input
|
||||
v-model.number="padding"
|
||||
type="range"
|
||||
min="8"
|
||||
max="40"
|
||||
class="w-full accent-(--accent)"
|
||||
class="w-full accent-accent"
|
||||
>
|
||||
</label>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="childCount <= 1"
|
||||
@click="childCount--"
|
||||
>
|
||||
Remove chip
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn flex-1 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="childCount >= chips.length"
|
||||
@click="childCount++"
|
||||
>
|
||||
@@ -78,22 +78,22 @@ const chips = ['vue', 'reactivity', 'composables', 'ssr', 'typescript', 'dom'];
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="flex flex-col gap-2 rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-(--fg-subtle)">el.value</span>
|
||||
<span class="text-fg-subtle">el.value</span>
|
||||
<span>{{ info ? `<${info.tag}>` : 'undefined' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-(--fg-subtle)">chips in DOM</span>
|
||||
<span class="text-fg-subtle">chips in DOM</span>
|
||||
<span>{{ info?.children ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-(--fg-subtle)">root height</span>
|
||||
<span class="text-fg-subtle">root height</span>
|
||||
<span>{{ info ? `${info.height}px` : '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
The computed re-reads <code class="font-mono">$el</code> on every update, so the readout tracks
|
||||
padding and chip changes automatically.
|
||||
</p>
|
||||
|
||||
@@ -19,7 +19,7 @@ const FieldWrapper = defineComponent({
|
||||
// expose the resolved element so the demo can show it changed live
|
||||
'data-tag': currentElement.value?.tagName.toLowerCase(),
|
||||
class:
|
||||
'w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)',
|
||||
'w-full rounded-lg border border-border bg-bg px-3 py-2 text-sm text-fg placeholder:text-fg-subtle transition focus:border-accent focus:outline-none focus:ring-2 focus:ring-ring',
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -49,13 +49,13 @@ function fillSample() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="flex flex-col gap-3 rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card flex flex-col gap-3 p-4">
|
||||
<div class="demo-label">
|
||||
Wrapper component
|
||||
</div>
|
||||
<FieldWrapper ref="field" />
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
<code class="font-mono"><FieldWrapper></code> renders an inner input, but
|
||||
<code class="font-mono">useForwardExpose</code> makes its <code class="font-mono">$el</code>
|
||||
resolve to that input.
|
||||
@@ -64,31 +64,31 @@ function fillSample() {
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary flex-1"
|
||||
@click="focusForwarded"
|
||||
>
|
||||
Focus forwarded $el
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="fillSample"
|
||||
>
|
||||
Fill sample
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="flex flex-col gap-2 rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-(--fg-subtle)">field.$el</span>
|
||||
<span class="text-fg-subtle">field.$el</span>
|
||||
<span>{{ resolved ? `<${resolved.tag}>` : 'undefined' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-(--fg-subtle)">value</span>
|
||||
<span class="text-fg-subtle">value</span>
|
||||
<span class="truncate">{{ resolved?.value || '""' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
The parent never touches the input directly — it holds a ref to the wrapper, whose
|
||||
<code class="font-mono">$el</code> is forwarded to the inner element.
|
||||
</p>
|
||||
|
||||
@@ -53,7 +53,7 @@ export function useForwardExpose<T extends ComponentPublicInstance>(): UseForwar
|
||||
|
||||
// localExpose should only be assigned once else will create infinite loop
|
||||
const localExpose = instance.exposed;
|
||||
const ret: Record<string, any> = {};
|
||||
const ret: Record<string, unknown> = {};
|
||||
|
||||
// Collect all property descriptors in a single pass
|
||||
const descriptors: PropertyDescriptorMap = {};
|
||||
|
||||
@@ -50,62 +50,62 @@ const collected = computed(() => refs.value.length);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Playlist</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-label">Playlist</span>
|
||||
<span class="demo-badge">
|
||||
{{ collected }} refs collected
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul class="flex flex-col gap-2 max-h-56 overflow-y-auto rounded-xl border border-(--border) bg-(--bg-elevated) p-2">
|
||||
<ul class="demo-card flex flex-col gap-2 max-h-56 overflow-y-auto p-2">
|
||||
<li
|
||||
v-for="(track, index) in tracks"
|
||||
:key="track.id"
|
||||
:ref="set"
|
||||
class="group flex items-center gap-3 rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2 transition"
|
||||
:class="lastMeasured?.index === index ? 'border-(--accent) ring-2 ring-(--ring)' : 'hover:border-(--border-strong)'"
|
||||
class="group flex items-center gap-3 rounded-lg border border-border bg-bg-inset px-3 py-2 transition"
|
||||
:class="lastMeasured?.index === index ? 'border-accent ring-2 ring-ring' : 'hover:border-border-strong'"
|
||||
>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-subtle) w-5 text-right">{{ index + 1 }}</span>
|
||||
<span class="font-mono text-xs tabular-nums text-fg-subtle w-5 text-right">{{ index + 1 }}</span>
|
||||
<span class="flex min-w-0 flex-1 flex-col">
|
||||
<span class="truncate text-sm font-medium text-(--fg)">{{ track.title }}</span>
|
||||
<span class="truncate text-xs text-(--fg-muted)">{{ track.artist }}</span>
|
||||
<span class="truncate text-sm font-medium text-fg">{{ track.title }}</span>
|
||||
<span class="truncate text-xs text-fg-muted">{{ track.artist }}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Remove track"
|
||||
class="rounded-md px-1.5 py-0.5 text-xs text-(--fg-subtle) opacity-0 transition hover:text-(--fg) group-hover:opacity-100 cursor-pointer"
|
||||
class="rounded-md px-1.5 py-0.5 text-xs text-fg-subtle opacity-0 transition hover:text-fg group-hover:opacity-100 cursor-pointer"
|
||||
@click="removeTrack(track.id)"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="tracks.length === 0" class="px-3 py-6 text-center text-sm text-(--fg-subtle)">
|
||||
<li v-if="tracks.length === 0" class="px-3 py-6 text-center text-sm text-fg-subtle">
|
||||
No tracks — add one to collect a ref.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div
|
||||
v-if="lastMeasured"
|
||||
class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums"
|
||||
class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums"
|
||||
>
|
||||
refs[{{ lastMeasured.index }}].width = {{ lastMeasured.width }}px
|
||||
</div>
|
||||
<p v-else class="text-xs text-(--fg-subtle)">
|
||||
<p v-else class="text-xs text-fg-subtle">
|
||||
Add a track to measure the newest collected element directly from the DOM.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary flex-1"
|
||||
@click="addTrack"
|
||||
>
|
||||
Add track
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="collected === 0"
|
||||
@click="measureLast"
|
||||
>
|
||||
|
||||
@@ -36,54 +36,54 @@ const visibleRange = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Virtual list</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-label">Virtual list</span>
|
||||
<span class="demo-badge">
|
||||
{{ total.toLocaleString() }} rows
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-bind="containerProps"
|
||||
class="h-64 rounded-xl border border-(--border) bg-(--bg-elevated)"
|
||||
class="demo-card h-64"
|
||||
>
|
||||
<div v-bind="wrapperProps">
|
||||
<div
|
||||
v-for="{ data, index } in list"
|
||||
:key="index"
|
||||
class="flex items-center gap-3 border-b border-(--border) px-3"
|
||||
class="flex items-center gap-3 border-b border-border px-3"
|
||||
:style="{ height: `${itemHeight}px` }"
|
||||
>
|
||||
<span
|
||||
class="size-6 shrink-0 rounded-md border border-(--border)"
|
||||
class="size-6 shrink-0 rounded-md border border-border"
|
||||
:style="{ backgroundColor: `hsl(${data.hue} 65% 55%)` }"
|
||||
/>
|
||||
<span class="flex-1 truncate font-mono text-sm text-(--fg) tabular-nums">{{ data.label }}</span>
|
||||
<span class="text-xs text-(--fg-subtle)">idx {{ index }}</span>
|
||||
<span class="flex-1 truncate font-mono text-sm text-fg tabular-nums">{{ data.label }}</span>
|
||||
<span class="text-xs text-fg-subtle">idx {{ index }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums flex items-center justify-between">
|
||||
<span class="text-(--fg-muted)">rendered</span>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums flex items-center justify-between">
|
||||
<span class="text-fg-muted">rendered</span>
|
||||
<span>{{ list.length }} nodes · idx {{ visibleRange }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end gap-2">
|
||||
<label class="flex flex-1 flex-col gap-1">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Scroll to index</span>
|
||||
<span class="demo-label">Scroll to index</span>
|
||||
<input
|
||||
v-model.number="jumpTo"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="total - 1"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-2 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-accent px-3 py-2 text-sm font-medium text-accent-fg transition hover:bg-accent-hover active:scale-[0.98] cursor-pointer"
|
||||
@click="go"
|
||||
>
|
||||
Jump
|
||||
|
||||
@@ -200,7 +200,7 @@ function createMetrics(length: number, itemSize: UseVirtualListItemSize): UseVir
|
||||
*
|
||||
* @since 0.0.15
|
||||
*/
|
||||
export function useVirtualList<T = any>(
|
||||
export function useVirtualList<T = unknown>(
|
||||
list: MaybeRefOrGetter<readonly T[]>,
|
||||
options: UseVirtualListOptions,
|
||||
): UseVirtualListReturn<T> {
|
||||
|
||||
@@ -15,33 +15,33 @@ function nudgeTint() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col items-center gap-2">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Render count</span>
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4 flex flex-col items-center gap-2">
|
||||
<span class="demo-label">Render count</span>
|
||||
<span
|
||||
class="font-mono text-3xl font-bold tabular-nums text-(--fg) transition-colors"
|
||||
class="demo-stat text-3xl transition-colors"
|
||||
:style="{ color: `hsl(${tint} 70% 55%)` }"
|
||||
>{{ renderCount }}</span>
|
||||
<span class="text-xs text-(--fg-subtle)">renders since mount</span>
|
||||
<span class="text-xs text-fg-subtle">renders since mount</span>
|
||||
</div>
|
||||
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Bound input</span>
|
||||
<span class="demo-label">Bound input</span>
|
||||
<input
|
||||
v-model="message"
|
||||
type="text"
|
||||
placeholder="Type to re-render…"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</label>
|
||||
|
||||
<p class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-sm text-(--fg)">
|
||||
<p class="rounded-lg border border-border bg-bg-inset p-3 text-sm text-fg">
|
||||
{{ message }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="nudgeTint"
|
||||
>
|
||||
Force re-render (shift color)
|
||||
|
||||
@@ -14,34 +14,34 @@ const mountedAt = new Date(lastRendered).toLocaleTimeString();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Render info</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-label">Render info</span>
|
||||
<span class="demo-badge">
|
||||
{{ component ?? 'anonymous' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
|
||||
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ count }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">renders</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
|
||||
<div class="demo-stat text-3xl">{{ count }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">renders</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
|
||||
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ duration.toFixed(2) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">last render ms</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
|
||||
<div class="demo-stat text-3xl">{{ duration.toFixed(2) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">last render ms</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums flex items-center justify-between">
|
||||
<span class="text-(--fg-muted)">mounted at</span>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums flex items-center justify-between">
|
||||
<span class="text-fg-muted">mounted at</span>
|
||||
<span>{{ mountedAt }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="rows">Render workload</label>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ rows }} cells</span>
|
||||
<label class="demo-label" for="rows">Render workload</label>
|
||||
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ rows }} cells</span>
|
||||
</div>
|
||||
<input
|
||||
id="rows"
|
||||
@@ -50,16 +50,16 @@ const mountedAt = new Date(lastRendered).toLocaleTimeString();
|
||||
min="1"
|
||||
max="400"
|
||||
step="1"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
class="w-full accent-accent cursor-pointer"
|
||||
>
|
||||
<p class="text-xs text-(--fg-subtle)">Drag to re-render a larger DOM subtree and watch the render duration climb.</p>
|
||||
<p class="text-xs text-fg-subtle">Drag to re-render a larger DOM subtree and watch the render duration climb.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-10 gap-1">
|
||||
<span
|
||||
v-for="i in grid"
|
||||
:key="i"
|
||||
class="aspect-square rounded-sm bg-(--accent-subtle)"
|
||||
class="aspect-square rounded-sm bg-accent-subtle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,51 +27,51 @@ function reset() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">DOM removal watcher</span>
|
||||
<span class="demo-label">DOM removal watcher</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
class="demo-badge"
|
||||
>
|
||||
<span class="size-1.5 rounded-full transition" :class="mounted ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
<span class="size-1.5 rounded-full transition" :class="mounted ? 'bg-emerald-500' : 'bg-fg-subtle'" />
|
||||
{{ mounted ? 'In DOM' : 'Removed' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 min-h-28 flex items-center justify-center">
|
||||
<div class="demo-card p-4 min-h-28 flex items-center justify-center">
|
||||
<div
|
||||
v-if="mounted"
|
||||
ref="watched"
|
||||
class="flex w-full flex-col items-center gap-1 rounded-lg border border-(--accent) bg-(--accent-subtle) px-4 py-6 text-center transition"
|
||||
class="flex w-full flex-col items-center gap-1 rounded-lg border border-accent bg-accent-subtle px-4 py-6 text-center transition"
|
||||
>
|
||||
<span class="text-sm font-medium text-(--accent-text)">Watched element</span>
|
||||
<span class="text-xs text-(--fg-muted)">Remove me and the callback fires</span>
|
||||
<span class="text-sm font-medium text-accent-text">Watched element</span>
|
||||
<span class="text-xs text-fg-muted">Remove me and the callback fires</span>
|
||||
</div>
|
||||
<span v-else class="text-sm text-(--fg-subtle)">Element detached from the document</span>
|
||||
<span v-else class="text-sm text-fg-subtle">Element detached from the document</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
|
||||
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ removals }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">removals fired</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
|
||||
<div class="demo-stat text-3xl">{{ removals }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">removals fired</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center flex flex-col justify-center">
|
||||
<div class="font-mono text-sm font-medium tabular-nums text-(--fg)">{{ lastEvent ?? '—' }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">last fired</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center flex flex-col justify-center">
|
||||
<div class="font-mono text-sm font-medium tabular-nums text-fg">{{ lastEvent ?? '—' }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">last fired</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn-primary flex-1"
|
||||
@click="toggle"
|
||||
>
|
||||
{{ mounted ? 'Remove element' : 'Mount element' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="removals === 0"
|
||||
@click="reset"
|
||||
>
|
||||
|
||||
@@ -15,9 +15,9 @@ const activeTag = computed(() => activeElement.value?.tagName.toLowerCase() ?? n
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="space-y-3">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<p class="demo-label">
|
||||
Focus a field
|
||||
</p>
|
||||
<div
|
||||
@@ -27,39 +27,39 @@ const activeTag = computed(() => activeElement.value?.tagName.toLowerCase() ?? n
|
||||
>
|
||||
<label
|
||||
:for="field.id"
|
||||
class="text-sm font-medium text-(--fg-muted)"
|
||||
class="text-sm font-medium text-fg-muted"
|
||||
>{{ field.label }}</label>
|
||||
<input
|
||||
:id="field.id"
|
||||
:type="field.type"
|
||||
:placeholder="field.placeholder"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="demo-input"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
>
|
||||
A focusable button
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 font-mono text-sm text-fg tabular-nums">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-(--fg-subtle)">activeElement</span>
|
||||
<span class="text-fg-subtle">activeElement</span>
|
||||
<span
|
||||
v-if="activeTag"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-elevated) px-2 py-0.5 text-xs font-medium text-(--accent-text)"
|
||||
class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg-elevated px-2 py-0.5 text-xs font-medium text-accent-text"
|
||||
>
|
||||
<{{ activeTag }}>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-xs text-(--fg-subtle)"
|
||||
class="text-xs text-fg-subtle"
|
||||
>none</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center justify-between gap-3">
|
||||
<span class="text-(--fg-subtle)">id</span>
|
||||
<span class="text-fg-subtle">id</span>
|
||||
<span class="truncate">{{ activeId ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,9 +14,9 @@ const activeIndex = computed(() => stages.findIndex(s => s.state === readyState.
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
document.readyState
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
|
||||
@@ -31,24 +31,24 @@ const activeIndex = computed(() => stages.findIndex(s => s.state === readyState.
|
||||
:key="stage.state"
|
||||
class="flex items-center gap-3 rounded-lg border p-3 transition"
|
||||
:class="i <= activeIndex
|
||||
? 'border-(--accent) bg-(--accent-subtle)'
|
||||
: 'border-(--border) bg-(--bg-elevated)'"
|
||||
? 'border-accent bg-accent-subtle'
|
||||
: 'border-border bg-bg-elevated'"
|
||||
>
|
||||
<span
|
||||
class="flex size-6 shrink-0 items-center justify-center rounded-full font-mono text-xs font-bold tabular-nums transition"
|
||||
:class="i < activeIndex
|
||||
? 'bg-(--accent) text-(--accent-fg)'
|
||||
? 'bg-accent text-accent-fg'
|
||||
: i === activeIndex
|
||||
? 'bg-(--accent) text-(--accent-fg) ring-4 ring-(--ring)'
|
||||
: 'bg-(--bg-inset) text-(--fg-subtle)'"
|
||||
? 'bg-accent text-accent-fg ring-4 ring-ring'
|
||||
: 'bg-bg-inset text-fg-subtle'"
|
||||
>
|
||||
{{ i + 1 }}
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<p class="font-mono text-sm font-medium text-(--fg)">
|
||||
<p class="font-mono text-sm font-medium text-fg">
|
||||
{{ stage.label }}
|
||||
</p>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
{{ stage.hint }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -18,8 +18,8 @@ const isVisible = computed(() => visibility.value === 'visible');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col items-center gap-3 text-center">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="demo-card p-4 flex flex-col items-center gap-3 text-center">
|
||||
<span
|
||||
class="flex size-14 items-center justify-center rounded-full text-2xl transition"
|
||||
:class="isVisible
|
||||
@@ -29,33 +29,33 @@ const isVisible = computed(() => visibility.value === 'visible');
|
||||
{{ isVisible ? '👁️' : '💤' }}
|
||||
</span>
|
||||
<div>
|
||||
<p class="font-mono text-2xl font-bold tabular-nums text-(--fg)">
|
||||
<p class="demo-stat text-2xl">
|
||||
{{ visibility }}
|
||||
</p>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
{{ isVisible ? 'This tab is in the foreground' : 'This tab is hidden' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-(--fg-subtle)">
|
||||
<p class="text-center text-xs text-fg-subtle">
|
||||
Switch to another tab or minimize the window to watch this update.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
|
||||
<p class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
|
||||
<p class="demo-stat text-3xl">
|
||||
{{ switches }}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<p class="demo-label mt-0.5">
|
||||
Times hidden
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
|
||||
<p class="font-mono text-sm font-medium tabular-nums text-(--fg) truncate">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
|
||||
<p class="font-mono text-sm font-medium tabular-nums text-fg truncate">
|
||||
{{ lastHidden ?? '—' }}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<p class="demo-label mt-0.5">
|
||||
Last hidden at
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -16,20 +16,20 @@ const { x, y, isDragging, style } = useDraggable(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Draggable, clamped to container
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
|
||||
:class="isDragging
|
||||
? 'border-(--accent) bg-(--accent-subtle) text-(--accent-text)'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
? 'border-accent bg-accent-subtle text-accent-text'
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
<span
|
||||
class="size-1.5 rounded-full transition"
|
||||
:class="isDragging ? 'bg-(--accent)' : 'bg-(--fg-subtle)'"
|
||||
:class="isDragging ? 'bg-accent' : 'bg-fg-subtle'"
|
||||
/>
|
||||
{{ isDragging ? 'dragging' : 'idle' }}
|
||||
</span>
|
||||
@@ -37,29 +37,29 @@ const { x, y, isDragging, style } = useDraggable(
|
||||
|
||||
<div
|
||||
ref="container"
|
||||
class="relative h-56 w-full overflow-hidden rounded-xl border border-(--border) bg-(--bg-inset)"
|
||||
class="relative h-56 w-full overflow-hidden rounded-xl border border-border bg-bg-inset"
|
||||
>
|
||||
<div
|
||||
ref="card"
|
||||
:style="style"
|
||||
class="absolute w-36 select-none rounded-lg border border-(--border-strong) bg-(--bg-elevated) shadow-lg"
|
||||
:class="isDragging ? 'ring-2 ring-(--ring)' : ''"
|
||||
class="absolute w-36 select-none rounded-lg border border-border-strong bg-bg-elevated shadow-lg"
|
||||
:class="isDragging ? 'ring-2 ring-ring' : ''"
|
||||
>
|
||||
<div
|
||||
ref="handle"
|
||||
class="flex items-center gap-1.5 rounded-t-lg border-b border-(--border) bg-(--bg-subtle) px-3 py-2 cursor-grab active:cursor-grabbing"
|
||||
class="flex items-center gap-1.5 rounded-t-lg border-b border-border bg-bg-subtle px-3 py-2 cursor-grab active:cursor-grabbing"
|
||||
>
|
||||
<span class="text-(--fg-subtle)">⠿</span>
|
||||
<span class="text-xs font-medium text-(--fg-muted)">Drag me</span>
|
||||
<span class="text-fg-subtle">⠿</span>
|
||||
<span class="text-xs font-medium text-fg-muted">Drag me</span>
|
||||
</div>
|
||||
<div class="p-3 font-mono text-xs tabular-nums text-(--fg)">
|
||||
<div class="p-3 font-mono text-xs tabular-nums text-fg">
|
||||
<div>x: {{ Math.round(x) }}</div>
|
||||
<div>y: {{ Math.round(y) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-(--fg-subtle)">
|
||||
<p class="text-center text-xs text-fg-subtle">
|
||||
Drag from the header. Movement is clamped to the container bounds.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { computed, shallowRef, toValue } from 'vue';
|
||||
import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
|
||||
import { noop } from '@robonen/stdlib';
|
||||
import { clamp, noop } from '@robonen/stdlib';
|
||||
import { defaultWindow } from '@/types';
|
||||
import { useEventListener } from '@/composables/browser/useEventListener';
|
||||
import { unrefElement } from '@/composables/component/unrefElement';
|
||||
@@ -259,13 +259,13 @@ export function useDraggable(
|
||||
if (axis === 'x' || axis === 'both') {
|
||||
x = event.clientX - pressedDelta.value.x;
|
||||
if (container && targetRect)
|
||||
x = Math.min(Math.max(0, x), container.scrollWidth - targetRect.width);
|
||||
x = clamp(x, 0, container.scrollWidth - targetRect.width);
|
||||
}
|
||||
|
||||
if (axis === 'y' || axis === 'both') {
|
||||
y = event.clientY - pressedDelta.value.y;
|
||||
if (container && targetRect)
|
||||
y = Math.min(Math.max(0, y), container.scrollHeight - targetRect.height);
|
||||
y = clamp(y, 0, container.scrollHeight - targetRect.height);
|
||||
}
|
||||
|
||||
position.value = { x, y };
|
||||
|
||||
@@ -24,7 +24,7 @@ function formatSize(bytes: number): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div
|
||||
v-if="!isSupported"
|
||||
class="rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-center text-sm text-amber-700 dark:text-amber-300"
|
||||
@@ -37,33 +37,33 @@ function formatSize(bytes: number): string {
|
||||
ref="dropZone"
|
||||
class="flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed p-8 text-center transition"
|
||||
:class="isOverDropZone
|
||||
? 'border-(--accent) bg-(--accent-subtle)'
|
||||
: 'border-(--border-strong) bg-(--bg-elevated)'"
|
||||
? 'border-accent bg-accent-subtle'
|
||||
: 'border-border-strong bg-bg-elevated'"
|
||||
>
|
||||
<span class="text-3xl transition" :class="isOverDropZone ? 'scale-110' : ''">
|
||||
{{ isOverDropZone ? '📥' : '🖼️' }}
|
||||
</span>
|
||||
<p class="text-sm font-medium text-(--fg)">
|
||||
<p class="text-sm font-medium text-fg">
|
||||
{{ isOverDropZone ? 'Release to drop' : 'Drop image files here' }}
|
||||
</p>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
Only <span class="font-mono">image/*</span> files are accepted
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
<span class="demo-label">
|
||||
Dropped files
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-elevated) px-2 py-0.5 text-xs font-medium text-(--fg-muted) tabular-nums">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-border bg-bg-elevated px-2 py-0.5 text-xs font-medium text-fg-muted tabular-nums">
|
||||
{{ fileList.length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="fileList.length === 0"
|
||||
class="mt-3 text-center text-sm text-(--fg-subtle)"
|
||||
class="mt-3 text-center text-sm text-fg-subtle"
|
||||
>
|
||||
Nothing dropped yet.
|
||||
</p>
|
||||
@@ -72,10 +72,10 @@ function formatSize(bytes: number): string {
|
||||
<li
|
||||
v-for="file in fileList"
|
||||
:key="file.name"
|
||||
class="flex items-center justify-between gap-3 rounded-md bg-(--bg-elevated) px-2.5 py-1.5"
|
||||
class="flex items-center justify-between gap-3 rounded-md bg-bg-elevated px-2.5 py-1.5"
|
||||
>
|
||||
<span class="truncate text-sm text-(--fg)">{{ file.name }}</span>
|
||||
<span class="shrink-0 font-mono text-xs tabular-nums text-(--fg-subtle)">
|
||||
<span class="truncate text-sm text-fg">{{ file.name }}</span>
|
||||
<span class="shrink-0 font-mono text-xs tabular-nums text-fg-subtle">
|
||||
{{ formatSize(file.size) }}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -34,20 +34,20 @@ function fmt(n: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="demo-card p-4 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">getBoundingClientRect</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="demo-label">getBoundingClientRect</span>
|
||||
<span class="demo-badge">
|
||||
{{ timing }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- The measured target. Resizing it mutates the reactive bounds. -->
|
||||
<div class="relative h-40 overflow-hidden rounded-lg border border-(--border) bg-(--bg-inset)">
|
||||
<div class="relative h-40 overflow-hidden rounded-lg border border-border bg-bg-inset">
|
||||
<div
|
||||
ref="target"
|
||||
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 grid place-items-center rounded-lg bg-(--accent) text-(--accent-fg) shadow transition-[width,height] duration-150"
|
||||
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 grid place-items-center rounded-lg bg-accent text-accent-fg shadow transition-[width,height] duration-150"
|
||||
:style="{ width: `${size}px`, height: `${size}px` }"
|
||||
>
|
||||
<span class="font-mono text-xs font-semibold tabular-nums">{{ fmt(bounds.width.value) }}×{{ fmt(bounds.height.value) }}</span>
|
||||
@@ -58,18 +58,18 @@ function fmt(n: number) {
|
||||
<div
|
||||
v-for="m in metrics"
|
||||
:key="m.label"
|
||||
class="rounded-lg border border-(--border) bg-(--bg-inset) p-2 text-center"
|
||||
class="rounded-lg border border-border bg-bg-inset p-2 text-center"
|
||||
>
|
||||
<div class="font-mono text-sm font-bold tabular-nums text-(--fg)">{{ fmt(m.value) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">{{ m.label }}</div>
|
||||
<div class="demo-stat text-sm">{{ fmt(m.value) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">{{ m.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="bound-size">Size</label>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ size }}px</span>
|
||||
<label class="demo-label" for="bound-size">Size</label>
|
||||
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ size }}px</span>
|
||||
</div>
|
||||
<input
|
||||
id="bound-size"
|
||||
@@ -78,7 +78,7 @@ function fmt(n: number) {
|
||||
min="32"
|
||||
max="140"
|
||||
step="2"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
class="w-full accent-accent cursor-pointer"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -89,22 +89,22 @@ function fmt(n: number) {
|
||||
type="button"
|
||||
class="flex-1 inline-flex items-center justify-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition active:scale-[0.98] cursor-pointer"
|
||||
:class="timing === opt
|
||||
? 'border-transparent bg-(--accent) text-(--accent-fg) hover:bg-(--accent-hover)'
|
||||
: 'border-(--border) bg-(--bg-elevated) text-(--fg) hover:bg-(--bg-inset) hover:border-(--border-strong)'"
|
||||
? 'border-transparent bg-accent text-accent-fg hover:bg-accent-hover'
|
||||
: 'border-border bg-bg-elevated text-fg hover:bg-bg-inset hover:border-border-strong'"
|
||||
@click="timing = opt"
|
||||
>
|
||||
{{ opt }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="bounds.update()"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
<span class="font-mono">next-frame</span> batches rapid scroll/resize reads into one measurement per animation frame.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -25,12 +25,12 @@ function fmt(n: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-md">
|
||||
<div class="demo-card p-4 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">ResizeObserver</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="size-1.5 rounded-full" :class="observing ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
<span class="demo-label">ResizeObserver</span>
|
||||
<span class="demo-badge">
|
||||
<span class="size-1.5 rounded-full" :class="observing ? 'bg-emerald-500' : 'bg-fg-subtle'" />
|
||||
{{ observing ? 'Observing' : 'Stopped' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -40,25 +40,25 @@ function fmt(n: number) {
|
||||
ref="target"
|
||||
readonly
|
||||
:style="{ padding: `${padding}px` }"
|
||||
class="w-full min-h-24 resize rounded-lg border border-(--border-strong) bg-(--bg-inset) text-sm leading-relaxed text-(--fg-muted) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
class="w-full min-h-24 resize rounded-lg border border-border-strong bg-bg-inset text-sm leading-relaxed text-fg-muted focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>Drag the bottom-right corner to resize me. The width and height update live as the ResizeObserver fires. Border-box sizing includes the padding below.</textarea>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
|
||||
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ fmt(width) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">width px</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
|
||||
<div class="demo-stat text-3xl">{{ fmt(width) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">width px</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
|
||||
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ fmt(height) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">height px</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
|
||||
<div class="demo-stat text-3xl">{{ fmt(height) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">height px</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="size-padding">Padding (border-box)</label>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ padding }}px</span>
|
||||
<label class="demo-label" for="size-padding">Padding (border-box)</label>
|
||||
<span class="font-mono text-xs tabular-nums text-fg-muted">{{ padding }}px</span>
|
||||
</div>
|
||||
<input
|
||||
id="size-padding"
|
||||
@@ -67,20 +67,20 @@ function fmt(n: number) {
|
||||
min="0"
|
||||
max="40"
|
||||
step="2"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
class="w-full accent-accent cursor-pointer"
|
||||
>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
class="demo-btn disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="!observing"
|
||||
@click="toggleObserver"
|
||||
>
|
||||
{{ observing ? 'Stop observing' : 'Observer stopped' }}
|
||||
</button>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
<p class="text-xs text-fg-subtle">
|
||||
With <span class="font-mono">box: 'border-box'</span> the reported size includes padding, so the slider changes the numbers without resizing the element.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -22,16 +22,16 @@ watch(isVisible, (visible, was) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="demo-stack max-w-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">IntersectionObserver</span>
|
||||
<span class="demo-label">IntersectionObserver</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
|
||||
:class="isVisible
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
: 'border-border bg-bg-inset text-fg-muted'"
|
||||
>
|
||||
<span class="size-1.5 rounded-full" :class="isVisible ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
<span class="size-1.5 rounded-full" :class="isVisible ? 'bg-emerald-500' : 'bg-fg-subtle'" />
|
||||
{{ isVisible ? 'In view' : 'Hidden' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -39,38 +39,38 @@ watch(isVisible, (visible, was) => {
|
||||
<!-- Scrollable root; the target card sits below the fold until scrolled. -->
|
||||
<div
|
||||
ref="root"
|
||||
class="h-44 overflow-y-auto rounded-xl border border-(--border) bg-(--bg-inset) p-3"
|
||||
class="h-44 overflow-y-auto rounded-xl border border-border bg-bg-inset p-3"
|
||||
>
|
||||
<p class="text-sm text-(--fg-subtle)">Scroll down inside this box…</p>
|
||||
<p class="text-sm text-fg-subtle">Scroll down inside this box…</p>
|
||||
<div class="h-40" />
|
||||
<div
|
||||
ref="target"
|
||||
class="rounded-lg border border-(--border) bg-(--accent) p-4 text-center text-(--accent-fg) shadow transition"
|
||||
class="rounded-lg border border-border bg-accent p-4 text-center text-accent-fg shadow transition"
|
||||
:class="isVisible ? 'opacity-100 scale-100' : 'opacity-60 scale-95'"
|
||||
>
|
||||
<div class="text-sm font-semibold">Target element</div>
|
||||
<div class="text-xs opacity-80">at least 50% visible to count</div>
|
||||
</div>
|
||||
<div class="h-24" />
|
||||
<p class="text-sm text-(--fg-subtle)">…and back up to hide it again.</p>
|
||||
<p class="text-sm text-fg-subtle">…and back up to hide it again.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
|
||||
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">{{ seenCount }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">times seen</div>
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center">
|
||||
<div class="demo-stat text-3xl">{{ seenCount }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">times seen</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center flex flex-col items-center justify-center">
|
||||
<div class="text-sm font-semibold" :class="isActive ? 'text-emerald-600 dark:text-emerald-400' : 'text-(--fg-muted)'">
|
||||
<div class="rounded-lg border border-border bg-bg-inset p-3 text-center flex flex-col items-center justify-center">
|
||||
<div class="text-sm font-semibold" :class="isActive ? 'text-emerald-600 dark:text-emerald-400' : 'text-fg-muted'">
|
||||
{{ isActive ? 'Active' : 'Paused' }}
|
||||
</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">observer</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-fg-subtle">observer</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
class="demo-btn"
|
||||
@click="isActive ? pause() : resume()"
|
||||
>
|
||||
{{ isActive ? 'Pause observer' : 'Resume observer' }}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user