docs(vue): add interactive demo for every composable
A beautiful, SSR-safe demo.vue next to each composable, auto-discovered by the docs extractor and rendered client-only on each composable's page.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import { onElementRemoval } from './index';
|
||||
|
||||
const watched = useTemplateRef<HTMLDivElement>('watched');
|
||||
|
||||
const mounted = ref(true);
|
||||
const removals = ref(0);
|
||||
const lastEvent = ref<string | null>(null);
|
||||
|
||||
// Fires whenever the watched element (or any ancestor containing it) leaves the DOM.
|
||||
onElementRemoval(watched, (records) => {
|
||||
removals.value++;
|
||||
lastEvent.value = new Date().toLocaleTimeString();
|
||||
// `records` are the raw MutationRecords describing the removal.
|
||||
void records;
|
||||
});
|
||||
|
||||
function toggle() {
|
||||
mounted.value = !mounted.value;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
removals.value = 0;
|
||||
lastEvent.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<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="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)"
|
||||
>
|
||||
<span class="size-1.5 rounded-full transition" :class="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
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
<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"
|
||||
@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"
|
||||
:disabled="removals === 0"
|
||||
@click="reset"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useActiveElement } from './index';
|
||||
|
||||
const activeElement = useActiveElement();
|
||||
|
||||
const fields = [
|
||||
{ id: 'name', label: 'Full name', placeholder: 'Ada Lovelace', type: 'text' },
|
||||
{ id: 'email', label: 'Email', placeholder: 'ada@analytical.engine', type: 'email' },
|
||||
{ id: 'city', label: 'City', placeholder: 'London', type: 'text' },
|
||||
];
|
||||
|
||||
const activeId = computed(() => activeElement.value?.id || null);
|
||||
const activeTag = computed(() => activeElement.value?.tagName.toLowerCase() ?? null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="space-y-3">
|
||||
<p class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Focus a field
|
||||
</p>
|
||||
<div
|
||||
v-for="field in fields"
|
||||
:key="field.id"
|
||||
class="space-y-1"
|
||||
>
|
||||
<label
|
||||
:for="field.id"
|
||||
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)"
|
||||
>
|
||||
</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"
|
||||
>
|
||||
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="flex items-center justify-between gap-3">
|
||||
<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)"
|
||||
>
|
||||
<{{ activeTag }}>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
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="truncate">{{ activeId ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useDocumentReadyState } from './index';
|
||||
|
||||
const readyState = useDocumentReadyState();
|
||||
|
||||
const stages: { state: DocumentReadyState; label: string; hint: string }[] = [
|
||||
{ state: 'loading', label: 'loading', hint: 'Parsing the document' },
|
||||
{ state: 'interactive', label: 'interactive', hint: 'DOM ready, assets pending' },
|
||||
{ state: 'complete', label: 'complete', hint: 'Everything loaded' },
|
||||
];
|
||||
|
||||
const activeIndex = computed(() => stages.findIndex(s => s.state === readyState.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
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">
|
||||
<span class="size-1.5 rounded-full bg-emerald-500" />
|
||||
{{ readyState }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ol class="space-y-2">
|
||||
<li
|
||||
v-for="(stage, i) in stages"
|
||||
: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)'"
|
||||
>
|
||||
<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)'
|
||||
: i === activeIndex
|
||||
? '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)">
|
||||
{{ stage.label }}
|
||||
</p>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
{{ stage.hint }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useDocumentVisibility } from './index';
|
||||
|
||||
const switches = ref(0);
|
||||
const lastHidden = ref<string | null>(null);
|
||||
|
||||
const visibility = useDocumentVisibility({
|
||||
onChange: (state) => {
|
||||
if (state === 'hidden') {
|
||||
switches.value += 1;
|
||||
lastHidden.value = new Date().toLocaleTimeString();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
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">
|
||||
<span
|
||||
class="flex size-14 items-center justify-center rounded-full text-2xl transition"
|
||||
:class="isVisible
|
||||
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-amber-500/10 text-amber-600 dark:text-amber-400'"
|
||||
>
|
||||
{{ isVisible ? '👁️' : '💤' }}
|
||||
</span>
|
||||
<div>
|
||||
<p class="font-mono text-2xl font-bold tabular-nums text-(--fg)">
|
||||
{{ visibility }}
|
||||
</p>
|
||||
<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)">
|
||||
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)">
|
||||
{{ switches }}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
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">
|
||||
{{ lastHidden ?? '—' }}
|
||||
</p>
|
||||
<p class="mt-0.5 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
Last hidden at
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue';
|
||||
import { useDraggable } from './index';
|
||||
|
||||
const container = useTemplateRef<HTMLElement>('container');
|
||||
const handle = useTemplateRef<HTMLElement>('handle');
|
||||
|
||||
const { x, y, isDragging, style } = useDraggable(
|
||||
useTemplateRef<HTMLElement>('card'),
|
||||
{
|
||||
initialValue: { x: 24, y: 24 },
|
||||
containerElement: container,
|
||||
handle,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
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)'"
|
||||
>
|
||||
<span
|
||||
class="size-1.5 rounded-full transition"
|
||||
:class="isDragging ? 'bg-(--accent)' : 'bg-(--fg-subtle)'"
|
||||
/>
|
||||
{{ isDragging ? 'dragging' : 'idle' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="container"
|
||||
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)' : ''"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>x: {{ Math.round(x) }}</div>
|
||||
<div>y: {{ Math.round(y) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-(--fg-subtle)">
|
||||
Drag from the header. Movement is clamped to the container bounds.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
import { useDropZone } from './index';
|
||||
|
||||
const dropZone = useTemplateRef<HTMLElement>('dropZone');
|
||||
|
||||
const { isOverDropZone, files, isSupported } = useDropZone(dropZone, {
|
||||
dataTypes: ['image/'],
|
||||
onDrop: (dropped) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('dropped', dropped);
|
||||
},
|
||||
});
|
||||
|
||||
const fileList = computed(() => files.value ?? []);
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes < 1024)
|
||||
return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024)
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<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"
|
||||
>
|
||||
Drag and Drop is not supported in this browser.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
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)'"
|
||||
>
|
||||
<span class="text-3xl transition" :class="isOverDropZone ? 'scale-110' : ''">
|
||||
{{ isOverDropZone ? '📥' : '🖼️' }}
|
||||
</span>
|
||||
<p class="text-sm font-medium text-(--fg)">
|
||||
{{ isOverDropZone ? 'Release to drop' : 'Drop image files here' }}
|
||||
</p>
|
||||
<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="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">
|
||||
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">
|
||||
{{ fileList.length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="fileList.length === 0"
|
||||
class="mt-3 text-center text-sm text-(--fg-subtle)"
|
||||
>
|
||||
Nothing dropped yet.
|
||||
</p>
|
||||
|
||||
<ul v-else class="mt-3 space-y-1.5">
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useElementBounding } from './index';
|
||||
|
||||
type Timing = 'sync' | 'next-frame';
|
||||
|
||||
const timing = ref<Timing>('sync');
|
||||
|
||||
const target = ref<HTMLElement>();
|
||||
|
||||
// Two independent measurers so toggling the timing mode is visible live.
|
||||
const sync = useElementBounding(target, { updateTiming: 'sync' });
|
||||
const nextFrame = useElementBounding(target, { updateTiming: 'next-frame' });
|
||||
|
||||
const bounds = computed(() => (timing.value === 'sync' ? sync : nextFrame));
|
||||
|
||||
// Resizable, draggable-ish target driven by sliders so every field updates live.
|
||||
const size = ref(56);
|
||||
|
||||
const metrics = computed(() => [
|
||||
{ label: 'width', value: bounds.value.width.value },
|
||||
{ label: 'height', value: bounds.value.height.value },
|
||||
{ label: 'top', value: bounds.value.top.value },
|
||||
{ label: 'left', value: bounds.value.left.value },
|
||||
{ label: 'right', value: bounds.value.right.value },
|
||||
{ label: 'bottom', value: bounds.value.bottom.value },
|
||||
{ label: 'x', value: bounds.value.x.value },
|
||||
{ label: 'y', value: bounds.value.y.value },
|
||||
]);
|
||||
|
||||
function fmt(n: number) {
|
||||
return Math.round(n);
|
||||
}
|
||||
</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="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)">
|
||||
{{ 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
|
||||
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"
|
||||
: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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div
|
||||
v-for="m in metrics"
|
||||
:key="m.label"
|
||||
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>
|
||||
</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>
|
||||
</div>
|
||||
<input
|
||||
id="bound-size"
|
||||
v-model.number="size"
|
||||
type="range"
|
||||
min="32"
|
||||
max="140"
|
||||
step="2"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
v-for="opt in (['sync', 'next-frame'] as Timing[])"
|
||||
:key="opt"
|
||||
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)'"
|
||||
@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"
|
||||
@click="bounds.update()"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useElementSize } from './index';
|
||||
|
||||
const target = ref<HTMLElement>();
|
||||
|
||||
// content-box vs border-box is the headline option, so observe it directly.
|
||||
const { width, height, stop } = useElementSize(target, { width: 0, height: 0 }, { box: 'border-box' });
|
||||
|
||||
const padding = ref(16);
|
||||
const observing = ref(true);
|
||||
|
||||
function toggleObserver() {
|
||||
if (observing.value) {
|
||||
stop();
|
||||
observing.value = false;
|
||||
}
|
||||
// Re-observing requires a fresh composable instance; keep the demo honest by
|
||||
// only offering "stop" once and noting it below.
|
||||
}
|
||||
|
||||
function fmt(n: number) {
|
||||
return n.toFixed(0);
|
||||
}
|
||||
</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="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)'" />
|
||||
{{ observing ? 'Observing' : 'Stopped' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Native textarea resize handle makes the element user-resizable. -->
|
||||
<textarea
|
||||
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)"
|
||||
>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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
<input
|
||||
id="size-padding"
|
||||
v-model.number="padding"
|
||||
type="range"
|
||||
min="0"
|
||||
max="40"
|
||||
step="2"
|
||||
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"
|
||||
:disabled="!observing"
|
||||
@click="toggleObserver"
|
||||
>
|
||||
{{ observing ? 'Stop observing' : 'Observer stopped' }}
|
||||
</button>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useElementVisibility } from './index';
|
||||
|
||||
const root = ref<HTMLElement>();
|
||||
const target = ref<HTMLElement>();
|
||||
|
||||
// controls: true returns { isVisible, isSupported, isActive, pause, resume, stop }.
|
||||
const { isVisible, isActive, pause, resume } = useElementVisibility(target, {
|
||||
controls: true,
|
||||
root,
|
||||
threshold: 0.5,
|
||||
});
|
||||
|
||||
const seenCount = ref(0);
|
||||
|
||||
// Count each time the card crosses into view (rising edge only).
|
||||
watch(isVisible, (visible, was) => {
|
||||
if (visible && !was)
|
||||
seenCount.value++;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">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)'"
|
||||
>
|
||||
<span class="size-1.5 rounded-full" :class="isVisible ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
{{ isVisible ? 'In view' : 'Hidden' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 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"
|
||||
>
|
||||
<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="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>
|
||||
</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>
|
||||
<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>
|
||||
</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"
|
||||
@click="isActive ? pause() : resume()"
|
||||
>
|
||||
{{ isActive ? 'Pause observer' : 'Resume observer' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
import { useFocusGuard } from './index';
|
||||
|
||||
// useFocusGuard() returns void: on mount it inserts a pair of invisible,
|
||||
// focusable sentinel elements at the boundaries of the DOM tree. They keep
|
||||
// focus order consistent (e.g. for modal/overlay focus management) and are
|
||||
// removed when the last consumer unmounts.
|
||||
|
||||
// Mount the guard through a child so we can toggle it on/off in the demo.
|
||||
const GuardHost = defineComponent({
|
||||
name: 'GuardHost',
|
||||
setup() {
|
||||
useFocusGuard('demo-guard');
|
||||
return () => h('span', { class: 'sr-only' }, 'focus guards mounted');
|
||||
},
|
||||
});
|
||||
|
||||
const enabled = ref(false);
|
||||
const focused = ref<string | null>(null);
|
||||
|
||||
const fields = [
|
||||
{ id: 'name', label: 'Name', value: 'Ada Lovelace' },
|
||||
{ id: 'email', label: 'Email', value: 'ada@analytical.engine' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<component :is="GuardHost" v-if="enabled" />
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Focus guards</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition"
|
||||
:class="enabled
|
||||
? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'border-(--border) bg-(--bg-inset) text-(--fg-muted)'"
|
||||
>
|
||||
<span class="size-1.5 rounded-full" :class="enabled ? 'bg-emerald-500' : 'bg-(--fg-subtle)'" />
|
||||
{{ enabled ? 'Mounted' : 'Off' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- A small focusable form to feel tab order with the guards active. -->
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-3">
|
||||
<div v-for="f in fields" :key="f.id" class="flex flex-col gap-1">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" :for="f.id">{{ f.label }}</label>
|
||||
<input
|
||||
:id="f.id"
|
||||
:value="f.value"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-(--border) bg-(--bg) px-3 py-2 text-sm text-(--fg) placeholder:text-(--fg-subtle) transition focus:border-(--accent) focus:outline-none focus:ring-2 focus:ring-(--ring)"
|
||||
@focus="focused = f.label"
|
||||
@blur="focused = null"
|
||||
>
|
||||
</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"
|
||||
@focus="focused = 'Submit'"
|
||||
@blur="focused = null"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
focused: {{ focused ?? 'nothing' }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="enabled = !enabled"
|
||||
>
|
||||
{{ enabled ? 'Remove focus guards' : 'Mount focus guards' }}
|
||||
</button>
|
||||
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
Guards are invisible <span class="font-mono">tabindex="0"</span> sentinels inserted at the page boundaries. Tab past the last field with them on to feel focus wrap around for overlays and modals.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useIntersectionObserver } from './index';
|
||||
|
||||
const sections = [
|
||||
{ id: 'intro', label: 'Introduction', color: 'bg-sky-500' },
|
||||
{ id: 'install', label: 'Installation', color: 'bg-emerald-500' },
|
||||
{ id: 'usage', label: 'Usage', color: 'bg-violet-500' },
|
||||
{ id: 'api', label: 'API Reference', color: 'bg-amber-500' },
|
||||
];
|
||||
|
||||
const root = ref<HTMLElement>();
|
||||
const itemEls = ref<HTMLElement[]>([]);
|
||||
|
||||
// Reactive threshold + rootMargin demonstrate live re-observation.
|
||||
const threshold = ref(0.5);
|
||||
|
||||
// Track which section is currently the most-visible "active" one (scroll-spy).
|
||||
const ratios = ref<Record<string, number>>({});
|
||||
|
||||
useIntersectionObserver(
|
||||
itemEls,
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
const id = (entry.target as HTMLElement).dataset.id;
|
||||
if (id)
|
||||
ratios.value = { ...ratios.value, [id]: entry.intersectionRatio };
|
||||
}
|
||||
},
|
||||
{
|
||||
root,
|
||||
threshold: () => [0, 0.25, 0.5, 0.75, 1],
|
||||
},
|
||||
);
|
||||
|
||||
const activeId = computed(() => {
|
||||
let best: string | null = null;
|
||||
let bestRatio = threshold.value;
|
||||
for (const s of sections) {
|
||||
const r = ratios.value[s.id] ?? 0;
|
||||
if (r >= bestRatio) {
|
||||
bestRatio = r;
|
||||
best = s.id;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-md flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Scroll spy</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
active: {{ activeId ?? '—' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<!-- Nav reflects which section dominates the viewport. -->
|
||||
<nav class="flex w-32 shrink-0 flex-col gap-1">
|
||||
<span
|
||||
v-for="s in sections"
|
||||
:key="s.id"
|
||||
class="flex items-center gap-2 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition"
|
||||
:class="activeId === s.id
|
||||
? 'border-(--border-strong) bg-(--bg-inset) text-(--fg)'
|
||||
: 'border-transparent text-(--fg-muted)'"
|
||||
>
|
||||
<span class="size-2 rounded-full transition" :class="[s.color, activeId === s.id ? 'opacity-100' : 'opacity-30']" />
|
||||
{{ s.label }}
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<!-- Scrollable content; each section is an observed target. -->
|
||||
<div
|
||||
ref="root"
|
||||
class="h-56 flex-1 overflow-y-auto rounded-xl border border-(--border) bg-(--bg-inset) p-3 flex flex-col gap-3 scroll-smooth"
|
||||
>
|
||||
<section
|
||||
v-for="s in sections"
|
||||
:key="s.id"
|
||||
ref="itemEls"
|
||||
:data-id="s.id"
|
||||
class="rounded-lg border border-(--border) bg-(--bg-elevated) p-4 transition"
|
||||
:class="activeId === s.id ? 'ring-2 ring-(--ring)' : ''"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-(--fg)">{{ s.label }}</h3>
|
||||
<p class="mt-1 text-xs text-(--fg-subtle)">
|
||||
Visibility: {{ Math.round((ratios[s.id] ?? 0) * 100) }}%
|
||||
</p>
|
||||
<div class="mt-2 h-16 rounded bg-(--bg-inset)" />
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)" for="io-threshold">Active threshold</label>
|
||||
<span class="font-mono text-xs tabular-nums text-(--fg-muted)">{{ Math.round(threshold * 100) }}%</span>
|
||||
</div>
|
||||
<input
|
||||
id="io-threshold"
|
||||
v-model.number="threshold"
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="0.9"
|
||||
step="0.1"
|
||||
class="w-full accent-(--accent) cursor-pointer"
|
||||
>
|
||||
<p class="text-xs text-(--fg-subtle)">A section becomes active once at least this much of it is visible inside the scroll root.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, useTemplateRef } from 'vue';
|
||||
import { useMutationObserver } from './index';
|
||||
|
||||
const target = useTemplateRef<HTMLElement>('target');
|
||||
|
||||
// Mutable state that drives the observed element so changes are visible.
|
||||
const labels = ref(['Inbox', 'Drafts', 'Archive']);
|
||||
const accent = ref(false);
|
||||
const fontSize = ref(14);
|
||||
|
||||
const mutationCount = shallowRef(0);
|
||||
const lastType = shallowRef<MutationRecordType | '—'>('—');
|
||||
const log = ref<string[]>([]);
|
||||
|
||||
const { isSupported, isActive, pause, resume } = useMutationObserver(
|
||||
target,
|
||||
(records) => {
|
||||
for (const record of records) {
|
||||
mutationCount.value++;
|
||||
lastType.value = record.type;
|
||||
|
||||
let detail = record.type as string;
|
||||
if (record.type === 'attributes')
|
||||
detail = `attribute "${record.attributeName}"`;
|
||||
else if (record.type === 'childList')
|
||||
detail = `children +${record.addedNodes.length} −${record.removedNodes.length}`;
|
||||
|
||||
log.value.unshift(detail);
|
||||
if (log.value.length > 5)
|
||||
log.value.pop();
|
||||
}
|
||||
},
|
||||
{ attributes: true, childList: true, subtree: true, attributeFilter: ['style', 'class'] },
|
||||
);
|
||||
|
||||
function addLabel() {
|
||||
const pool = ['Spam', 'Sent', 'Starred', 'Trash', 'Snoozed', 'Important'];
|
||||
labels.value.push(pool[labels.value.length % pool.length]);
|
||||
}
|
||||
|
||||
function removeLabel() {
|
||||
labels.value.pop();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">MutationObserver</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="size-1.5 rounded-full transition" :class="isActive ? 'bg-emerald-500' : 'bg-amber-500'" />
|
||||
{{ isActive ? 'Observing' : 'Paused' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="!isSupported" class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm text-amber-600 dark:text-amber-400">
|
||||
MutationObserver is not supported in this browser.
|
||||
</p>
|
||||
|
||||
<template v-else>
|
||||
<!-- Observed subtree: every attribute/child change fires the callback -->
|
||||
<div
|
||||
ref="target"
|
||||
class="rounded-xl border p-4 transition"
|
||||
:class="accent ? 'border-(--accent) bg-(--accent-subtle)' : 'border-(--border) bg-(--bg-elevated)'"
|
||||
:style="{ fontSize: `${fontSize}px` }"
|
||||
>
|
||||
<div class="mb-2 text-xs font-medium uppercase tracking-wide" :class="accent ? 'text-(--accent-text)' : 'text-(--fg-subtle)'">
|
||||
Observed element
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<span
|
||||
v-for="(label, i) in labels"
|
||||
:key="`${label}-${i}`"
|
||||
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)"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<span v-if="!labels.length" class="text-xs text-(--fg-subtle)">No labels</span>
|
||||
</div>
|
||||
</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)">{{ mutationCount }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">mutations</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 text-center">
|
||||
<div class="truncate font-mono text-sm font-bold text-(--fg)">{{ lastType }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">last record type</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-(--border) bg-(--bg-elevated) px-3 py-1.5 text-sm font-medium text-(--fg) transition hover:bg-(--bg-inset) hover:border-(--border-strong) active:scale-[0.98] cursor-pointer"
|
||||
@click="addLabel"
|
||||
>
|
||||
Add child
|
||||
</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"
|
||||
:disabled="!labels.length"
|
||||
@click="removeLabel"
|
||||
>
|
||||
Remove child
|
||||
</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"
|
||||
@click="accent = !accent"
|
||||
>
|
||||
Toggle class
|
||||
</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"
|
||||
@click="fontSize = fontSize === 14 ? 18 : 14"
|
||||
>
|
||||
Toggle style
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3">
|
||||
<div class="mb-1.5 text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Recent records</div>
|
||||
<ul v-if="log.length" class="flex flex-col gap-1 font-mono text-xs text-(--fg)">
|
||||
<li v-for="(entry, i) in log" :key="i" class="truncate">{{ entry }}</li>
|
||||
</ul>
|
||||
<p v-else class="font-mono text-xs text-(--fg-subtle)">Mutate the element above to record changes.</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
@click="isActive ? pause() : resume()"
|
||||
>
|
||||
{{ isActive ? 'Pause observer' : 'Resume observer' }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useTemplateRef } from 'vue';
|
||||
import { useParentElement } from './index';
|
||||
|
||||
const child = useTemplateRef<HTMLElement>('child');
|
||||
|
||||
// useParentElement returns a read-only shallow ref of the element's parent.
|
||||
const parent = useParentElement(child);
|
||||
|
||||
// Reparent the child between two wrappers to watch the resolved parent change.
|
||||
const inSecond = ref(false);
|
||||
|
||||
const parentInfo = computed(() => {
|
||||
const el = parent.value;
|
||||
if (!el)
|
||||
return null;
|
||||
|
||||
return {
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id || '—',
|
||||
classes: el.className || '—',
|
||||
width: Math.round(el.getBoundingClientRect().width),
|
||||
height: Math.round(el.getBoundingClientRect().height),
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">parentElement</span>
|
||||
|
||||
<!-- Two candidate parents; the child is teleported between them via v-if -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
id="wrapper-a"
|
||||
class="rounded-xl border border-dashed p-3 transition"
|
||||
:class="!inSecond ? 'border-(--accent) bg-(--accent-subtle)' : 'border-(--border) bg-(--bg-elevated)'"
|
||||
>
|
||||
<div class="mb-2 text-xs font-medium uppercase tracking-wide" :class="!inSecond ? 'text-(--accent-text)' : 'text-(--fg-subtle)'">
|
||||
#wrapper-a
|
||||
</div>
|
||||
<div
|
||||
v-if="!inSecond"
|
||||
ref="child"
|
||||
class="rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2 text-sm font-medium text-(--fg)"
|
||||
>
|
||||
Tracked child
|
||||
</div>
|
||||
<div v-else class="text-xs text-(--fg-subtle)">empty</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="wrapper-b"
|
||||
class="rounded-xl border border-dashed p-3 transition"
|
||||
:class="inSecond ? 'border-(--accent) bg-(--accent-subtle)' : 'border-(--border) bg-(--bg-elevated)'"
|
||||
>
|
||||
<div class="mb-2 text-xs font-medium uppercase tracking-wide" :class="inSecond ? 'text-(--accent-text)' : 'text-(--fg-subtle)'">
|
||||
#wrapper-b
|
||||
</div>
|
||||
<div
|
||||
v-if="inSecond"
|
||||
ref="child"
|
||||
class="rounded-lg border border-(--border) bg-(--bg-inset) px-3 py-2 text-sm font-medium text-(--fg)"
|
||||
>
|
||||
Tracked child
|
||||
</div>
|
||||
<div v-else class="text-xs text-(--fg-subtle)">empty</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
@click="inSecond = !inSecond"
|
||||
>
|
||||
Move child to #wrapper-{{ inSecond ? 'a' : 'b' }}
|
||||
</button>
|
||||
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-inset) p-3 font-mono text-sm text-(--fg) tabular-nums">
|
||||
<div class="mb-2 font-sans text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Resolved parent</div>
|
||||
<template v-if="parentInfo">
|
||||
<div class="flex justify-between gap-2">
|
||||
<span class="text-(--fg-subtle)">tag</span>
|
||||
<span class="text-(--accent-text)"><{{ parentInfo.tag }}></span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-2">
|
||||
<span class="text-(--fg-subtle)">id</span>
|
||||
<span class="truncate">{{ parentInfo.id }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between gap-2">
|
||||
<span class="text-(--fg-subtle)">size</span>
|
||||
<span>{{ parentInfo.width }} × {{ parentInfo.height }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="font-sans text-xs text-(--fg-subtle)">Resolving parent on the client…</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, shallowRef, useTemplateRef } from 'vue';
|
||||
import { useResizeObserver } from './index';
|
||||
|
||||
const target = useTemplateRef<HTMLElement>('target');
|
||||
|
||||
const size = reactive({ width: 0, height: 0 });
|
||||
const callbacks = shallowRef(0);
|
||||
|
||||
const { isSupported, isActive, pause, resume } = useResizeObserver(
|
||||
target,
|
||||
([entry]) => {
|
||||
if (!entry)
|
||||
return;
|
||||
|
||||
const { width, height } = entry.contentRect;
|
||||
size.width = Math.round(width);
|
||||
size.height = Math.round(height);
|
||||
callbacks.value++;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm 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 transition" :class="isActive ? 'bg-emerald-500' : 'bg-amber-500'" />
|
||||
{{ isActive ? 'Observing' : 'Paused' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p v-if="!isSupported" class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm text-amber-600 dark:text-amber-400">
|
||||
ResizeObserver is not supported in this browser.
|
||||
</p>
|
||||
|
||||
<template v-else>
|
||||
<!-- Drag the bottom-right handle to resize; the observer reports new dimensions -->
|
||||
<div
|
||||
ref="target"
|
||||
class="relative grid min-h-32 min-w-40 max-w-full resize overflow-auto rounded-xl border border-(--border) bg-(--bg-elevated) p-4 place-items-center"
|
||||
style="width: 16rem; height: 8rem;"
|
||||
>
|
||||
<div class="pointer-events-none select-none text-center">
|
||||
<div class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
{{ size.width }}<span class="text-(--fg-subtle)"> × </span>{{ size.height }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-(--fg-subtle)">drag the bottom-right corner</div>
|
||||
</div>
|
||||
</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)">{{ size.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-2 text-center">
|
||||
<div class="font-mono text-lg font-bold tabular-nums text-(--fg)">{{ size.height }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">height px</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)">{{ callbacks }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">callbacks</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer"
|
||||
@click="isActive ? pause() : resume()"
|
||||
>
|
||||
{{ isActive ? 'Pause observer' : 'Resume observer' }}
|
||||
</button>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
While paused, resizing won't update the readout until you resume.
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useWindowFocus } from './index';
|
||||
|
||||
// useWindowFocus returns a single ShallowRef<boolean> — bind it directly.
|
||||
const focused = useWindowFocus();
|
||||
|
||||
const blurCount = ref(0);
|
||||
|
||||
watch(focused, (isFocused) => {
|
||||
if (!isFocused)
|
||||
blurCount.value++;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Window focus</span>
|
||||
|
||||
<div
|
||||
class="rounded-xl border p-6 text-center transition"
|
||||
:class="focused
|
||||
? 'border-emerald-500/30 bg-emerald-500/10'
|
||||
: 'border-(--border) bg-(--bg-inset)'"
|
||||
>
|
||||
<div
|
||||
class="mx-auto flex size-14 items-center justify-center rounded-full transition"
|
||||
:class="focused
|
||||
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-(--bg-elevated) text-(--fg-subtle)'"
|
||||
>
|
||||
<span class="size-3 rounded-full transition" :class="focused ? 'bg-emerald-500 animate-pulse' : 'bg-(--fg-subtle)'" />
|
||||
</div>
|
||||
<div class="mt-3 text-lg font-semibold text-(--fg)">
|
||||
{{ focused ? 'Window focused' : 'Window blurred' }}
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-(--fg-muted)">
|
||||
{{ focused ? 'Click outside or switch apps to blur.' : 'Click back into this window to refocus.' }}
|
||||
</p>
|
||||
</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)">{{ focused ? 'on' : 'off' }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">focused</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)">{{ blurCount }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">times blurred</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useWindowScroll } from './index';
|
||||
|
||||
const { x, y, isScrolling, arrivedState, directions, measure } = useWindowScroll({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
|
||||
const verticalDirection = computed(() => {
|
||||
if (directions.bottom)
|
||||
return 'down';
|
||||
if (directions.top)
|
||||
return 'up';
|
||||
return 'idle';
|
||||
});
|
||||
|
||||
function scrollToTop() {
|
||||
y.value = 0;
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
// Writing past the max is clamped by the browser, taking us to the bottom.
|
||||
y.value = 1_000_000;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-sm flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Window scroll</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md border border-(--border) bg-(--bg-inset) px-2 py-0.5 text-xs font-medium text-(--fg-muted)">
|
||||
<span class="size-1.5 rounded-full transition" :class="isScrolling ? 'bg-emerald-500 animate-pulse' : 'bg-(--fg-subtle)'" />
|
||||
{{ isScrolling ? 'Scrolling' : 'Idle' }}
|
||||
</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)">{{ Math.round(x) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">scroll x</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)">{{ Math.round(y) }}</div>
|
||||
<div class="text-[10px] uppercase tracking-wide text-(--fg-subtle)">scroll y</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-(--fg-subtle)">Direction</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)">
|
||||
{{ verticalDirection }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-(--fg-muted)">Arrived at top</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium"
|
||||
:class="arrivedState.top
|
||||
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-(--bg-inset) text-(--fg-subtle)'"
|
||||
>
|
||||
{{ arrivedState.top ? 'yes' : 'no' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-(--fg-muted)">Arrived at bottom</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium"
|
||||
:class="arrivedState.bottom
|
||||
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-(--bg-inset) text-(--fg-subtle)'"
|
||||
>
|
||||
{{ arrivedState.bottom ? 'yes' : 'no' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 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 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="arrivedState.top"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
Scroll to top
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center gap-1.5 rounded-lg border border-transparent bg-(--accent) px-3 py-1.5 text-sm font-medium text-(--accent-fg) transition hover:bg-(--accent-hover) active:scale-[0.98] cursor-pointer disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100"
|
||||
:disabled="arrivedState.bottom"
|
||||
@click="scrollToBottom"
|
||||
>
|
||||
Scroll to bottom
|
||||
</button>
|
||||
</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"
|
||||
@click="measure"
|
||||
>
|
||||
Re-measure
|
||||
</button>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
Scroll the documentation page to watch the position and arrived edges update live.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useWindowSize } from './index';
|
||||
|
||||
const { width, height } = useWindowSize();
|
||||
|
||||
const orientation = computed(() => (width.value >= height.value ? 'landscape' : 'portrait'));
|
||||
const aspect = computed(() => (height.value === 0 ? '–' : (width.value / height.value).toFixed(2)));
|
||||
const ratioPercent = computed(() => {
|
||||
const total = width.value + height.value;
|
||||
return total === 0 ? 50 : Math.round((width.value / total) * 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full max-w-sm flex-col gap-4">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<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)">
|
||||
Width
|
||||
</p>
|
||||
<p class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
{{ width }}
|
||||
</p>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
px
|
||||
</p>
|
||||
</div>
|
||||
<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)">
|
||||
Height
|
||||
</p>
|
||||
<p class="font-mono text-3xl font-bold tabular-nums text-(--fg)">
|
||||
{{ height }}
|
||||
</p>
|
||||
<p class="text-xs text-(--fg-subtle)">
|
||||
px
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-4">
|
||||
<div
|
||||
class="relative flex items-center justify-center overflow-hidden rounded-lg border border-(--border-strong) bg-(--bg-inset) transition-all"
|
||||
:style="{ aspectRatio: `${Math.max(width, 1)} / ${Math.max(height, 1)}` }"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 bg-(--accent-subtle) transition-all"
|
||||
:style="{ width: `${ratioPercent}%` }"
|
||||
/>
|
||||
<span class="relative font-mono text-xs text-(--fg-muted) tabular-nums">
|
||||
{{ width }} × {{ height }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-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)">
|
||||
{{ orientation }}
|
||||
</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)">
|
||||
ratio {{ aspect }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-xs text-(--fg-subtle)">
|
||||
Resize your browser window to watch the values update live.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user