@vielzeug/codex 26.10.14 → 26.10.16

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,6 +1,6 @@
1
1
  # Vielzeug: Full Documentation
2
2
 
3
- > Complete documentation for 42 packages. Version: 26.10.13
3
+ > Complete documentation for 43 packages. Version: 26.10.15
4
4
 
5
5
  ---
6
6
 
@@ -2669,6 +2669,741 @@ use the flat tree for DOM algorithms; this helper is for test code that needs eq
2669
2669
 
2670
2670
  ---
2671
2671
 
2672
+ ## @vielzeug/beacon
2673
+
2674
+ **Category:** ui-interaction
2675
+
2676
+ ### Overview
2677
+
2678
+ ## Why Beacon?
2679
+
2680
+ Product tours rot fast: hand-wired step sequences hard-code positioning, persistence, and target waiting into application code, and break the first time the UI moves. Beacon is the headless half of a tour — it owns step order, target waiting, resumable progress, and events — while your markup (or `@vielzeug/refine`'s `ore-tour` components) decides what a step looks like.
2681
+
2682
+ ```ts
2683
+ // Before: step logic tangled with rendering and storage
2684
+ let step = Number(localStorage.getItem('tour-step') ?? 0);
2685
+ function showStep() {
2686
+ const el = document.querySelector(steps[step].selector);
2687
+ card.style.top = `${el.getBoundingClientRect().bottom + 8}px`; // breaks on scroll
2688
+ card.innerHTML = steps[step].html;
2689
+ }
2690
+
2691
+ // After: the engine owns order, persistence, and the anchor rect
2692
+ import { createTour } from '@vielzeug/beacon';
2693
+
2694
+ const tour = createTour({ id: 'welcome', steps, storage });
2695
+ tour.current.subscribe(() => renderCard(tour.current.value)); // rect is the live anchor
2696
+ tour.start();
2697
+ ```
2698
+
2699
+ | Feature | Beacon | Shepherd.js | Intro.js |
2700
+ | --- | --- | --- | --- |
2701
+ | Bundle size | | ~50 kB | ~25 kB |
2702
+ | External dependencies | (none) | | |
2703
+ | Headless (you own the UI) | | | |
2704
+ | Resumable persisted progress | | | |
2705
+ | Versioned re-offers | | | |
2706
+ | Coachmarks (single hints) | | | |
2707
+
2708
+ **Use Beacon when** tours must survive UI changes: steps anchor to your real DOM, progress persists and resumes, and the card markup is yours (or refine's ready-made `ore-tour`).
2709
+
2710
+ **Consider Shepherd or Intro.js when** you want a fully-rendered, opinionated tour widget you never style yourself and can accept their DOM and CSS.
2711
+
2712
+ ## Installation
2713
+
2714
+ ```sh [pnpm]
2715
+ pnpm add @vielzeug/beacon
2716
+ ```
2717
+
2718
+ ```sh [npm]
2719
+ npm install @vielzeug/beacon
2720
+ ```
2721
+
2722
+ ```sh [yarn]
2723
+ yarn add @vielzeug/beacon
2724
+ ```
2725
+
2726
+ ## Quick Start
2727
+
2728
+ Define the tour as data, run it, and render the current step wherever you like. The handle is disposable, so component teardown cleans everything up.
2729
+
2730
+ ```ts
2731
+ import { createTour, type BeaconTourConfig } from '@vielzeug/beacon';
2732
+
2733
+ const config = {
2734
+ id: 'getting-started',
2735
+ steps: [
2736
+ { id: 'welcome', title: 'Welcome', description: 'A two-minute look around.' },
2737
+ { id: 'nav', target: '[data-tour="nav"]', title: 'Navigation', placement: 'bottom-start' },
2738
+ ],
2739
+ } satisfies BeaconTourConfig;
2740
+
2741
+ const tour = createTour(config);
2742
+
2743
+ // Render from the live snapshot: rect is the anchor, null centers the card.
2744
+ const unsubscribe = tour.current.subscribe(() => {
2745
+ const { presentation, rect, step } = tour.current.value;
2746
+ console.log({ presentation, rect, step }); // drive your card from this
2747
+ });
2748
+
2749
+ tour.start();
2750
+
2751
+ // On teardown:
2752
+ unsubscribe();
2753
+ tour.dispose();
2754
+ ```
2755
+
2756
+ ## Features
2757
+
2758
+ - `createTour()` runs a sequential, resumable walkthrough over your own DOM.
2759
+ - `createCoachmark()` shows a single contextual hint once per user per version.
2760
+ - `current` is a live `Readable` snapshot: step, index, placement, presentation, and resolved anchor rect, kept fresh on scroll, resize, and layout changes.
2761
+ - `target` accepts a selector, `Element`, rect, or lazy resolver, and beacon waits for lazily-mounted targets before showing the step.
2762
+ - `storage` persists progress through any `BeaconStorage` bridge (localStorage, IndexedDB via `@vielzeug/vault`, a server API).
2763
+ - `version` re-offers a finished tour when the product changes underneath it.
2764
+ - `tap()` subscribes to lifecycle events (`step:start`, `tour:complete`, `target-missing`, ...) with `AbortSignal` support.
2765
+ - `resolveTarget()` turns any target into a viewport rect compatible with `@vielzeug/orbit` positioners.
2766
+ - `ore-tour`'s `bind()` in `@vielzeug/refine` renders a handle and routes its actions with zero glue code.
2767
+
2768
+ ## Documentation
2769
+
2770
+ - [Usage Guide](./usage.md)
2771
+ - [API Reference](./api.md)
2772
+ - [Examples](./examples.md)
2773
+ - [Migration Guide](./migration.md)
2774
+
2775
+ ## See Also
2776
+
2777
+ - [Refine](/refine/): ships `ore-tour` and `ore-spotlight`, the ready-made dialog/popover/bottom-sheet presentation layer for a beacon handle.
2778
+ - [Orbit](/orbit/): compute exact popover coordinates from `current.rect` when you build a custom presentation.
2779
+ - [Vault](/vault/): durable storage backends that plug into `BeaconStorage` with a six-line bridge.
2780
+ - [Ripple](/ripple/): the `Readable` signals every beacon handle exposes come from here.
2781
+
2782
+ ### API Reference
2783
+
2784
+ ## API Overview
2785
+
2786
+ | Symbol | Purpose | Execution mode | Common gotcha |
2787
+ | --- | --- | --- | --- |
2788
+ | `createTour(config)` | Runs a sequential, resumable walkthrough over the app's DOM | Sync (storage reads may be async) | `start()` is a no-op when the stored record says the tour is completed |
2789
+ | `createCoachmark(config)` | Shows a single contextual hint anchored to one target | Sync (storage reads may be async) | `show()` is a no-op after `dismiss()` until `reset()` or a `version` bump |
2790
+ | `resolveTarget(target)` | Resolves any target to a viewport-space rect or `null` | Sync | Call it per position update; a scrolled target's rect changes |
2791
+ | `BeaconError` | Base class for all beacon-originated errors | — | Catch this to catch any beacon error |
2792
+ | `BeaconConfigError` | Invalid tour/coachmark configuration | Thrown at creation | Empty `steps` or duplicate step ids fail at `createTour()` |
2793
+ | `BeaconDisposedError` | Handle used after `dispose()` | Thrown on call | `dispose()` is idempotent; later method calls throw |
2794
+
2795
+ ## Package Entry Point
2796
+
2797
+ | Import | Purpose |
2798
+ | --- | --- |
2799
+ | `@vielzeug/beacon` | Complete public Beacon API |
2800
+
2801
+ ---
2802
+
2803
+ ## Core Functions
2804
+
2805
+ ### createTour
2806
+
2807
+ ```ts
2808
+ function createTour(config: BeaconTourConfig): BeaconTourHandle
2809
+ ```
2810
+
2811
+ Returns a live handle for one guided tour. Throws `BeaconConfigError` when `id` is empty, `steps` is empty, or step ids are duplicated.
2812
+
2813
+ Parameters: see [`BeaconTourConfig`](#beacontourconfig).
2814
+
2815
+ **Returns:** a [`BeaconTourHandle`](#beacontourhandle) that is also `Disposable`.
2816
+
2817
+ **Example:**
2818
+
2819
+ ```ts
2820
+ import { createTour } from '@vielzeug/beacon';
2821
+
2822
+ const tour = createTour({
2823
+ id: 'release-tour',
2824
+ steps: [{ id: 'intro', title: 'What changed' }],
2825
+ autoStart: true,
2826
+ });
2827
+
2828
+ tour.status.subscribe(() => console.log(tour.status.value));
2829
+ ```
2830
+
2831
+ Handle methods:
2832
+
2833
+ | Member | Behavior |
2834
+ | --- | --- |
2835
+ | `start()` | Begin or resume the tour. No-op when completed; throws after dispose. |
2836
+ | `next()` | Advance; past the last step, finishes the tour. |
2837
+ | `back()` | Return to the previous step. No-op on the first step. |
2838
+ | `goto(step)` | Jump to a step by index or id. Unknown values warn and no-op. |
2839
+ | `pause()` | Stop showing, keep progress so `start()` resumes it. |
2840
+ | `skip()` | End from a "Skip" control. Persisted as dismissed, reported with reason `'skip'`. |
2841
+ | `finish()` | Mark finished and stop showing. |
2842
+ | `dismiss()` | End by closing (Escape, backdrop). Persisted as dismissed. |
2843
+ | `reset()` | Discard persisted progress; the tour is eligible again. |
2844
+ | `tap(handler, options?)` | Observe [`BeaconTourEvent`](#beacontourentype)s; returns an unsubscribe, honors `options.signal`. |
2845
+ | `dispose()` / `[Symbol.dispose]()` | Release all listeners and observers. Idempotent. |
2846
+ Handle properties:
2847
+
2848
+ | Member | Type | Notes |
2849
+ | --- | --- | --- |
2850
+ | `current` | `Readable` | The snapshot to render; `step` is `null` while closed. Beacon re-publishes the rect on scroll, resize, and target layout changes while a step is active. |
2851
+ | `status` | `Readable` | Lifecycle, persisted on every change. |
2852
+ | `eligible` | `Readable` | Whether the tour should be offered to this user. |
2853
+ | `steps` | `readonly BeaconStep[]` | The configured steps, in order. |
2854
+ | `id` | `string` | The configured id (also the storage key suffix). |
2855
+ | `disposed` | `boolean` | Whether `dispose()` has run. |
2856
+ | `disposalSignal` | `AbortSignal` | Aborted on dispose; pass it to your own listeners. |
2857
+
2858
+ ---
2859
+
2860
+ ### createCoachmark
2861
+
2862
+ ```ts
2863
+ function createCoachmark(config: BeaconCoachmarkConfig): BeaconCoachmarkHandle
2864
+ ```
2865
+
2866
+ Returns a handle for one contextual hint. Throws `BeaconConfigError` when `id` is empty.
2867
+
2868
+ Parameters: see [`BeaconCoachmarkConfig`](#beaconcoachmarkconfig).
2869
+
2870
+ **Returns:** a [`BeaconCoachmarkHandle`](#beaconcoachmarkhandle) that is also `Disposable`.
2871
+
2872
+ **Example:**
2873
+
2874
+ ```ts
2875
+ import { createCoachmark } from '@vielzeug/beacon';
2876
+
2877
+ const hint = createCoachmark({
2878
+ id: 'share-hint',
2879
+ target: '[data-tour="share"]',
2880
+ title: 'Share this board',
2881
+ });
2882
+
2883
+ hint.show(); // waits for the target to mount if it is not on screen yet
2884
+ ```
2885
+
2886
+ Handle members:
2887
+
2888
+ | Member | Behavior |
2889
+ | --- | --- |
2890
+ | `show()` | Show the hint. No-op once dismissed without a newer version. |
2891
+ | `hide()` | Hide without recording it as seen; it can show again. |
2892
+ | `dismiss()` | Hide and record as seen permanently (until `reset()`/`version`). |
2893
+ | `reset()` | Discard the persisted record; the hint is eligible again. |
2894
+ | `tap(handler, options?)` | Observe `coachmark:show`, `coachmark:dismiss`, and common events. |
2895
+ | `current` | `Readable` — the hint as a single-step snapshot: `step` is the hint while visible, `null` while hidden. The rect stays live on scroll, resize, and target layout changes. |
2896
+ | `eligible` | `Readable` — never seen, or superseded by a newer version. |
2897
+
2898
+ ---
2899
+
2900
+ ### resolveTarget
2901
+
2902
+ ```ts
2903
+ function resolveTarget(target: BeaconTarget): BeaconRect | null
2904
+ ```
2905
+
2906
+ Resolves a target to a viewport-space rect, or `null` when it is not available yet. Strings resolve through `document.querySelector`; functions are called and their result normalized.
2907
+
2908
+ | Parameter | Type | Description |
2909
+ | --- | --- | --- |
2910
+ | `target` | `BeaconTarget` | Selector, `Element`, rect, or lazy resolver. |
2911
+
2912
+ **Returns:** `BeaconRect | null` — shape-compatible with `getBoundingClientRect()` and `@vielzeug/orbit`'s `Rect`.
2913
+
2914
+ **Example:**
2915
+
2916
+ ```ts
2917
+ import { resolveTarget } from '@vielzeug/beacon';
2918
+
2919
+ const rect = resolveTarget('[data-tour="share"]');
2920
+ // Anchor an orbit positioner from a virtual reference:
2921
+ const reference = { getBoundingClientRect: () => resolveTarget('[data-tour="share"]') ?? ZERO_RECT };
2922
+ ```
2923
+
2924
+ ---
2925
+
2926
+ ## Types
2927
+
2928
+ ### BeaconTourConfig
2929
+
2930
+ | Option | Type | Default | Description |
2931
+ | --- | --- | --- | --- |
2932
+ | `id` | `string` | required | Unique identity; used as the storage key. |
2933
+ | `steps` | `readonly BeaconStep[]` | required | Every step, in order. At least one. |
2934
+ | `autoScroll` | `boolean` | `true` | Scroll each anchored step's target into view when it activates. |
2935
+ | `autoStart` | `boolean` | `false` | Start on the next microtask when eligible. |
2936
+ | `placement` | `BeaconPlacement` | `'auto'` | Default placement handed to the presentation layer. |
2937
+ | `presentation` | `BeaconPresentation` | `'auto'` | Default presentation handed to the presentation layer. |
2938
+ | `scrollOffset` | `number \| (() => number)` | `0` | Pixels kept clear at the viewport top for a sticky header, read at scroll time. |
2939
+ | `storage` | `BeaconStorage` | memory-only | Progress persistence. |
2940
+ | `targetTimeoutMs` | `number` | `Infinity` | How long to wait for a step's target before skipping that step. `0` publishes immediately with a `null` rect. |
2941
+ | `version` | `number` | `1` | Bump to re-show a finished tour. |
2942
+
2943
+ ### BeaconCoachmarkConfig
2944
+
2945
+ | Option | Type | Default | Description |
2946
+ | --- | --- | --- | --- |
2947
+ | `id` | `string` | required | Unique identity; used as the storage key. |
2948
+ | `target` | `BeaconTarget` | required | Where the hint points. |
2949
+ | `autoShow` | `boolean` | `true` | Show automatically when not yet seen. |
2950
+ | `data` | `unknown` | — | Free-form data for the presentation layer. |
2951
+ | `description` | `string` | — | Supporting copy. |
2952
+ | `placement` | `BeaconPlacement` | `'auto'` | Preferred side. |
2953
+ | `presentation` | `BeaconPresentation` | `'auto'` | Preferred presentation. |
2954
+ | `storage` | `BeaconStorage` | memory-only | Seen-record persistence. |
2955
+ | `title` | `string` | — | Headline copy. |
2956
+ | `version` | `number` | `1` | Bump to re-show a seen coachmark. |
2957
+
2958
+ ### BeaconStep
2959
+
2960
+ ```ts
2961
+ interface BeaconStep {
2962
+ readonly data?: unknown;
2963
+ readonly description?: string;
2964
+ readonly id: string;
2965
+ readonly placement?: BeaconPlacement;
2966
+ readonly presentation?: BeaconPresentation;
2967
+ readonly target?: BeaconTarget;
2968
+ readonly title?: string;
2969
+ }
2970
+ ```
2971
+
2972
+ One stop in a tour. `id` is stable and persisted — renaming it resets progress for that step. Omit `target` for a centered intro/outro step. `placement`/`presentation` override the tour-level defaults.
2973
+
2974
+ ### BeaconTarget
2975
+
2976
+ ```ts
2977
+ type BeaconTarget = BeaconTargetResolver | BeaconRect | Element | string;
2978
+ ```
2979
+
2980
+ What a step points at. Selectors and elements anchor to real DOM; rects (or resolvers returning one) anchor to element-less geometry such as a canvas region.
2981
+
2982
+ ### BeaconTargetResolver
2983
+
2984
+ ```ts
2985
+ type BeaconTargetResolver = () => Element | BeaconRect | null;
2986
+ ```
2987
+
2988
+ Resolves a target lazily; `null` means "not available yet".
2989
+
2990
+ ### BeaconRect
2991
+
2992
+ ```ts
2993
+ interface BeaconRect {
2994
+ readonly height: number;
2995
+ readonly width: number;
2996
+ readonly x: number;
2997
+ readonly y: number;
2998
+ }
2999
+ ```
3000
+
3001
+ A viewport-space rectangle in CSS pixels. Matches `@vielzeug/orbit`'s `Rect` shape.
3002
+
3003
+ ### BeaconPlacement
3004
+
3005
+ ```ts
3006
+ type BeaconPlacement =
3007
+ | 'auto'
3008
+ | 'bottom' | 'bottom-end' | 'bottom-start'
3009
+ | 'left' | 'left-end' | 'left-start'
3010
+ | 'right' | 'right-end' | 'right-start'
3011
+ | 'top' | 'top-end' | 'top-start';
3012
+ ```
3013
+
3014
+ Preferred side for an anchored presentation; `'auto'` asks the positioning layer to choose.
3015
+
3016
+ ### BeaconPresentation
3017
+
3018
+ ```ts
3019
+ type BeaconPresentation = 'auto' | 'bottom-sheet' | 'modal' | 'popover';
3020
+ ```
3021
+
3022
+ How a step should be presented. `'auto'` lets the presentation layer pick by available space.
3023
+
3024
+ ### BeaconStatus
3025
+
3026
+ ```ts
3027
+ type BeaconStatus = 'active' | 'completed' | 'dismissed' | 'idle' | 'paused';
3028
+ ```
3029
+
3030
+ Tour lifecycle, persisted across visits. `'idle'` means never started.
3031
+
3032
+ ### BeaconProgress
3033
+
3034
+ ```ts
3035
+ interface BeaconProgress {
3036
+ readonly status: BeaconStatus;
3037
+ readonly stepIndex: number;
3038
+ readonly version: number;
3039
+ }
3040
+ ```
3041
+
3042
+ The persisted record for one tour or coachmark. `stepIndex` is `-1` before the first step. Records are validated on read; invalid records fall back to a fresh tour.
3043
+
3044
+ ### BeaconStorage
3045
+
3046
+ ```ts
3047
+ interface BeaconStorage {
3048
+ get(key: string): Promise | BeaconProgress | null | undefined;
3049
+ remove(key: string): Promise | unknown;
3050
+ set(key: string, value: BeaconProgress): Promise | unknown;
3051
+ }
3052
+ ```
3053
+
3054
+ Minimal storage contract; sync or async. Values are JSON. Keys arrive namespaced (`@vielzeug/beacon:`). A `@vielzeug/vault` store plugs in with:
3055
+
3056
+ ```ts
3057
+ const storage: BeaconStorage = {
3058
+ get: (key) => store.get('progress', key),
3059
+ remove: (key) => store.delete('progress', key),
3060
+ set: (key, value) => store.put('progress', { id: key, ...value }),
3061
+ };
3062
+ ```
3063
+
3064
+ ### BeaconCurrentStep
3065
+
3066
+ ```ts
3067
+ interface BeaconCurrentStep {
3068
+ readonly placement: BeaconPlacement;
3069
+ readonly presentation: BeaconPresentation;
3070
+ readonly rect: BeaconRect | null;
3071
+ readonly step: BeaconStep | null;
3072
+ readonly stepIndex: number;
3073
+ readonly totalSteps: number;
3074
+ }
3075
+ ```
3076
+
3077
+ The snapshot a presentation layer renders from. `step` is `null` before start and after the tour ends; `rect` is `null` for centered steps and unresolved targets. While a step is active, Beacon keeps `rect` fresh on scroll, resize, and target layout changes — no app-side refresh call is needed.
3078
+
3079
+ ### BeaconTourEvent
3080
+
3081
+ ```ts
3082
+ type BeaconTourEvent =
3083
+ | { readonly index: number; readonly step: BeaconStep; readonly type: 'step:start' }
3084
+ | { readonly index: number; readonly step: BeaconStep; readonly type: 'step:complete' }
3085
+ | { readonly reason: 'finish' | 'skip'; readonly type: 'tour:complete' }
3086
+ | { readonly type: 'tour:dismiss' }
3087
+ | { readonly status: BeaconStatus; readonly type: 'status-change' }
3088
+ | { readonly message: string; readonly type: 'target-missing' }
3089
+ | { readonly index: number; readonly message: string; readonly step: BeaconStep; readonly type: 'target-waiting' }
3090
+ | { readonly error: unknown; readonly message: string; readonly type: 'warning' }
3091
+ | { readonly type: 'dispose' };
3092
+ ```
3093
+
3094
+ Notable moments a tour reports through `tap()`. `target-missing` means a step was skipped because its target did not appear in time; `target-waiting` fires when a step begins waiting for its target to mount; `warning` carries storage or handler failures.
3095
+
3096
+ ### BeaconCoachmarkEvent
3097
+
3098
+ ```ts
3099
+ type BeaconCoachmarkEvent =
3100
+ | { readonly type: 'coachmark:show' }
3101
+ | { readonly type: 'coachmark:dismiss' }
3102
+ | { readonly type: 'target-waiting' }
3103
+ | { readonly error: unknown; readonly message: string; readonly type: 'warning' }
3104
+ | { readonly type: 'dispose' };
3105
+ ```
3106
+
3107
+ Notable moments a coachmark reports through `tap()`.
3108
+
3109
+ ### BeaconTourHandle / BeaconCoachmarkHandle
3110
+
3111
+ The objects returned by `createTour()` and `createCoachmark()`, documented in [Core Functions](#core-functions). Both expose `disposalSignal`, `disposed`, `tap()`, `dispose()`, and `[Symbol.dispose]()`.
3112
+
3113
+ ## Errors
3114
+
3115
+ | Error | Triggered by | Notes |
3116
+ | --- | --- | --- |
3117
+ | `BeaconError` | Base for all beacon errors | `name` is the subclass name; `cause` supported via `ErrorOptions`. |
3118
+ | `BeaconConfigError` | Empty `id`, empty `steps`, missing/duplicate step ids | Thrown synchronously from `createTour()`/`createCoachmark()`. |
3119
+ | `BeaconDisposedError` | Calling handle methods after `dispose()` | `dispose()` itself never throws. |
3120
+
3121
+ ### Usage Guide
3122
+
3123
+ ## Basic Usage
3124
+
3125
+ A tour is a config object plus a handle you drive from your own UI. The handle never renders anything: it publishes a `current` snapshot, and your card (or refine's `ore-tour`) renders it.
3126
+
3127
+ ```ts
3128
+ import { createTour } from '@vielzeug/beacon';
3129
+
3130
+ const tour = createTour({
3131
+ id: 'product-tour',
3132
+ steps: [
3133
+ { id: 'welcome', title: 'Welcome', description: 'See what changed in this release.' },
3134
+ { id: 'nav', target: '[data-tour="nav"]', title: 'Find everything', placement: 'bottom-start' },
3135
+ { id: 'share', target: '[data-tour="share"]', title: 'Share the board', placement: 'bottom-end' },
3136
+ ],
3137
+ autoStart: true,
3138
+ });
3139
+
3140
+ // Readable from @vielzeug/ripple: the listener takes no arguments;
3141
+ // read .value inside it.
3142
+ const unsubscribe = tour.current.subscribe(() => {
3143
+ const current = tour.current.value;
3144
+ if (!current.step) {
3145
+ hideCard();
3146
+ return;
3147
+ }
3148
+ renderCard({
3149
+ anchor: current.rect, // viewport rect of the target, null for centered steps
3150
+ index: current.stepIndex,
3151
+ placement: current.placement,
3152
+ presentation: current.presentation,
3153
+ title: current.step.title,
3154
+ total: current.totalSteps,
3155
+ });
3156
+ });
3157
+
3158
+ // Wire your card's controls back to the handle.
3159
+ cardEl.addEventListener('next', () => tour.next());
3160
+ cardEl.addEventListener('back', () => tour.back());
3161
+ cardEl.addEventListener('skip', () => tour.skip());
3162
+
3163
+ // On teardown:
3164
+ unsubscribe();
3165
+ tour.dispose();
3166
+ ```
3167
+
3168
+ `start()` is a no-op when the stored record says this user already completed the tour, so calling it on every page load is safe.
3169
+
3170
+ ## Steps and Targets
3171
+
3172
+ A step without a `target` centers (intro/outro copy). A step with one anchors, and beacon waits for the target to exist before publishing the step — a tour can walk through lazily-mounted routes without any coordination code.
3173
+
3174
+ ```ts
3175
+ const tour = createTour({
3176
+ id: 'deep-tour',
3177
+ steps: [
3178
+ // Selector: resolved with document.querySelector on activation.
3179
+ { id: 'sidebar', target: '[data-tour="sidebar"]', title: 'Your libraries' },
3180
+ // Element: a ref your framework already holds.
3181
+ { id: 'editor', target: editorEl, title: 'The editor' },
3182
+ // Rect: geometry with no element, such as a canvas region.
3183
+ { id: 'chart', target: { height: 120, width: 240, x: 400, y: 300 }, title: 'This trend' },
3184
+ // Resolver: computed on every activation, and on scroll/resize while the step is active.
3185
+ { id: 'selection', target: () => window.getSelection()?.getRangeAt(0).getBoundingClientRect() ?? null, title: 'Your selection' },
3186
+ ],
3187
+ // Give up waiting after 4s and skip the step instead of hanging the tour.
3188
+ targetTimeoutMs: 4000,
3189
+ });
3190
+ ```
3191
+
3192
+ With the default `targetTimeoutMs: Infinity`, a step whose target never mounts simply waits: the tour resumes the moment the user navigates to it. Set `0` to publish immediately with a `null` rect instead of waiting.
3193
+
3194
+ ## Persisting Progress
3195
+
3196
+ Without `storage`, progress lives in memory for the handle's lifetime only. Any object matching `BeaconStorage` makes it durable — values are JSON, keys are namespaced under `@vielzeug/beacon:`.
3197
+
3198
+ ```ts
3199
+ import type { BeaconStorage } from '@vielzeug/beacon';
3200
+
3201
+ const storage: BeaconStorage = {
3202
+ get: (key) => JSON.parse(localStorage.getItem(key) ?? 'null'),
3203
+ remove: (key) => localStorage.removeItem(key),
3204
+ set: (key, value) => localStorage.setItem(key, JSON.stringify(value)),
3205
+ };
3206
+
3207
+ const tour = createTour({ id: 'product-tour', steps, storage });
3208
+ ```
3209
+
3210
+ Async storage works too: `get` may return a promise, and the tour boots as soon as it resolves. A `set` that rejects is reported through `tap()` as a `warning` event, never thrown.
3211
+
3212
+ Records are validated on read, so a hand-edited or stale record falls back to a fresh tour instead of crashing.
3213
+
3214
+ ### Re-offering a finished tour
3215
+
3216
+ Bump `version` when the product changes and every user who finished the old tour becomes eligible again — users mid-tour on the old version keep their progress.
3217
+
3218
+ ```ts
3219
+ const tour = createTour({
3220
+ id: 'product-tour',
3221
+ steps: newSteps,
3222
+ version: 2, // stored records at version 1 no longer block the tour
3223
+ });
3224
+ ```
3225
+
3226
+ `reset()` clears the persisted record for the current user, which is what a "Replay tour" or "Reset onboarding" control should call before `start()`.
3227
+
3228
+ ## Driving a Refine Presentation
3229
+
3230
+ `@vielzeug/refine` ships `ore-tour` (a dialog with step footer, progress, and focus handling) and `ore-spotlight` (a decorative cutout that dims everything but the target). A beacon handle satisfies `ore-tour`'s `TourController` shape, so one call wires both directions:
3231
+
3232
+ ```ts
3233
+ import type { OreTourElement } from '@vielzeug/refine/tour';
3234
+
3235
+ const card = document.querySelector('#tour-card');
3236
+
3237
+ const tour = createTour({ id: 'product-tour', presentation: 'auto', steps });
3238
+
3239
+ // bind() renders `current` into the card's props and routes the card's
3240
+ // next/back/skip/finish/dismiss actions back into the handle.
3241
+ const unbind = card.bind(tour);
3242
+
3243
+ tour.start();
3244
+
3245
+ // On teardown:
3246
+ unbind();
3247
+ tour.dispose();
3248
+ ```
3249
+
3250
+ `presentation: 'auto'` maps to a popover on wide viewports and a bottom sheet on narrow ones; ore-tour makes that switch itself and freezes the choice while the card is open, so a mid-step resize never rebuilds the card. Beacon keeps `current.rect` fresh on scroll, resize, and layout changes, so the anchored card follows the layout with no app-side listeners.
3251
+
3252
+ Apps that render their own markup read `current` directly instead (see [Basic Usage](#basic-usage)).
3253
+
3254
+ ## Coachmarks
3255
+
3256
+ A coachmark is one hint anchored to one target, shown the first time the target appears and dismissed permanently after. Use it for the "explain this button once" moment a tour would be too heavy for — or as the follow-up a tour hands off to.
3257
+
3258
+ ```ts
3259
+ import { createCoachmark } from '@vielzeug/beacon';
3260
+
3261
+ const share = createCoachmark({
3262
+ id: 'share-hint',
3263
+ target: '[data-tour="share"]',
3264
+ title: 'Share this board',
3265
+ description: 'Send a link that opens straight into this view.',
3266
+ storage,
3267
+ });
3268
+
3269
+ // Render from `current` — the same single-step snapshot a tour publishes.
3270
+ share.current.subscribe(() => console.log(share.current.value.step !== null, share.current.value.rect));
3271
+
3272
+ // The target may never appear on this page; show() waits for it.
3273
+ share.show();
3274
+
3275
+ // "Got it" button:
3276
+ share.dismiss(); // records it as seen; never shows again for this version
3277
+ ```
3278
+
3279
+ `hide()` closes the hint without recording it, so it can appear again later; `dismiss()` is permanent until `reset()` or a `version` bump.
3280
+
3281
+ ## Events and Analytics
3282
+
3283
+ `tap()` observes everything the engine considers notable. It returns an unsubscribe function and accepts an `AbortSignal`, so one controller can tear down every subscription.
3284
+
3285
+ ```ts
3286
+ const controller = new AbortController();
3287
+
3288
+ tour.tap((event) => {
3289
+ switch (event.type) {
3290
+ case 'step:start':
3291
+ analytics.track('tour_step', { id: tour.id, step: event.step.id });
3292
+ break;
3293
+ case 'tour:complete':
3294
+ analytics.track('tour_complete', { id: tour.id, reason: event.reason });
3295
+ break;
3296
+ case 'target-missing':
3297
+ // A step's target never mounted: your selector drifted from the DOM.
3298
+ console.warn(event.message);
3299
+ break;
3300
+ case 'warning':
3301
+ console.warn(event.message, event.error);
3302
+ break;
3303
+ }
3304
+ }, { signal: controller.signal });
3305
+ ```
3306
+
3307
+ `target-missing` is the event to alert on: it means a step was skipped because its target did not appear within `targetTimeoutMs`, which is exactly how stale tours announce themselves.
3308
+
3309
+ ## Testing
3310
+
3311
+ Beacon runs without a rendering layer, so plain DOM tests are enough. The default in-memory storage keeps each handle's progress isolated, and `jsdom`'s `getBoundingClientRect()` returns zero-sized rects, which is fine for step-order assertions.
3312
+
3313
+ ```ts
3314
+ import { createTour } from '@vielzeug/beacon';
3315
+ import { expect, test } from 'vitest';
3316
+
3317
+ test('walks steps in order and completes', () => {
3318
+ const tour = createTour({
3319
+ id: 'test-tour',
3320
+ steps: [{ id: 'a' }, { id: 'b' }],
3321
+ targetTimeoutMs: 0, // publish immediately even without targets
3322
+ });
3323
+
3324
+ tour.start();
3325
+ expect(tour.current.value.step?.id).toBe('a');
3326
+
3327
+ tour.next();
3328
+ expect(tour.current.value.step?.id).toBe('b');
3329
+
3330
+ tour.next(); // past the last step finishes
3331
+ expect(tour.status.value).toBe('completed');
3332
+ });
3333
+ ```
3334
+
3335
+ For persistence assertions, pass a `Map`-backed `BeaconStorage` and a second handle with the same `id` to check it resumes where the first stopped.
3336
+
3337
+ ## Framework Integration
3338
+
3339
+ Beacon is framework-neutral: bridge the `Readable` snapshot with whatever primitive your framework already has.
3340
+
3341
+ ```tsx [React]
3342
+ import { useEffect, useState } from 'react';
3343
+ import { createTour } from '@vielzeug/beacon';
3344
+
3345
+ function useTourState(tour: ReturnType) {
3346
+ const [current, setCurrent] = useState(tour.current.value);
3347
+ useEffect(() => tour.current.subscribe(() => setCurrent(tour.current.value)), [tour]);
3348
+ return current;
3349
+ }
3350
+ ```
3351
+
3352
+ ```vue [Vue 3]
3353
+
3354
+ import { onScopeDispose, shallowRef } from 'vue';
3355
+ import { createTour } from '@vielzeug/beacon';
3356
+
3357
+ const current = shallowRef(tour.current.value);
3358
+ const unsubscribe = tour.current.subscribe(() => (current.value = tour.current.value));
3359
+ onScopeDispose(unsubscribe);
3360
+
3361
+ ```
3362
+
3363
+ ```svelte [Svelte]
3364
+
3365
+ import { onDestroy } from 'svelte';
3366
+ import { readable } from 'svelte/store';
3367
+
3368
+ const current = readable(tour.current.value, (set) => {
3369
+ set(tour.current.value);
3370
+ return tour.current.subscribe(() => set(tour.current.value));
3371
+ });
3372
+ onDestroy(() => tour.dispose());
3373
+
3374
+ ```
3375
+
3376
+ ## Working with Other Vielzeug Libraries
3377
+
3378
+ - **Refine**: `ore-tour` and `ore-spotlight` are the intended presentation layer; see [Driving a Refine Presentation](#driving-a-refine-presentation).
3379
+ - **Orbit**: `current.rect` matches orbit's `Rect` shape, so a custom card positions with `computePosition({ getBoundingClientRect: () => current.rect ?? ZERO_RECT }, floatingEl, middleware)`.
3380
+ - **Vault**: any vault store becomes a `BeaconStorage` with a six-line bridge (see the `BeaconStorage` docs in [API Reference](./api.md)).
3381
+ - **Ripple**: every handle exposes ripple `Readable`s; `createRipple().isBatchingObservable` semantics apply when you subscribe from effects.
3382
+
3383
+ ## Best Practices
3384
+
3385
+ - Give every tour and coachmark a stable `id` — it is the storage key; treat renames as data migrations.
3386
+ - Keep tour configs in their own modules as plain data checked with `satisfies BeaconTourConfig`, so steps stay reviewable data, not component code.
3387
+ - Prefer `data-tour="..."` attributes as target selectors: they survive class renames and read as intent.
3388
+ - Set a finite `targetTimeoutMs` in production and alert on `target-missing` to catch stale selectors.
3389
+ - Call `reset()` before `start()` for replay controls; never delete storage keys by hand.
3390
+ - Bump `version` when a tour's meaning changes, not when its copy is typo-fixed.
3391
+ - Dispose handles on teardown; `tap()` subscriptions with a shared `AbortSignal` clean up in one call.
3392
+
3393
+ ### Examples
3394
+
3395
+ ## Examples
3396
+
3397
+ - [Getting Started](./examples/getting-started.md)
3398
+
3399
+ ### REPL Examples
3400
+
3401
+ - Coachmark - Show Once, Dismiss, Reset (id: `coachmark`)
3402
+ - Persist Progress - Resume, Finish, Replay (id: `persist-progress`)
3403
+ - Tour Basics - Steps, Rects, Completion (id: `tour-basics`)
3404
+
3405
+ ---
3406
+
2672
3407
  ## @vielzeug/clockwork
2673
3408
 
2674
3409
  **Category:** state
@@ -20917,7 +21652,7 @@ Component registration happens through side-effect imports such as `@vielzeug/re
20917
21652
 
20918
21653
  **Layout:** `ore-box`, `ore-grid`, `ore-grid-item`, `ore-navbar`, `ore-sidebar`
20919
21654
 
20920
- **Overlay:** `ore-command-palette`, `ore-command-palette-item`, `ore-dialog`, `ore-drawer`, `ore-menu`, `ore-popover`, `ore-tooltip`
21655
+ **Overlay:** `ore-command-palette`, `ore-command-palette-item`, `ore-dialog`, `ore-drawer`, `ore-menu`, `ore-popover`, `ore-spotlight`, `ore-tooltip`, `ore-tour`
20921
21656
 
20922
21657
  ## Component portfolio
20923
21658
 
@@ -21057,6 +21792,8 @@ import type { OreButtonProps } from '@vielzeug/refine/button';
21057
21792
 
21058
21793
  Each component page lists its attributes, JavaScript properties, events, slots, parts, and CSS custom properties.
21059
21794
 
21795
+ The [`query-bar`](./components/query-bar.md) entry also exports `QueryState`, `QueryTokenDef`, `QueryQuickFilter`, `QueryBarLabels`, and pure grammar helpers. Vocabulary entries default to single selection; `selection: 'multiple'` allows distinct values for the same key. `anyLabel` customizes the unrestricted single-selection choice, such as “Default order” for sorting. Options accept an optional `icon` (registered `ore-icon` name), displayed beside their labels in the drawer. `tokenValues(state, key)` reads all values, `removeToken(state, token)` removes an exact pair, and `clearTokens(state, key)` clears a group. Its unified field contains applied filters, with a compact secondary row for filter entry, quick toggles, and a `toolbar-end` slot for application content such as a result count. Drawer selection uses the same state and vocabulary as typed suggestions. Pending edits block incidental backdrop/swipe dismissal; explicit Cancel, Close, and Escape discard them. Search-hidden selections remain visible in a summary.
21796
+
21060
21797
  ## Framework Types
21061
21798
 
21062
21799
  ```ts