@vielzeug/codex 26.10.5 → 26.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "apiSource": "/**\n * Refine components register through their explicit component entry points.\n *\n * Keeping the package root free of registration side effects makes dependency\n * ownership and bundle contents obvious to application code.\n */\nexport { RefineError } from './errors';\nexport { eventFieldChecked, eventFieldValue } from './inputs/shared/native-field-event';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: 'Refine: Web component library'\ndescription: Accessible, themeable web components built with Ore for framework and vanilla DOM apps.\npackage: refine\ncategory: ui-components\nkeywords: [web-components, accessible, themeable, ui, components, design-system]\nrelated: [ore, orbit, forge, keymap]\nexports:\n [\n ore-accordion,\n ore-accordion-item,\n ore-alert,\n ore-async,\n ore-avatar,\n ore-avatar-group,\n ore-badge,\n ore-box,\n ore-breadcrumb,\n ore-breadcrumb-item,\n ore-button,\n ore-button-group,\n ore-calendar,\n ore-card,\n ore-carousel,\n ore-carousel-slide,\n ore-chat-message,\n ore-checkbox,\n ore-checkbox-group,\n ore-chip,\n ore-code-window,\n ore-column,\n ore-combobox,\n ore-combobox-option,\n ore-command-palette,\n ore-command-palette-item,\n ore-cookie-banner,\n ore-copy-command,\n ore-counter,\n ore-datagrid,\n ore-date-picker,\n ore-dialog,\n ore-drawer,\n ore-file-input,\n ore-grid,\n ore-grid-item,\n ore-icon,\n ore-input,\n ore-keyboard-key,\n ore-keyboard-shortcut,\n ore-list,\n ore-list-item,\n ore-marquee,\n ore-menu,\n ore-menu-item,\n ore-menu-separator,\n ore-message-composer,\n ore-navbar,\n ore-navbar-item,\n ore-navigation-menu,\n ore-navigation-menu-item,\n ore-navigation-menu-panel,\n ore-number-input,\n ore-otp-input,\n ore-pagination,\n ore-password-strength,\n ore-popover,\n ore-progress,\n ore-qr-code,\n ore-qr-scanner,\n ore-radio,\n ore-radio-group,\n ore-rating,\n ore-select,\n ore-separator,\n ore-sidebar,\n ore-sidebar-group,\n ore-sidebar-item,\n ore-skeleton,\n ore-slider,\n ore-speech-player,\n ore-stats,\n ore-step,\n ore-stepper,\n ore-switch,\n ore-tab-item,\n ore-tab-panel,\n ore-table,\n ore-tabs,\n ore-td,\n ore-text,\n ore-th,\n ore-tr,\n ore-textarea,\n ore-time-picker,\n ore-toast,\n ore-tooltip,\n ore-typing-indicator,\n ]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"refine\" />\n\n## Why Refine?\n\nEvery project needs UI primitives. Refine provides accessible web components that work natively anywhere HTML is rendered, no framework required.\n\n```html\n<!-- Before: roll your own button with ARIA -->\n<button class=\"btn btn-primary\" role=\"button\" aria-pressed=\"false\" tabindex=\"0\">\n <span class=\"btn-spinner\" aria-hidden=\"true\"></span>\n Save\n</button>\n\n<!-- After: Refine -->\n<ore-button variant=\"solid\" color=\"primary\" loading>Save</ore-button>\n```\n\n| Feature | Refine | Shoelace | Material Web |\n| ------------------ | ------------------------------------------- | ------------------------------------------ | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"refine\" type=\"size\" /> | ~145 kB | ~200 kB |\n| Built with | Ore | Lit | Lit |\n| Runtime dependencies | Vielzeug UI primitives + Lucide | Lit + utilities | Lit + Material foundations |\n| Accessible | WCAG AA | WCAG AA | WCAG AA |\n| Framework agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Refine when** you want accessible web components that match the Vielzeug design system without a heavy framework dependency.\n\n**Consider Shoelace or Material Web** if your team is already standardized on those ecosystems and you need their established component catalogs.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/refine\n```\n\n```sh [npm]\nnpm install @vielzeug/refine\n```\n\n```sh [yarn]\nyarn add @vielzeug/refine\n```\n\n:::\n\n## Quick Start\n\n```ts\n// 1. Import global styles once\nimport '@vielzeug/refine/fouc.css'; // Hide unupgraded custom elements until first paint\nimport '@vielzeug/refine/tokens.css'; // Tokens, animations, cascade layers\n\n// 2. Register only the elements you need\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/card';\n```\n\n```html\n<ore-button variant=\"solid\" color=\"primary\">Save</ore-button>\n<ore-input label=\"Email\" type=\"email\" required></ore-input>\n<ore-card padding=\"lg\">\n <span slot=\"header\">Account</span>\n <p>Card content goes here.</p>\n</ore-card>\n```\n\n```ts\n```\n\n### CDN / Vanilla HTML\n\nUse the self-contained IIFE bundle to load Refine directly from a CDN in any HTML page: no build step required:\n\n```html\n<!-- 1. Styles -->\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/fouc.css\" />\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/tokens.css\" />\n\n<!-- 2. All components (IIFE: registers global Refine namespace) -->\n<script src=\"https://unpkg.com/@vielzeug/refine/dist/refine.iife.js\"></script>\n```\n\nFor bundler-based projects that still want a CDN URL, use the ESM bundle via an import map:\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"@vielzeug/refine\": \"https://esm.sh/@vielzeug/refine\",\n \"@vielzeug/refine/button\": \"https://esm.sh/@vielzeug/refine/button\",\n \"@vielzeug/refine/input\": \"https://esm.sh/@vielzeug/refine/input\"\n }\n }\n</script>\n\n<script type=\"module\">\n import '@vielzeug/refine/button';\n import '@vielzeug/refine/input';\n</script>\n```\n\n### Package Entry Points\n\n| Import | Purpose |\n| ------------------------ | ----------------------------------------- |\n| `@vielzeug/refine/fouc.css` | FOUC suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | Global design tokens and cascade layers |\n| `@vielzeug/refine/styles/preflight.css` | Optional browser-default reset (includes FOUC suppression) |\n\nComponent registration happens through side-effect imports such as `@vielzeug/refine/button` and `@vielzeug/refine/dialog`.\n\n### Components\n\n**Content:** `ore-avatar`, `ore-avatar-group`, `ore-breadcrumb`, `ore-card`, `ore-carousel`, `ore-carousel-slide`, `ore-chat-message`, `ore-icon`, `ore-keyboard-key`, `ore-keyboard-shortcut`, `ore-list`, `ore-list-item`, `ore-marquee`, `ore-pagination`, `ore-qr-code`, `ore-separator`, `ore-stats`, `ore-step`, `ore-stepper`, `ore-table`, `ore-text`\n\n**Disclosure:** `ore-accordion`, `ore-accordion-item`, `ore-tabs`, `ore-tab-item`, `ore-tab-panel`\n\n**Feedback:** `ore-alert`, `ore-async`, `ore-badge`, `ore-chip`, `ore-cookie-banner`, `ore-password-strength`, `ore-progress`, `ore-skeleton`, `ore-toast`, `ore-typing-indicator`\n\n**Inputs:** `ore-button`, `ore-button-group`, `ore-calendar`, `ore-checkbox`, `ore-checkbox-group`, `ore-column`, `ore-combobox`, `ore-counter`, `ore-datagrid`, `ore-date-picker`, `ore-file-input`, `ore-input`, `ore-message-composer`, `ore-number-input`, `ore-otp-input`, `ore-qr-scanner`, `ore-radio`, `ore-radio-group`, `ore-rating`, `ore-select`, `ore-slider`, `ore-switch`, `ore-textarea`, `ore-time-picker`\n\n**Layout:** `ore-box`, `ore-grid`, `ore-grid-item`, `ore-navbar`, `ore-sidebar`\n\n**Overlay:** `ore-command-palette`, `ore-command-palette-item`, `ore-dialog`, `ore-drawer`, `ore-menu`, `ore-popover`, `ore-tooltip`\n\n## Component portfolio\n\nRefine keeps one public package and classifies components by composition level rather than stability. All listed components remain supported and tested.\n\n- **Foundations**: buttons, inputs, selection controls, disclosure, dialogs, menus, tooltips, layout primitives, and feedback indicators. Start here when building an application-specific interface.\n- **Composed patterns**: data grids, command palettes, navigation menus, steppers, calendars, date/time pickers, file inputs, and message composers. These package recurring accessibility and interaction behavior that is costly to rebuild correctly.\n- **Presentation patterns**: cards, chat messages, code windows, stats, marquees, avatars, badges, and skeletons. Use these when their product vocabulary matches the application; prefer foundations when it does not.\n\nClassification guides discovery and bundle selection. It is not a reason to remove working components or move them into unstable packages; per-component subpath imports remain the tree-shaking boundary.\n\n## Features\n\n<div class=\"features-grid\">\n\n- **Accessible**: keyboard navigation, ARIA wiring, and focus management across interactive components\n- **Themeable**: global tokens plus component-level CSS custom properties\n- **Framework agnostic**: works anywhere HTML can be rendered\n- **Tree-shakeable**: import only the component entry points you register\n- **Comprehensive surface**: inputs, content, disclosure, feedback, layout, and overlay primitives\n- **Typed framework declarations**: DOM, React, and Vue tags derive from the component property contracts\n\n</div>\n\n### Prerequisites\n\n- Browser runtime with Custom Elements support.\n- Import `@vielzeug/refine/fouc.css` and `@vielzeug/refine/tokens.css` before rendering components.\n- For SSR, render placeholders server-side and hydrate components only on the client.\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/): Web component runtime that powers Refine\n- [Orbit](/orbit/): Floating UI positioning used in Refine's overlays\n- [Forge](/forge/): Form state management for use with Refine inputs\n- [Keymap](/keymap/): Keyboard shortcut manager that powers the command palette's global trigger\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: 'Refine: API Reference'\ndescription: Published component, stylesheet, framework type, and error entry points for @vielzeug/refine.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `@vielzeug/refine/<component>` | Register one custom element and export its public types | Sync | Importing a type alone does not register the element |\n| `@vielzeug/refine/tokens.css` | Load required theme tokens, animations, and cascade layers | CSS | Import once before components render |\n| `@vielzeug/refine/fouc.css` | Hide unregistered `ore-*` elements until upgrade | CSS | Load before first paint |\n| `@vielzeug/refine/styles/preflight.css` | Apply the optional browser reset | CSS | The reset affects global elements |\n| `@vielzeug/refine/frameworks/elements` | Register typed DOM tag mappings | Types only | Import for side effects in TypeScript |\n| `@vielzeug/refine/frameworks/react` | Register typed React JSX elements | Types only | Does not provide runtime wrappers |\n| `@vielzeug/refine/frameworks/vue` | Register typed Vue global components | Types only | Does not install a Vue plugin |\n| `@vielzeug/refine/theme` | `createThemeController()`: light/dark/system document theme | Sync | Does not persist the preference or set an accent color |\n| `eventFieldValue()` / `eventFieldChecked()` | Read a form control's value/checked from `event.currentTarget` | Sync | Exported from the package root; guards non-field targets |\n| `RefineError` | Base class for package-defined public errors | Sync | Component configuration warnings do not throw this error |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/refine` | Export `RefineError` and the `eventFieldValue`/`eventFieldChecked` field readers without registering components |\n| `@vielzeug/refine/<component>` | Register one component and export its tag constant, props, events, and related types |\n| `@vielzeug/refine/theme` | `createThemeController()` and its theme types |\n| `@vielzeug/refine/tokens.css` | Required global design contract |\n| `@vielzeug/refine/fouc.css` | Optional pre-upgrade visibility rule |\n| `@vielzeug/refine/styles/*` | Focused theme, animation, layer, and preflight stylesheets |\n| `@vielzeug/refine/frameworks/*` | TypeScript augmentations for DOM, React, and Vue |\n\n## Styles\n\n```ts\nimport '@vielzeug/refine/fouc.css';\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n`tokens.css` is required. It defines tokens, animations, and cascade-layer order without resetting global elements. `preflight.css` is optional and includes FOUC suppression.\n\n| Import path | Purpose |\n| --- | --- |\n| `@vielzeug/refine/fouc.css` | FOUC suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | Tokens, animation helpers, and cascade layers |\n| `@vielzeug/refine/styles/theme.css` | Theme token declarations |\n| `@vielzeug/refine/styles/animation.css` | Animation helpers |\n| `@vielzeug/refine/styles/layers.css` | Cascade layer declarations |\n| `@vielzeug/refine/styles/preflight.css` | Optional browser reset and FOUC suppression |\n\n## Components\n\nEach component uses one registration and type entry point:\n\n```ts\nimport '@vielzeug/refine/button';\nimport type { OreButtonProps } from '@vielzeug/refine/button';\n```\n\n| Area | Components |\n| --- | --- |\n| Content | `avatar`, `avatar-group`, `breadcrumb`, `card`, `carousel`, `chat-message`, `code-window`, `copy-command`, `icon`, `keyboard-key`, `list`, `list-item`, `marquee`, `pagination`, `separator`, `speech-player`, `stats`, `step`, `stepper`, `table`, `text` |\n| Disclosure | `accordion`, `accordion-item`, `tabs`, `tab-item`, `tab-panel` |\n| Feedback | `alert`, `async`, `badge`, `chip`, `cookie-banner`, `password-strength`, `progress`, `skeleton`, `toast`, `typing-indicator` |\n| Inputs | `button`, `button-group`, `calendar`, `checkbox`, `checkbox-group`, `combobox`, `counter`, `datagrid`, `date-picker`, `file-input`, `input`, `message-composer`, `number-input`, `otp-input`, `radio`, `radio-group`, `rating`, `select`, `slider`, `switch`, `textarea`, `time-picker` |\n| Layout | `box`, `grid`, `grid-item`, `navbar`, `sidebar` |\n| Overlays | `command-palette`, `dialog`, `drawer`, `menu`, `navigation-menu`, `popover`, `tooltip` |\n\nEach component page lists its attributes, JavaScript properties, events, slots, parts, and CSS custom properties.\n\n## Framework Types\n\n```ts\nimport type {} from '@vielzeug/refine/frameworks/elements';\nimport type {} from '@vielzeug/refine/frameworks/react';\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\nThe declarations cover every supported tag and derive component properties from Refine's authoritative `HTMLElementTagNameMap`. They provide types only; component registration still uses component subpaths.\n\n## Events and Form Controls\n\nForm controls expose `.value` or `.checked` and dispatch standard `input` and `change` events. Read the property from `event.currentTarget`.\n\nStateful overlays expose controlled `open`, optional `default-open`, and an `open-change` custom event with `{ open, reason }` detail. Component pages define additional detail fields and reasons.\n\n## Utilities\n\n### `createThemeController(options?)`\n\n```ts\nimport { createThemeController } from '@vielzeug/refine/theme';\n\nconst theme = createThemeController(); // starts in 'system' mode\ntheme.setPreference('dark');\ntheme.dispose();\n```\n\nOwns the light/dark/system theme for the document: a reactive `preference` signal, the effective `resolved` mode (`'dark' | 'light'` after resolving `system` against the OS), and the DOM application `styles/theme.css` expects: the `.dark` class plus the `color-scheme` property on the root element, so every `light-dark()` token resolves to the right branch. While in `system` mode the controller tracks the OS `prefers-color-scheme` via sentinel's `createMediaQuery`.\n\nThe controller does **not** persist the preference or expose an accent color: those are consumer concerns (wire `watch(controller.preference, save)` for persistence; set `--color-primary-hue` yourself). Options: `initial` (starting preference, default `'system'`), `root` (element carrying the classes, default `document.documentElement`), `target` (window whose `matchMedia` backs `system`). The handle is disposable: `dispose()`, `disposed`, `disposalSignal`, `[Symbol.dispose]`.\n\n### `eventFieldValue(event)` / `eventFieldChecked(event)`\n\n```ts\nimport { eventFieldChecked, eventFieldValue } from '@vielzeug/refine';\n\ninput.addEventListener('input', (event) => {\n const value = eventFieldValue(event); // string | undefined: never a blind cast\n});\n```\n\nGuarded readers for the standard `input`/`change` event pattern above: they read `value`/`checked` from `event.currentTarget` only when the target actually carries it, so a handler wired to the wrong element yields `undefined`/`false` instead of a crash or a lie. Exported from the package root, which registers no components.\n\n## Types\n\nComponent props follow the `Ore<Component>Props` naming pattern. Event maps follow `Ore<Component>Events`. Import both from the component subpath that owns them.\n\n`RefineElementMap`, `RefineReactIntrinsicElements`, and `RefineVueGlobalComponents` are exported from their respective `frameworks/*` type entry points.\n\n## Errors\n\n### `RefineError`\n\n```ts\nclass RefineError extends Error {\n constructor(message: string, opts?: ErrorOptions);\n}\n```\n\nBase class for public Refine errors. It preserves `cause` through `ErrorOptions`.\n",
4
+ "index": "---\ntitle: 'Refine: Web component library'\ndescription: Accessible, themeable web components built with Ore for framework and vanilla DOM apps.\npackage: refine\ncategory: ui-components\nkeywords: [web-components, accessible, themeable, ui, components, design-system]\nrelated: [ore, orbit, forge, keymap]\nexports:\n [\n ore-accordion,\n ore-accordion-item,\n ore-alert,\n ore-async,\n ore-avatar,\n ore-avatar-group,\n ore-badge,\n ore-box,\n ore-breadcrumb,\n ore-breadcrumb-item,\n ore-button,\n ore-button-group,\n ore-calendar,\n ore-card,\n ore-carousel,\n ore-carousel-slide,\n ore-chat-message,\n ore-checkbox,\n ore-checkbox-group,\n ore-chip,\n ore-code-window,\n ore-column,\n ore-combobox,\n ore-combobox-option,\n ore-command-palette,\n ore-command-palette-item,\n ore-cookie-banner,\n ore-copy-command,\n ore-counter,\n ore-datagrid,\n ore-date-picker,\n ore-dialog,\n ore-drawer,\n ore-file-input,\n ore-grid,\n ore-grid-item,\n ore-icon,\n ore-input,\n ore-keyboard-key,\n ore-keyboard-shortcut,\n ore-list,\n ore-list-item,\n ore-marquee,\n ore-menu,\n ore-menu-item,\n ore-menu-separator,\n ore-message-composer,\n ore-navbar,\n ore-navbar-item,\n ore-navigation-menu,\n ore-navigation-menu-item,\n ore-navigation-menu-panel,\n ore-number-input,\n ore-otp-input,\n ore-pagination,\n ore-password-strength,\n ore-popover,\n ore-progress,\n ore-qr-code,\n ore-qr-scanner,\n ore-query-bar,\n ore-radio,\n ore-radio-group,\n ore-rating,\n ore-select,\n ore-separator,\n ore-sidebar,\n ore-sidebar-group,\n ore-sidebar-item,\n ore-skeleton,\n ore-slider,\n ore-speech-player,\n ore-stats,\n ore-step,\n ore-stepper,\n ore-switch,\n ore-tab-item,\n ore-tab-panel,\n ore-table,\n ore-tabs,\n ore-td,\n ore-text,\n ore-th,\n ore-tr,\n ore-textarea,\n ore-time-picker,\n ore-toast,\n ore-tooltip,\n ore-typing-indicator,\n ]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"refine\" />\n\n## Why Refine?\n\nEvery project needs UI primitives. Refine provides accessible web components that work natively anywhere HTML is rendered, no framework required.\n\n```html\n<!-- Before: roll your own button with ARIA -->\n<button class=\"btn btn-primary\" role=\"button\" aria-pressed=\"false\" tabindex=\"0\">\n <span class=\"btn-spinner\" aria-hidden=\"true\"></span>\n Save\n</button>\n\n<!-- After: Refine -->\n<ore-button variant=\"solid\" color=\"primary\" loading>Save</ore-button>\n```\n\n| Feature | Refine | Shoelace | Material Web |\n| ------------------ | ------------------------------------------- | ------------------------------------------ | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"refine\" type=\"size\" /> | ~145 kB | ~200 kB |\n| Built with | Ore | Lit | Lit |\n| Runtime dependencies | Vielzeug UI primitives + Lucide | Lit + utilities | Lit + Material foundations |\n| Accessible | WCAG AA | WCAG AA | WCAG AA |\n| Framework agnostic | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Refine when** you want accessible web components that match the Vielzeug design system without a heavy framework dependency.\n\n**Consider Shoelace or Material Web** if your team is already standardized on those ecosystems and you need their established component catalogs.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/refine\n```\n\n```sh [npm]\nnpm install @vielzeug/refine\n```\n\n```sh [yarn]\nyarn add @vielzeug/refine\n```\n\n:::\n\n## Quick Start\n\n```ts\n// 1. Import global styles once\nimport '@vielzeug/refine/fouc.css'; // Hide unupgraded custom elements until first paint\nimport '@vielzeug/refine/tokens.css'; // Tokens, animations, cascade layers\n\n// 2. Register only the elements you need\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/card';\n```\n\n```html\n<ore-button variant=\"solid\" color=\"primary\">Save</ore-button>\n<ore-input label=\"Email\" type=\"email\" required></ore-input>\n<ore-card padding=\"lg\">\n <span slot=\"header\">Account</span>\n <p>Card content goes here.</p>\n</ore-card>\n```\n\n```ts\n```\n\n### CDN / Vanilla HTML\n\nUse the self-contained IIFE bundle to load Refine directly from a CDN in any HTML page: no build step required:\n\n```html\n<!-- 1. Styles -->\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/fouc.css\" />\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@vielzeug/refine/dist/styles/tokens.css\" />\n\n<!-- 2. All components (IIFE: registers global Refine namespace) -->\n<script src=\"https://unpkg.com/@vielzeug/refine/dist/refine.iife.js\"></script>\n```\n\nFor bundler-based projects that still want a CDN URL, use the ESM bundle via an import map:\n\n```html\n<script type=\"importmap\">\n {\n \"imports\": {\n \"@vielzeug/refine\": \"https://esm.sh/@vielzeug/refine\",\n \"@vielzeug/refine/button\": \"https://esm.sh/@vielzeug/refine/button\",\n \"@vielzeug/refine/input\": \"https://esm.sh/@vielzeug/refine/input\"\n }\n }\n</script>\n\n<script type=\"module\">\n import '@vielzeug/refine/button';\n import '@vielzeug/refine/input';\n</script>\n```\n\n### Package Entry Points\n\n| Import | Purpose |\n| ------------------------ | ----------------------------------------- |\n| `@vielzeug/refine/fouc.css` | FOUC suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | Global design tokens and cascade layers |\n| `@vielzeug/refine/styles/preflight.css` | Optional browser-default reset (includes FOUC suppression) |\n\nComponent registration happens through side-effect imports such as `@vielzeug/refine/button` and `@vielzeug/refine/dialog`.\n\n### Components\n\n**Content:** `ore-avatar`, `ore-avatar-group`, `ore-breadcrumb`, `ore-card`, `ore-carousel`, `ore-carousel-slide`, `ore-chat-message`, `ore-icon`, `ore-keyboard-key`, `ore-keyboard-shortcut`, `ore-list`, `ore-list-item`, `ore-marquee`, `ore-pagination`, `ore-qr-code`, `ore-separator`, `ore-stats`, `ore-step`, `ore-stepper`, `ore-table`, `ore-text`\n\n**Disclosure:** `ore-accordion`, `ore-accordion-item`, `ore-tabs`, `ore-tab-item`, `ore-tab-panel`\n\n**Feedback:** `ore-alert`, `ore-async`, `ore-badge`, `ore-chip`, `ore-cookie-banner`, `ore-password-strength`, `ore-progress`, `ore-skeleton`, `ore-toast`, `ore-typing-indicator`\n\n**Inputs:** `ore-button`, `ore-button-group`, `ore-calendar`, `ore-checkbox`, `ore-checkbox-group`, `ore-column`, `ore-combobox`, `ore-counter`, `ore-datagrid`, `ore-date-picker`, `ore-file-input`, `ore-input`, `ore-message-composer`, `ore-number-input`, `ore-otp-input`, `ore-qr-scanner`, `ore-query-bar`, `ore-radio`, `ore-radio-group`, `ore-rating`, `ore-select`, `ore-slider`, `ore-switch`, `ore-textarea`, `ore-time-picker`\n\n**Layout:** `ore-box`, `ore-grid`, `ore-grid-item`, `ore-navbar`, `ore-sidebar`\n\n**Overlay:** `ore-command-palette`, `ore-command-palette-item`, `ore-dialog`, `ore-drawer`, `ore-menu`, `ore-popover`, `ore-tooltip`\n\n## Component portfolio\n\nRefine keeps one public package and classifies components by composition level rather than stability. All listed components remain supported and tested.\n\n- **Foundations**: buttons, inputs, selection controls, disclosure, dialogs, menus, tooltips, layout primitives, and feedback indicators. Start here when building an application-specific interface.\n- **Composed patterns**: data grids, command palettes, navigation menus, steppers, calendars, date/time pickers, file inputs, and message composers. These package recurring accessibility and interaction behavior that is costly to rebuild correctly.\n- **Presentation patterns**: cards, chat messages, code windows, stats, marquees, avatars, badges, and skeletons. Use these when their product vocabulary matches the application; prefer foundations when it does not.\n\nClassification guides discovery and bundle selection. It is not a reason to remove working components or move them into unstable packages; per-component subpath imports remain the tree-shaking boundary.\n\n## Features\n\n<div class=\"features-grid\">\n\n- **Accessible**: keyboard navigation, ARIA wiring, and focus management across interactive components\n- **Themeable**: global tokens plus component-level CSS custom properties\n- **Framework agnostic**: works anywhere HTML can be rendered\n- **Tree-shakeable**: import only the component entry points you register\n- **Comprehensive surface**: inputs, content, disclosure, feedback, layout, and overlay primitives\n- **Typed framework declarations**: DOM, React, and Vue tags derive from the component property contracts\n\n</div>\n\n### Prerequisites\n\n- Browser runtime with Custom Elements support.\n- Import `@vielzeug/refine/fouc.css` and `@vielzeug/refine/tokens.css` before rendering components.\n- For SSR, render placeholders server-side and hydrate components only on the client.\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ore](/ore/): Web component runtime that powers Refine\n- [Orbit](/orbit/): Floating UI positioning used in Refine's overlays\n- [Forge](/forge/): Form state management for use with Refine inputs\n- [Keymap](/keymap/): Keyboard shortcut manager that powers the command palette's global trigger\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'Refine: API Reference'\ndescription: Published component, stylesheet, framework type, and error entry points for @vielzeug/refine.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `@vielzeug/refine/<component>` | Register one custom element and export its public types | Sync | Importing a type alone does not register the element |\n| `@vielzeug/refine/tokens.css` | Load required theme tokens, animations, and cascade layers | CSS | Import once before components render |\n| `@vielzeug/refine/fouc.css` | Hide unregistered `ore-*` elements until upgrade | CSS | Load before first paint |\n| `@vielzeug/refine/styles/preflight.css` | Apply the optional browser reset | CSS | The reset affects global elements |\n| `@vielzeug/refine/frameworks/elements` | Register typed DOM tag mappings | Types only | Import for side effects in TypeScript |\n| `@vielzeug/refine/frameworks/react` | Register typed React JSX elements | Types only | Does not provide runtime wrappers |\n| `@vielzeug/refine/frameworks/vue` | Register typed Vue global components | Types only | Does not install a Vue plugin |\n| `@vielzeug/refine/theme` | `createThemeController()`: light/dark/system document theme | Sync | Does not persist the preference or set an accent color |\n| `eventFieldValue()` / `eventFieldChecked()` | Read a form control's value/checked from `event.currentTarget` | Sync | Exported from the package root; guards non-field targets |\n| `RefineError` | Base class for package-defined public errors | Sync | Component configuration warnings do not throw this error |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/refine` | Export `RefineError` and the `eventFieldValue`/`eventFieldChecked` field readers without registering components |\n| `@vielzeug/refine/<component>` | Register one component and export its tag constant, props, events, and related types |\n| `@vielzeug/refine/theme` | `createThemeController()` and its theme types |\n| `@vielzeug/refine/tokens.css` | Required global design contract |\n| `@vielzeug/refine/fouc.css` | Optional pre-upgrade visibility rule |\n| `@vielzeug/refine/styles/*` | Focused theme, animation, layer, and preflight stylesheets |\n| `@vielzeug/refine/frameworks/*` | TypeScript augmentations for DOM, React, and Vue |\n\n## Styles\n\n```ts\nimport '@vielzeug/refine/fouc.css';\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n`tokens.css` is required. It defines tokens, animations, and cascade-layer order without resetting global elements. `preflight.css` is optional and includes FOUC suppression.\n\n| Import path | Purpose |\n| --- | --- |\n| `@vielzeug/refine/fouc.css` | FOUC suppression for unupgraded custom elements |\n| `@vielzeug/refine/tokens.css` | Tokens, animation helpers, and cascade layers |\n| `@vielzeug/refine/styles/theme.css` | Theme token declarations |\n| `@vielzeug/refine/styles/animation.css` | Animation helpers |\n| `@vielzeug/refine/styles/layers.css` | Cascade layer declarations |\n| `@vielzeug/refine/styles/preflight.css` | Optional browser reset and FOUC suppression |\n\n## Components\n\nEach component uses one registration and type entry point:\n\n```ts\nimport '@vielzeug/refine/button';\nimport type { OreButtonProps } from '@vielzeug/refine/button';\n```\n\n| Area | Components |\n| --- | --- |\n| Content | `avatar`, `avatar-group`, `breadcrumb`, `card`, `carousel`, `chat-message`, `code-window`, `copy-command`, `icon`, `keyboard-key`, `list`, `list-item`, `marquee`, `pagination`, `separator`, `speech-player`, `stats`, `step`, `stepper`, `table`, `text` |\n| Disclosure | `accordion`, `accordion-item`, `tabs`, `tab-item`, `tab-panel` |\n| Feedback | `alert`, `async`, `badge`, `chip`, `cookie-banner`, `password-strength`, `progress`, `skeleton`, `toast`, `typing-indicator` |\n| Inputs | `button`, `button-group`, `calendar`, `checkbox`, `checkbox-group`, `combobox`, `counter`, `datagrid`, `date-picker`, `file-input`, `input`, `message-composer`, `number-input`, `otp-input`, `query-bar`, `radio`, `radio-group`, `rating`, `select`, `slider`, `switch`, `textarea`, `time-picker` |\n| Layout | `box`, `grid`, `grid-item`, `navbar`, `sidebar` |\n| Overlays | `command-palette`, `dialog`, `drawer`, `menu`, `navigation-menu`, `popover`, `tooltip` |\n\nEach component page lists its attributes, JavaScript properties, events, slots, parts, and CSS custom properties.\n\n## Framework Types\n\n```ts\nimport type {} from '@vielzeug/refine/frameworks/elements';\nimport type {} from '@vielzeug/refine/frameworks/react';\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\nThe declarations cover every supported tag and derive component properties from Refine's authoritative `HTMLElementTagNameMap`. They provide types only; component registration still uses component subpaths.\n\n## Events and Form Controls\n\nForm controls expose `.value` or `.checked` and dispatch standard `input` and `change` events. Read the property from `event.currentTarget`.\n\nStateful overlays expose controlled `open`, optional `default-open`, and an `open-change` custom event with `{ open, reason }` detail. Component pages define additional detail fields and reasons.\n\n## Utilities\n\n### `createThemeController(options?)`\n\n```ts\nimport { createThemeController } from '@vielzeug/refine/theme';\n\nconst theme = createThemeController(); // starts in 'system' mode\ntheme.setPreference('dark');\ntheme.dispose();\n```\n\nOwns the light/dark/system theme for the document: a reactive `preference` signal, the effective `resolved` mode (`'dark' | 'light'` after resolving `system` against the OS), and the DOM application `styles/theme.css` expects: the `.dark` class plus the `color-scheme` property on the root element, so every `light-dark()` token resolves to the right branch. While in `system` mode the controller tracks the OS `prefers-color-scheme` via sentinel's `createMediaQuery`.\n\nThe controller does **not** persist the preference or expose an accent color: those are consumer concerns (wire `watch(controller.preference, save)` for persistence; set `--color-primary-hue` yourself). Options: `initial` (starting preference, default `'system'`), `root` (element carrying the classes, default `document.documentElement`), `target` (window whose `matchMedia` backs `system`). The handle is disposable: `dispose()`, `disposed`, `disposalSignal`, `[Symbol.dispose]`.\n\n### `eventFieldValue(event)` / `eventFieldChecked(event)`\n\n```ts\nimport { eventFieldChecked, eventFieldValue } from '@vielzeug/refine';\n\ninput.addEventListener('input', (event) => {\n const value = eventFieldValue(event); // string | undefined: never a blind cast\n});\n```\n\nGuarded readers for the standard `input`/`change` event pattern above: they read `value`/`checked` from `event.currentTarget` only when the target actually carries it, so a handler wired to the wrong element yields `undefined`/`false` instead of a crash or a lie. Exported from the package root, which registers no components.\n\n## Types\n\nComponent props follow the `Ore<Component>Props` naming pattern. Event maps follow `Ore<Component>Events`. Import both from the component subpath that owns them.\n\n`RefineElementMap`, `RefineReactIntrinsicElements`, and `RefineVueGlobalComponents` are exported from their respective `frameworks/*` type entry points.\n\n## Errors\n\n### `RefineError`\n\n```ts\nclass RefineError extends Error {\n constructor(message: string, opts?: ErrorOptions);\n}\n```\n\nBase class for public Refine errors. It preserves `cause` through `ErrorOptions`.\n",
6
6
  "usage": "---\ntitle: 'Refine: Usage Guide'\ndescription: Register Refine components, configure properties, handle events, compose slots, and integrate frameworks.\n---\n\n[[toc]]\n\n## Basic Usage\n\nLoad the global design contract once, then register only the components used by the page.\n\n```ts\nimport '@vielzeug/refine/tokens.css';\nimport '@vielzeug/refine/button';\nimport '@vielzeug/refine/input';\nimport '@vielzeug/refine/dialog';\n\nconst input = document.querySelector('ore-input');\nconst save = document.querySelector('ore-button');\n\nif (!(input instanceof HTMLElement) || !(save instanceof HTMLElement)) {\n throw new Error('Expected Refine controls');\n}\n\nsave.addEventListener('click', () => {\n console.log((input as HTMLElement & { value: string }).value);\n});\n```\n\nAdd the browser reset only when the application owns global element styling:\n\n```ts\nimport '@vielzeug/refine/styles/preflight.css';\n```\n\n## Set Attributes and Properties\n\nUse attributes for serializable HTML state.\n\n```html\n<ore-button variant=\"outline\" color=\"secondary\" size=\"lg\" disabled>\n Save changes\n</ore-button>\n```\n\nUse JavaScript properties for arrays, objects, callbacks, and controlled state.\n\n```ts\nimport '@vielzeug/refine/datagrid';\nimport type { OreDataGridProps } from '@vielzeug/refine/datagrid';\n\nconst grid = document.querySelector('ore-datagrid') as HTMLElement & OreDataGridProps;\ngrid.columns = [\n { key: 'name', label: 'Name', sortable: true },\n { key: 'role', label: 'Role' },\n];\ngrid.rows = [\n { id: '1', name: 'Ada', role: 'Admin' },\n { id: '2', name: 'Linus', role: 'Editor' },\n];\n```\n\n## Handle Events\n\nForm controls dispatch standard `input` and `change` events. Read their current property from `event.currentTarget`.\n\n```ts\nconst input = document.querySelector('ore-input');\ninput?.addEventListener('input', (event) => {\n const target = event.currentTarget as HTMLElement & { value: string };\n console.log(target.value);\n});\n```\n\nCustom events use kebab-case names and typed detail objects.\n\n```ts\nimport type { OreDialogEvents } from '@vielzeug/refine/dialog';\n\nconst dialog = document.querySelector('ore-dialog');\ndialog?.addEventListener('open-change', (event: CustomEvent<OreDialogEvents['open-change']>) => {\n console.log(event.detail.open, event.detail.reason);\n});\n```\n\n## Compose Slots\n\nDefault slots hold primary content. Named slots identify component-owned regions.\n\n```html\n<ore-card>\n <span slot=\"header\">Account</span>\n <p>Profile settings</p>\n <div slot=\"footer\">\n <ore-button variant=\"ghost\">Cancel</ore-button>\n <ore-button>Save</ore-button>\n </div>\n</ore-card>\n```\n\nInput components commonly expose `prefix`, `suffix`, `label`, `helper`, and `error` slots. Each component reference lists its supported slots.\n\n## Load TypeScript Framework Declarations\n\nImport the declaration matching the framework once in an application type entry point.\n\n::: code-group\n\n```ts [DOM]\nimport type {} from '@vielzeug/refine/frameworks/elements';\n```\n\n```ts [React]\nimport type {} from '@vielzeug/refine/frameworks/react';\n```\n\n```ts [Vue]\nimport type {} from '@vielzeug/refine/frameworks/vue';\n```\n\n:::\n\nThese entry points provide typed tags and component properties. They do not register components or install runtime adapters.\n\n## Framework Integration\n\nRefine elements use native DOM properties, events, and slots. Frameworks differ only in how they forward custom-element properties and events.\n\n::: code-group\n\n```tsx [React 19]\nimport '@vielzeug/refine/button';\n\nexport function SaveButton() {\n return <ore-button variant=\"solid\">Save</ore-button>;\n}\n```\n\n```vue [Vue 3]\n<script setup lang=\"ts\">\nimport '@vielzeug/refine/button';\n</script>\n\n<template>\n <ore-button variant=\"solid\">Save</ore-button>\n</template>\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import '@vielzeug/refine/button';\n</script>\n\n<ore-button variant=\"solid\">Save</ore-button>\n```\n\n:::\n\nSee [Framework Integration](./frameworks.md) for custom events, controlled properties, SSR guards, and framework-specific configuration.\n\n## Working with Other Vielzeug Libraries\n\n### Ore\n\nUse Ore when authoring a custom element that composes Refine components.\n\n```ts\nimport '@vielzeug/refine/button';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('save-panel', {\n setup() {\n return html`<ore-button @click=${() => console.log('save')}>Save</ore-button>`;\n },\n});\n```\n\n### Ripple\n\nUse Ripple to drive component properties from application state.\n\n```ts\nimport '@vielzeug/refine/button';\nimport { effect, signal } from '@vielzeug/ripple';\n\nconst loading = signal(false);\nconst button = document.querySelector('ore-button') as HTMLElement & { loading?: boolean };\nconst loadingEffect = effect(() => {\n button.loading = loading.value;\n});\n\nloadingEffect.dispose();\n```\n\n## Best Practices\n\n- **Import** `tokens.css` once before rendering components.\n- **Register** components through explicit subpaths.\n- **Set** structured data and callbacks as JavaScript properties.\n- **Read** form state from the event target instead of custom event payloads.\n- **Provide** accessible labels for icon-only controls.\n- **Use** component reference pages for supported slots, parts, and CSS properties.\n- **Dispose** application-owned Ripple effects and service handles.\n- **Guard** DOM-dependent component registration during SSR.\n- **Working with AI agents?** Install the `vielzeug` skill (`npx -y @vielzeug/codex skills install`) so agents reach for Refine components and tokens instead of custom widgets: see the [Codex usage guide](../codex/usage.md#agent-skill).\n"
7
7
  },
8
8
  "examples": [],
@@ -4,7 +4,7 @@
4
4
  "index": "---\ntitle: 'Tandem: Offline-first sync engine'\ndescription: Offline-first sync engine with rev baselines, tombstoned deletions, and idle-batched pushes for browser applications.\npackage: tandem\ncategory: sync\nkeywords: [sync, offline, conflict, tombstone, rev, replication, last-write-wins]\nrelated: [vault, postmaster, sentinel, ripple]\nexports: [createSync, SyncPort, SyncGateway, SyncHandle, TandemEvent, TandemError, TandemDisposedError]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"tandem\" />\n\n## Why Tandem?\n\nA local-first app keeps its records in IndexedDB but must still reach a server: batch bursts of edits into one upload, propagate deletions that a plain pull would resurrect, and reconcile two devices that edited the same record while offline. Tandem drives that round trip against any backend, tracking a per-record write counter so a push carries only what actually changed.\n\n```ts\n// Before\n// Hand-rolled: debounce each save, POST the whole table, hope deletions survive.\nlet timer;\nfunction onSave(record) {\n clearTimeout(timer);\n timer = setTimeout(() => fetch('/sync', { method: 'POST', body: JSON.stringify(allRecords()) }), 3000);\n}\n\n// After\nimport { createSync } from '@vielzeug/tandem';\n\nconst sync = createSync({ gateway, port, idleDelayMs: 3000 });\nsync.changed(); // a local edit happened: the engine batches and pushes dirty records\n```\n\n| Feature | Tandem | RxSync | PowerSync |\n| ------------------------------------ | ------------------------------------------ | ------------------- | -------------------- |\n| Bundle size | <PackageInfo package=\"tandem\" type=\"size\" /> | <ore-icon name=\"x\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> |\n| Backend-agnostic (bring your `port`) | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> |\n| Storage-agnostic (bring your `gateway`) | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"x\" size=\"16\" /> | <ore-icon name=\"check\" size=\"16\" /> |\n| Tombstoned deletions + rev conflicts | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"check\" size=\"16\" /> | <ore-icon name=\"check\" size=\"16\" /> |\n\n<div class=\"decision-callout\">\n\n**Use `tandem` when** you already own a local store and a backend and want the sync scheduler: dirty tracking, batching, hide-flows, and conflict reconcile: without adopting a database or a server.\n\n**Consider `PowerSync` when** you want a managed sync service with its own SQLite client and server component rather than a scheduler over your existing storage.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/tandem\n```\n\n```sh [npm]\nnpm install @vielzeug/tandem\n```\n\n```sh [yarn]\nyarn add @vielzeug/tandem\n```\n\n:::\n\n## Quick Start\n\nImplement the two seams: a `SyncPort` for the server and a `SyncGateway` for local storage: then start the scheduler. Records are opaque to Tandem; it reads only `id` and `rev`, and every rev comparison is the engine's: the gateway upserts, validates, and tombstones.\n\n```ts\nimport { createSync, type SyncGateway, type SyncPort } from '@vielzeug/tandem';\n\nconst port: SyncPort = {\n pull: (since) => fetch(`/sync?since=${since ?? ''}`).then((r) => r.json()),\n push: async (records, deletions) => {\n const response = await fetch('/sync', { method: 'POST', body: JSON.stringify({ records, deletions }) });\n if (!response.ok) throw new Error(await response.text()); // divergence → the engine pulls to reconcile\n },\n};\n\nconst gateway: SyncGateway = {\n records: () => localRecords.map((record) => ({ entity: 'docs', record })),\n pendingDeletions: () => readTombstones(),\n applyRecords: (pulled) => upsert(pulled), // the engine pre-filters; you validate untrusted records\n applyDeletions: (deletions) => removeLocal(deletions),\n clearDeletions: (deletions) => dropTombstones(deletions),\n loadState: () => readSyncState(),\n saveState: (state) => writeSyncState(state),\n};\n\nconst sync = createSync({ gateway, port });\nsync.tap((event) => console.debug('sync', event)); // observe pushes, pulls, warnings\nmyStore.subscribe(() => sync.changed()); // a local write happened\n\n// On account switch or teardown: after flushing this account's work:\nawait sync.flush();\nsync.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`createSync()`**: drives a port against a gateway with a serialized push queue.\n- **`SyncPort`**: the server seam: `pull(since)` and `push(records, deletions)`.\n- **`SyncGateway`**: the storage seam: upserts, tombstones, and the persisted baseline: the engine owns every rev comparison.\n- **`rev` baselines**: a record is dirty exactly when its rev is above the last-synced one, across reloads.\n- **`changed()`**: wire it to your write path; the engine debounces a burst into one push.\n- **`flush()`**: a full cycle on demand: pull remote changes, then push dirty records; rejects on failure.\n- **Hide-flows**: flushes with `keepalive` on `pagehide`/`hidden`, runs a full cycle on return to the foreground.\n- **`tap()`**: typed `TandemEvent`s for pushes, pulls, invalid records, and warnings.\n- **Lifecycle-owned**: `dispose()`, `disposed`, `disposalSignal`, and `[Symbol.dispose]`; post-dispose `changed()`/`flush()` fail with `TandemDisposedError`.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Vault](/vault/): the typed storage core most gateways wrap; its IndexedDB adapter is a natural `records()`/`applyRecords()` backend.\n- [Postmaster](/postmaster/): the durable outbox half of offline sync; pair it with Tandem for at-least-once mutations alongside state sync.\n- [Sentinel](/sentinel/): subscribable browser state; its network signal can gate when you call `flush()`.\n- [Ripple](/ripple/): signals and effects; `store.subscribe(() => sync.changed())` is the usual wiring.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
5
  "api": "---\ntitle: 'Tandem: API Reference'\ndescription: Factory, port and gateway contracts, events, and types for Tandem.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createSync()` | Scheduler driving a port against a gateway | Sync (queues async work) | Boot runs pull-then-push; dispose before switching accounts |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/tandem` | `createSync`, `TandemError`, `TandemDisposedError`, port/gateway contracts, events, and types |\n\n## Factories\n\n### `createSync()`\n\n```ts\nfunction createSync<TRecord extends SyncRecordBase = SyncRecordBase>(\n options: SyncOptions<TRecord>,\n): SyncHandle;\n```\n\nStarts a sync scheduler: it loads the persisted baseline, pulls remote changes, pushes anything dirty, then flushes on `changed()`, on tab hide, and on foreground return.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options.gateway` | `SyncGateway<TRecord>` | The device's storage seam |\n| `options.idleDelayMs` | `number \\| undefined` | Quiet period before a flush (default `3000`) |\n| `options.port` | `SyncPort<TRecord>` | The server seam |\n\n**Returns** a `SyncHandle`. Ambient flushes never reject: they surface failures as `warning` events through `tap()`. An explicit `flush()` rejects when its cycle fails.\n\n**Example**\n\n```ts\nimport { createSync } from '@vielzeug/tandem';\n\nconst sync = createSync({ gateway, idleDelayMs: 1500, port });\nsync.changed();\nawait sync.flush();\nsync.dispose();\n```\n\n#### `SyncHandle` methods\n\n| Member | Description |\n| --- | --- |\n| `changed()` | Notes a local change and (re)starts the idle flush timer; throws `TandemDisposedError` after `dispose` |\n| `dispose()` | Stops the scheduler, clears the timer, removes document/window listeners |\n| `disposed` | `true` once disposed |\n| `disposalSignal` | `AbortSignal` aborted on dispose |\n| `flush()` | Runs a full cycle: pull, then push; resolves when it completes, rejects when it fails, and rejects with `TandemDisposedError` after `dispose` |\n| `tap(handler, options?)` | Observes `TandemEvent`s; returns an unsubscribe function |\n| `[Symbol.dispose]()` | Calls `dispose()` for `using` scopes |\n\n## Contracts\n\n### `SyncPort`\n\nThe server seam. `pull(since)` returns remote records, remote deletions, and the next cursor. `push(records, deletions, options)` sends local changes; reject it when the server diverged so the engine pulls to reconcile. `options.keepalive` marks a flush that must survive the tab closing.\n\n### `SyncGateway`\n\nThe storage seam. The engine owns every rev comparison: it calls your gateway with the decisions already made.\n\n| Member | Contract |\n| --- | --- |\n| `records()` | This device's own records, remote mirrors excluded; sync or async, so a disk-backed gateway can read from storage instead of holding a mirror |\n| `pendingDeletions()` | Tombstones the server has not acknowledged |\n| `applyRecords(records)` | Upsert the given records: already filtered to what the server is ahead on; return the ids that failed validation |\n| `applyDeletions(deletions)` | Remove records deleted remotely |\n| `clearDeletions(deletions)` | Drop the given tombstones a successful push carried |\n| `loadState()` | The persisted `SyncState`, or `null` on a fresh device |\n| `saveState(state)` | Persist the baseline after a completed pull or push |\n\n## Events\n\n### `TandemEvent`\n\n```ts\ntype TandemEvent =\n | { readonly records: number; readonly type: 'pull' }\n | { readonly records: number; readonly type: 'push' }\n | { readonly message: string; readonly skipped: readonly string[]; readonly type: 'invalid' }\n | { readonly error: unknown; readonly message: string; readonly type: 'warning' }\n | { readonly type: 'dispose' };\n```\n\n`invalid` reports record ids `applyRecords` refused; `warning` reports a rejected push or transport failure and carries the original cause in `error` beside its `message`; `pull` and `push` report the record counts applied and pushed. `dispose` fires once, before tappers are cleared.\n\n## Types\n\n### `SyncRecordBase`\n\n```ts\ninterface SyncRecordBase {\n readonly id: string;\n readonly rev: number;\n}\n```\n\nThe only shape Tandem reads from a record: identity and a write counter. `rev` is required: the dirty model is a rev comparison, so every synced record carries one from birth.\n\n### `SyncEnvelope`\n\n```ts\ninterface SyncEnvelope<TRecord extends SyncRecordBase = SyncRecordBase> {\n readonly entity: string;\n readonly record: TRecord;\n}\n```\n\nA record paired with the entity (table or kind) it belongs to.\n\n### `SyncDeletion`\n\n```ts\ninterface SyncDeletion {\n readonly deletedAt?: string;\n readonly entity: string;\n readonly id: string;\n}\n```\n\nA tombstone: a deletion that travels to the server on the next push. `deletedAt` is set by whoever recorded the deletion and the engine never reads it: include it when your server wants the timestamp, omit it otherwise.\n\n### `SyncState`\n\n```ts\ninterface SyncState {\n cursor: string | null;\n revs: Record<string, number>;\n}\n```\n\nThe persisted baseline. `revs` maps `\"${entity}:${id}\"` to the newest rev the server has seen.\n\n### `SyncPullResult`\n\n```ts\ninterface SyncPullResult<TRecord extends SyncRecordBase = SyncRecordBase> {\n readonly cursor: string | null;\n readonly deletions: readonly SyncDeletion[];\n readonly records: readonly SyncEnvelope<TRecord>[];\n}\n```\n\n### `SyncOptions`\n\n```ts\ninterface SyncOptions<TRecord extends SyncRecordBase = SyncRecordBase> {\n gateway: SyncGateway<TRecord>;\n idleDelayMs?: number;\n port: SyncPort<TRecord>;\n}\n```\n\n## Errors\n\n### `TandemError`\n\n```ts\nclass TandemError extends Error {}\n```\n\nBase class for every error Tandem throws; `instanceof TandemError` catches them all in one branch.\n\n### `TandemDisposedError`\n\n```ts\nclass TandemDisposedError extends TandemError {}\n```\n\nThrown by `changed()` and rejected by `flush()` after `dispose()`. A scheduler that stops silently is a sync engine's worst failure: the device keeps editing while nothing uploads, so a leaked handle fails loudly instead.\n\nBeyond disposal, Tandem defines no error classes. A rejected `push` is expected input, not a failure: the engine pulls to reconcile and reports it as a `warning` event. `flush()` additionally rejects with the underlying port or gateway error; dirty records survive the failure and retry on the next cycle.\n",
6
6
  "usage": "---\ntitle: 'Tandem: Usage Guide'\ndescription: 'How to drive offline sync with @vielzeug/tandem: ports, gateways, rev baselines, tombstones, and reconciliation.'\n---\n\n[[toc]]\n\n## Basic Usage\n\nTandem sits between your storage and your server. You implement two interfaces and call `createSync`; the engine handles batching, dirty tracking, hide-flows, and conflict reconcile.\n\n```ts\nimport { createSync, type SyncGateway, type SyncPort } from '@vielzeug/tandem';\n\ninterface Doc {\n id: string;\n rev: number;\n title: string;\n}\n\nconst port: SyncPort<Doc> = {\n async pull(since) {\n return fetch(`/sync/pull?since=${since ?? ''}`).then((response) => response.json());\n },\n async push(records, deletions, options) {\n const response = await fetch('/sync/push', {\n body: JSON.stringify({ records, deletions }),\n headers: { 'Content-Type': 'application/json' },\n keepalive: options?.keepalive,\n method: 'POST',\n });\n if (!response.ok) throw new Error(`server diverged: ${await response.text()}`);\n },\n};\n\nconst gateway: SyncGateway<Doc> = {\n records: () => docs.map((record) => ({ entity: 'docs', record })),\n pendingDeletions: () => readTombstones(),\n async applyRecords(pulled) {\n // The engine already filtered these to records the server is ahead on;\n // validate the untrusted remotes and upsert the rest.\n const skipped: string[] = [];\n for (const { record } of pulled) {\n if (!isValid(record)) skipped.push(record.id);\n else upsert(record);\n }\n return skipped;\n },\n applyDeletions: (deletions) => Promise.all(deletions.map((d) => remove(d.id))),\n clearDeletions: (deletions) => Promise.all(deletions.map((d) => dropTombstone(d.id))),\n loadState: () => readRow('syncState'),\n saveState: (state) => writeRow('syncState', state),\n};\n\nconst sync = createSync<Doc>({ gateway, port });\n```\n\n## Track Dirty Records with Revs\n\nTandem never diffs record contents. Each record carries a `rev` counting its committed writes, and the gateway persists a baseline of the newest rev the server has seen. A record is dirty exactly when its current rev is above that baseline.\n\n```ts\n// Your write path bumps the rev on every committed change.\nfunction saveDoc(id: string, patch: Partial<Doc>): void {\n const current = docs.find((doc) => doc.id === id)!;\n upsert({ ...current, ...patch, rev: current.rev + 1 });\n sync.changed(); // schedule a flush\n}\n```\n\nBecause the baseline is persisted, dirtiness survives a reload: a record edited while offline is still above the baseline when the app restarts, and the boot sequence pushes it. The engine never asks your gateway to compare revs: ahead, dirty, and deletion precedence are all its decisions.\n\n## Propagate Deletions with Tombstones\n\nA plain pull cannot carry a deletion: the record is gone. Record a tombstone when you delete locally; Tandem pushes it and clears it only once the server acknowledges.\n\n```ts\nfunction deleteDoc(id: string): void {\n remove(id);\n writeTombstone({ deletedAt: new Date().toISOString(), entity: 'docs', id });\n sync.changed();\n}\n```\n\nA remote deletion loses to a local record with unpushed changes: if your local rev is above the baseline, Tandem keeps your edit and pushes it instead of applying the deletion. The guard runs both ways: a pulled record whose id is locally tombstoned, or deleted in the same pull, never applies, so a backend without an incremental cursor cannot resurrect your deletions.\n\n## Batch Changes and Flush\n\nCall `changed()` on every local write. The engine debounces a burst into one push after `idleDelayMs` (default 3000), and serializes pushes so two never overlap.\n\n```ts\nconst sync = createSync({ gateway, idleDelayMs: 1500, port });\n\n// Force a full cycle now: on a save button, or before navigating away.\ntry {\n await sync.flush();\n} catch {\n // the cycle failed: edits stay local and dirty; the next cycle retries them\n}\n```\n\n`flush()` pulls remote changes and then pushes dirty records, so it is also the wiring point for a returning network connection. It rejects when either half fails; the records stay dirty and the next cycle retries them. After `dispose()`, `flush()` rejects with `TandemDisposedError` and `changed()` throws it: a leaked scheduler fails loudly instead of silently stopping. If your reactive wiring can fire after disposal, unsubscribe from your store before disposing the handle, or guard with `sync.disposed`.\n\nTandem also flushes on its own: it pushes with `keepalive: true` when the tab hides or closes, and runs a full cycle when the tab returns to the foreground. Serve the keepalive push with `navigator.sendBeacon` or a `keepalive` fetch so it survives the tab closing.\n\n## Reconcile a Rejected Push\n\nWhen the server is ahead of the lineage you pushed, reject the push. Tandem immediately pulls to reconcile, then the next flush retries with the merged state. Ambient flushes (debounce, hide) report the failure as a `warning` event; `flush()` additionally rejects with the port's error.\n\n```ts\nasync push(records, deletions) {\n const response = await fetch('/sync/push', { body: JSON.stringify({ records, deletions }), method: 'POST' });\n if (response.status === 409) throw new Error('server ahead'); // triggers a reconcile pull\n}\n```\n\n## Observe with tap()\n\n`tap()` is the observation seam: pushes, pulls, refused records, and warnings. Handler errors are swallowed, so an observer never breaks sync.\n\n```ts\nconst unsubscribe = sync.tap((event) => {\n switch (event.type) {\n case 'push':\n case 'pull':\n console.debug(`sync ${event.type}: ${event.records} records`);\n break;\n case 'invalid':\n console.warn(event.message, event.skipped);\n break;\n case 'warning':\n console.warn(event.message, event.error); // push rejected, network failure, etc.\n break;\n }\n});\n```\n\n## Lifecycle and Disposal\n\nDispose before switching accounts: the baseline is per-account state and a running scheduler would push into the wrong port.\n\n```ts\nconst sync = createSync({ gateway, port });\n// ...\nsync.dispose(); // stops the scheduler, clears the timer, removes document/window listeners\n```\n\n`disposed` and `disposalSignal` report the state; `[Symbol.dispose]` lets you scope it with `using`.\n\n```ts\n{\n using sync = createSync({ gateway, port });\n sync.changed();\n} // disposed here\n```\n\n## Testing\n\nDrive the engine against in-memory doubles. A `SyncPort` double records pushes and returns scripted pulls; a `SyncGateway` double is a record array plus a tombstone list plus a state row.\n\n```ts\nimport { createSync } from '@vielzeug/tandem';\n\nconst pushes: unknown[][] = [];\nconst port: SyncPort = {\n pull: async () => ({ cursor: null, deletions: [], records: [] }),\n push: async (records, deletions) => void pushes.push([records, deletions]),\n};\n\nconst sync = createSync({ gateway: fakeGateway, idleDelayMs: 10, port });\nawait vi.waitFor(() => expect(pushes.length).toBe(1)); // startup push\nsync.dispose();\n```\n\n## Framework Integration\n\nTandem is framework-neutral. Wire `changed()` to your reactive layer and `dispose()` to the component or scope lifecycle.\n\n::: code-group\n\n```ts [Vue 3]\nimport { onScopeDispose, watch } from 'vue';\n\nconst sync = createSync({ gateway, port });\nwatch(store.records, () => sync.changed(), { deep: true });\nonScopeDispose(() => sync.dispose());\n```\n\n```tsx [React]\nimport { useEffect, useRef } from 'react';\n\nconst syncRef = useRef(createSync({ gateway, port }));\nuseEffect(() => {\n const sync = syncRef.current;\n return () => sync.dispose();\n}, []);\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { onDestroy } from 'svelte';\n const sync = createSync({ gateway, port });\n $effect(() => { $records; sync.changed(); });\n onDestroy(() => sync.dispose());\n</script>\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n- **Vault**: the usual `gateway` backend. Its IndexedDB adapter stores records and the sync-state row; `records()` reads them from storage (it may be async), `applyRecords()` writes through the codec.\n- **Postmaster**: complementary, not overlapping. Postmaster guarantees at-least-once delivery of discrete mutations; Tandem keeps whole-record state in step. Apps that need both run them side by side.\n- **Sentinel**: subscribe to `createNetwork()` and call `sync.flush()` when the connection returns; the cycle pulls and pushes, so remote changes land without waiting for the next foreground return.\n- **Ripple**: `store.subscribe(() => sync.changed())` is the idiomatic wiring for a signal-backed store.\n\n## Best Practices\n\n- Bump `rev` on every committed write: the whole dirty model depends on it.\n- Validate remote records in `applyRecords()` and return the ids you refuse; never trust a pulled record.\n- Keep `records()` free of remote session mirrors: it must return only this device's own records.\n- Dispose on account switch; a stale scheduler pushes into the wrong port.\n- Serve `keepalive` pushes with `sendBeacon` or a `keepalive` fetch so they survive tab close.\n- Reject a push on divergence instead of silently overwriting; let the reconcile pull merge.\n- Persist the sync-state row per account, alongside the records it describes.\n- Call `flush()` before actions that assume both sides are current, such as a share or export, and handle its rejection.\n",
7
- "examples": "---\ntitle: 'Tandem: Examples'\ndescription: Offline sync recipes for two-device editing, deletions, and account switching.\n---\n\n## Examples\n\n- [Sync a Vault Store](./examples/sync-a-vault-store.md)\n- [Keep Deletions from Resurrecting](./examples/keep-deletions-from-resurrecting.md)\n- [Switch Accounts Safely](./examples/switch-accounts-safely.md)\n"
7
+ "examples": "---\ntitle: 'Tandem: Examples'\ndescription: Offline sync recipes for two-device editing, deletions, and account switching.\n---\n\n## Examples\n\n- [Sync a Vault Store](./examples/sync-a-vault-store.md)\n- [Make Store Commands Undoable](./examples/make-store-commands-undoable.md)\n- [Keep Deletions from Resurrecting](./examples/keep-deletions-from-resurrecting.md)\n- [Switch Accounts Safely](./examples/switch-accounts-safely.md)\n"
8
8
  },
9
9
  "examples": [
10
10
  {