@vielzeug/codex 26.10.15 → 26.10.17

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