docs: add package introductions and the @robonen/crdt guide
An intro.vue landing for all 12 packages, plus a multi-section crdt guide (Concepts, Primitives, Replication & Sync, and an interactive convergence Playground).
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
<script setup lang="ts">
|
||||
const quickStart = `<script setup lang="ts">
|
||||
import { createDefaultRegistry, createEditor, createEditorState, EditorRoot } from '@robonen/editor';
|
||||
|
||||
const registry = createDefaultRegistry();
|
||||
const editor = createEditor({ state: createEditorState({ registry }) });
|
||||
<\/script>
|
||||
|
||||
<template>
|
||||
<EditorRoot :editor="editor" autofocus class="editor" />
|
||||
<\/template>`;
|
||||
|
||||
const composeSlots = `<EditorRoot :editor="editor" autofocus>
|
||||
<EditorContent />
|
||||
<EditorBubbleMenu /> <!-- formatting toolbar on selection -->
|
||||
<EditorSlashMenu /> <!-- type \`/\` to insert blocks -->
|
||||
</EditorRoot>`;
|
||||
|
||||
const commands = `import { setBlockType, toggleMark } from '@robonen/editor';
|
||||
|
||||
editor.command(toggleMark('bold'));
|
||||
editor.command(setBlockType('heading', { level: 2 }));
|
||||
|
||||
// Called without a dispatch they run dry — perfect for
|
||||
// computing disabled / active toolbar state.
|
||||
const canBold = editor.command(toggleMark('bold'));`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="docs-section">
|
||||
<div class="prose-docs">
|
||||
<h1>@robonen/editor</h1>
|
||||
<p>
|
||||
A <strong>headless, block-based rich-text editor for Vue 3</strong> — in the spirit of
|
||||
Tiptap / ProseMirror / Editor.js, but with a registry-driven schema and a
|
||||
<strong>hand-built CRDT</strong> for collaboration (no Yjs / Loro / Automerge).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="prose-docs">
|
||||
<p>
|
||||
Most editors force a trade: the structured, block-first authoring of Editor.js, or the
|
||||
document fidelity of ProseMirror where native cross-block selection and arrow navigation
|
||||
just work. <code>@robonen/editor</code> takes the ProseMirror route — a single
|
||||
<code>contenteditable</code> surface — and layers a modular block registry on top, so blocks
|
||||
and inline marks are added without touching the core. The model, schema, state, commands and
|
||||
keymap are entirely DOM-free and Vue-free; the Vue layer only renders and handles input.
|
||||
Every edit is a step-based transaction with an exact inverse, which gives you real undo/redo
|
||||
and — because the same steps drive the CRDT — conflict-free collaboration for free.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5">
|
||||
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Headless by design</h3>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
Ships behavior and DOM structure (<code class="text-(--accent-text)">data-block-*</code>
|
||||
hooks), never styling. Bring your own CSS and own the look completely.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5">
|
||||
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Registry-driven schema</h3>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<code class="text-(--accent-text)">defineBlock</code> /
|
||||
<code class="text-(--accent-text)">defineMark</code> register into an immutable schema —
|
||||
add a custom block or mark with no core changes.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5">
|
||||
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Step-based transactions</h3>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
Every edit is a step with an exact inverse, powering reliable undo/redo and a single source
|
||||
of truth for both local edits and sync.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-(--border) bg-(--bg-subtle) p-5">
|
||||
<h3 class="mb-1.5 text-sm font-semibold text-(--fg)">Own CRDT, pluggable</h3>
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
RGA text, fractional-indexed blocks, Peritext-style marks and presence behind a
|
||||
<code class="text-(--accent-text)">CrdtProvider</code> — over any transport.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prose-docs">
|
||||
<h2>Install</h2>
|
||||
<p>
|
||||
The editor depends on <code>@robonen/crdt</code> for the built-in collaboration provider, and
|
||||
on <code>vue</code> as a peer.
|
||||
</p>
|
||||
</div>
|
||||
<DocsCode :code="`pnpm add @robonen/editor @robonen/crdt vue`" lang="bash" />
|
||||
|
||||
<div class="prose-docs">
|
||||
<h2>Quick start</h2>
|
||||
<p>
|
||||
Create a registry, build an editor around its state, and mount <code>EditorRoot</code>. Its
|
||||
default slot renders <code>EditorContent</code> (the single <code>contenteditable</code>), so
|
||||
this is a fully working editor with all built-in blocks and marks.
|
||||
</p>
|
||||
</div>
|
||||
<DocsCode :code="quickStart" lang="vue" />
|
||||
|
||||
<div class="prose-docs">
|
||||
<p>
|
||||
Provide your own slot to add UI around the editable surface — the bubble toolbar floats over a
|
||||
selection, and the slash menu opens when you type <code>/</code> at the start of a line.
|
||||
</p>
|
||||
</div>
|
||||
<DocsCode :code="composeSlots" lang="vue" />
|
||||
|
||||
<div class="prose-docs">
|
||||
<h2>Commands</h2>
|
||||
<p>
|
||||
Commands are <code>(state, dispatch?, view?) => boolean</code> functions that power the
|
||||
keymap, the UI, and programmatic edits. Run one with <code>editor.command(...)</code>; omit
|
||||
the dispatch to dry-run it for active/disabled state.
|
||||
</p>
|
||||
</div>
|
||||
<DocsCode :code="commands" lang="ts" />
|
||||
|
||||
<div class="prose-docs">
|
||||
<h2>Built-in blocks & marks</h2>
|
||||
<p>
|
||||
<code>createDefaultRegistry()</code> wires up a full set out of the box —
|
||||
<strong>blocks:</strong> <code>paragraph</code>, <code>heading</code> (1–6),
|
||||
<code>bulleted-list</code> / <code>numbered-list</code> / <code>todo-list</code>,
|
||||
<code>blockquote</code>, <code>code-block</code>, <code>callout</code>, <code>divider</code>,
|
||||
<code>image</code>; <strong>marks:</strong> <code>bold</code>, <code>italic</code>,
|
||||
<code>underline</code>, <code>strike</code>, <code>highlight</code>, <code>code</code>,
|
||||
<code>link</code>. Markdown input rules (<code># </code>, <code>- </code>, <code>1. </code>,
|
||||
<code>> </code>, <code>[] </code>) and hotkeys (<code>Mod-b/i/u</code>,
|
||||
<code>Mod-z</code>, …) are included.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-amber-500/30 bg-amber-500/10 p-4">
|
||||
<p class="text-sm leading-relaxed text-(--fg-muted)">
|
||||
<strong class="text-amber-700 dark:text-amber-400">Status: v0, work in progress.</strong>
|
||||
Core logic is covered by unit + convergence tests; the contenteditable / Playwright suite
|
||||
runs locally. The collaboration layer has a few documented, deferred limitations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="prose-docs">
|
||||
<h2>Where to next</h2>
|
||||
<p>Jump into the pieces you'll reach for first:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>EditorRoot</code> and <code>EditorContent</code> — the mount
|
||||
surface and the single contenteditable.
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/editor/create-default-registry"><code>createDefaultRegistry</code></NuxtLink>,
|
||||
<NuxtLink to="/editor/define-block"><code>defineBlock</code></NuxtLink> and
|
||||
<NuxtLink to="/editor/define-mark"><code>defineMark</code></NuxtLink> — extend the schema.
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/editor/toggle-mark"><code>toggleMark</code></NuxtLink> /
|
||||
<NuxtLink to="/editor/set-block-type"><code>setBlockType</code></NuxtLink> — the commands
|
||||
API for programmatic and toolbar edits.
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/editor/bind-crdt"><code>bindCrdt</code></NuxtLink> and
|
||||
<NuxtLink to="/editor/create-native-provider"><code>createNativeProvider</code></NuxtLink>
|
||||
— wire up real-time collaboration with the built-in CRDT.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
The full API reference for every export is listed right below.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,145 @@
|
||||
<script setup lang="ts">
|
||||
// Landing hero for @robonen/primitives. Static content only — no runtime
|
||||
// logic at setup top-level, so it prerenders and hydrates cleanly.
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="docs-section">
|
||||
<div class="prose-docs">
|
||||
<h1>@robonen/primitives</h1>
|
||||
<p>
|
||||
A collection of unstyled, accessible UI primitives for Vue 3 — the headless
|
||||
building blocks for design systems and component libraries.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="prose-docs">
|
||||
Most component libraries bundle behavior and styling together, so the moment
|
||||
your design diverges you end up fighting the framework. <code>@robonen/primitives</code>
|
||||
ships the hard part — state, focus management, keyboard interaction, ARIA wiring,
|
||||
portalling and positioning — and leaves the markup and styling entirely to you.
|
||||
Every primitive is composed from small, controllable parts (a <code>Root</code>,
|
||||
a <code>Trigger</code>, a <code>Content</code>, and so on) following the same
|
||||
conventions, so once you learn one you know them all.
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<h3 class="m-0 text-sm font-semibold text-(--fg)">Unstyled by design</h3>
|
||||
<p class="mt-2 mb-0 text-sm text-(--fg-muted)">
|
||||
No CSS shipped. Primitives render the DOM you ask for and expose state via
|
||||
data attributes, so you bring your own styles — Tailwind, vanilla CSS, anything.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<h3 class="m-0 text-sm font-semibold text-(--fg)">Accessible out of the box</h3>
|
||||
<p class="mt-2 mb-0 text-sm text-(--fg-muted)">
|
||||
Focus scopes, roving tabindex, visually-hidden labels and correct ARIA roles
|
||||
are handled for you. The suite is tested against
|
||||
<code>axe-core</code> in a real browser.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<h3 class="m-0 text-sm font-semibold text-(--fg)">Controlled or uncontrolled</h3>
|
||||
<p class="mt-2 mb-0 text-sm text-(--fg-muted)">
|
||||
Bind state with <code>v-model</code> when you need control, or set a
|
||||
<code>defaultValue</code> / <code>defaultOpen</code> and let the primitive
|
||||
manage itself.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-(--border) bg-(--bg-elevated) p-5">
|
||||
<h3 class="m-0 text-sm font-semibold text-(--fg)">Composable & polymorphic</h3>
|
||||
<p class="mt-2 mb-0 text-sm text-(--fg-muted)">
|
||||
Every part takes an <code>as</code> prop, or use <code>as="template"</code>
|
||||
to merge behavior onto your own element. Floating UI powers positioning for
|
||||
popovers, tooltips and menus.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prose-docs">
|
||||
<h2>Install</h2>
|
||||
</div>
|
||||
|
||||
<DocsCode :code="`pnpm add @robonen/primitives`" lang="bash" />
|
||||
|
||||
<div class="prose-docs">
|
||||
<h2>Usage</h2>
|
||||
<p>
|
||||
Primitives are assembled from named parts. Here is a complete dialog — open
|
||||
state is uncontrolled, focus is trapped, body scroll is locked, and the
|
||||
content is portalled out of the DOM flow:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DocsCode lang="vue" :code="`<script setup lang="ts">
|
||||
import {
|
||||
DialogRoot,
|
||||
DialogTrigger,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogClose,
|
||||
} from '@robonen/primitives';
|
||||
</scr­ipt>
|
||||
|
||||
<template>
|
||||
<DialogRoot>
|
||||
<DialogTrigger class="btn">Open</DialogTrigger>
|
||||
|
||||
<DialogPortal>
|
||||
<DialogOverlay class="overlay" />
|
||||
<DialogContent class="dialog">
|
||||
<DialogTitle>Delete project</DialogTitle>
|
||||
<DialogDescription>This action cannot be undone.</DialogDescription>
|
||||
<DialogClose class="btn">Cancel</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>`" />
|
||||
|
||||
<div class="prose-docs">
|
||||
<p>
|
||||
Need full control over open state? Bind it directly — the same primitive works
|
||||
either way:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DocsCode lang="vue" :code="`<DialogRoot v-model:open="isOpen">
|
||||
<!-- ... -->
|
||||
</DialogRoot>`" />
|
||||
|
||||
<div class="prose-docs">
|
||||
<h2>The Primitive component</h2>
|
||||
<p>
|
||||
At the core of every part is <code>Primitive</code>, a polymorphic functional
|
||||
component. Pass <code>as</code> to choose the element, or <code>as="template"</code>
|
||||
to forward behavior onto a child of your own.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DocsCode lang="ts" :code="`import { Primitive, Slot } from '@robonen/primitives';
|
||||
|
||||
// <Primitive as="button" /> renders a <button>
|
||||
// <Primitive as="template"> merges props onto the slotted child`" />
|
||||
|
||||
<div class="prose-docs">
|
||||
<h2>Where to next</h2>
|
||||
<p>
|
||||
The full primitive index is listed below. A few good starting points:
|
||||
</p>
|
||||
<ul>
|
||||
<li><NuxtLink to="/primitives/dialog">Dialog</NuxtLink> and <NuxtLink to="/primitives/alert-dialog">Alert Dialog</NuxtLink> — modal layers with focus trapping.</li>
|
||||
<li><NuxtLink to="/primitives/popover">Popover</NuxtLink>, <NuxtLink to="/primitives/tooltip">Tooltip</NuxtLink> and <NuxtLink to="/primitives/hover-card">Hover Card</NuxtLink> — Floating UI positioned surfaces.</li>
|
||||
<li><NuxtLink to="/primitives/select">Select</NuxtLink>, <NuxtLink to="/primitives/combobox">Combobox</NuxtLink> and <NuxtLink to="/primitives/listbox">Listbox</NuxtLink> — keyboard-driven option pickers.</li>
|
||||
<li><NuxtLink to="/primitives/switch">Switch</NuxtLink>, <NuxtLink to="/primitives/checkbox">Checkbox</NuxtLink> and <NuxtLink to="/primitives/slider">Slider</NuxtLink> — form controls that integrate with native inputs.</li>
|
||||
<li><NuxtLink to="/primitives/focus-scope">Focus Scope</NuxtLink> and <NuxtLink to="/primitives/presence">Presence</NuxtLink> — the shared foundations every part builds on.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { useCounter } from '../src';
|
||||
|
||||
const { count, increment, decrement, reset } = useCounter(0, { min: 0, max: 10 });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="docs-section">
|
||||
<!-- Hero -->
|
||||
<div class="prose-docs">
|
||||
<h1>@robonen/vue</h1>
|
||||
<p>
|
||||
A collection of <strong>213+ tree-shakeable, SSR-safe composables</strong> for Vue 3 —
|
||||
reactive primitives for state, sensors, the DOM, browser APIs, animation, forms and more.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="prose-docs">
|
||||
<p>
|
||||
Every Vue app ends up re-implementing the same building blocks: a toggle, a debounced ref,
|
||||
an event listener that cleans itself up, a media query, local-storage state. @robonen/vue
|
||||
ships those building blocks as small, composable functions with a consistent API. Each one
|
||||
is independently tree-shakeable, written in TypeScript with full inference, and safe to call
|
||||
during server-side rendering — guards for <code>window</code>, <code>document</code> and
|
||||
<code>navigator</code> are built in, so the same code runs on the server and hydrates cleanly
|
||||
on the client.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
From state and reactivity to sensors, elements, storage, math and form handling —
|
||||
one cohesive toolkit spanning the whole surface of a Vue app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Install -->
|
||||
<div class="prose-docs">
|
||||
<h2>Install</h2>
|
||||
</div>
|
||||
<DocsCode :code="`pnpm add @robonen/vue`" lang="bash" />
|
||||
|
||||
<!-- Usage -->
|
||||
<div class="prose-docs">
|
||||
<h2>Quick start</h2>
|
||||
<p>
|
||||
Import the composables you need and use them inside <code><script setup></code>.
|
||||
Here's a counter clamped to a range, with auto-cleaning keyboard shortcuts:
|
||||
</p>
|
||||
</div>
|
||||
<DocsCode
|
||||
:code="`import { useCounter, useEventListener, useToggle } from '@robonen/vue';
|
||||
|
||||
// Clamped, reactive counter
|
||||
const { count, increment, decrement, reset } = useCounter(0, { min: 0, max: 10 });
|
||||
|
||||
// A boolean toggle with custom truthy/falsy values
|
||||
const { value: theme, toggle } = useToggle('light', {
|
||||
truthyValue: 'dark',
|
||||
falsyValue: 'light',
|
||||
});
|
||||
|
||||
// Listener is removed automatically on unmount
|
||||
useEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowUp') increment();
|
||||
if (e.key === 'ArrowDown') decrement();
|
||||
});`"
|
||||
lang="ts"
|
||||
/>
|
||||
|
||||
<!-- Live demo -->
|
||||
<div class="prose-docs">
|
||||
<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">
|
||||
<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"
|
||||
:disabled="count <= 0"
|
||||
@click="decrement()"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<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"
|
||||
:disabled="count >= 10"
|
||||
@click="increment()"
|
||||
>
|
||||
+
|
||||
</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)"
|
||||
@click="reset()"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
|
||||
<!-- Where to next -->
|
||||
<div class="prose-docs">
|
||||
<h2>Where to next</h2>
|
||||
<p>
|
||||
The full API reference is listed right below. A few good starting points:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<NuxtLink to="/vue/use-counter">useCounter</NuxtLink> — a clamped, reactive counter
|
||||
with increment / decrement / set / reset.
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/vue/use-toggle">useToggle</NuxtLink> — a boolean toggle with
|
||||
customizable truthy / falsy values.
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/vue/use-event-listener">useEventListener</NuxtLink> — declarative
|
||||
event listeners that clean up on unmount.
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/vue/use-storage">useStorage</NuxtLink> — reactive state synced to
|
||||
<code>localStorage</code> / <code>sessionStorage</code>.
|
||||
</li>
|
||||
<li>
|
||||
<NuxtLink to="/vue/use-magic-keys">useMagicKeys</NuxtLink> — reactive keyboard
|
||||
state for building shortcuts.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user