@virtualdisplay.io/client 4.0.0-alpha.2 → 4.0.0-alpha.21

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.
package/dist/index.d.ts CHANGED
@@ -1 +1,1158 @@
1
- export { }
1
+ /**
2
+ * Virtualdisplay iframe messaging protocol
3
+ *
4
+ * All message types exchanged between client (parent page)
5
+ * and server (iframe viewer engine) via postMessage.
6
+ */
7
+ declare const MESSAGE_TYPES: {
8
+ readonly HANDSHAKE: "handshake";
9
+ readonly SESSION: "session";
10
+ readonly MUTATION: "mutation";
11
+ readonly STATE: "state";
12
+ readonly LOADING_INDICATOR: "loading_indicator";
13
+ readonly CAMERA: "camera";
14
+ readonly SNAPSHOT: "snapshot";
15
+ readonly MAPPING: "mapping";
16
+ readonly AR_ACTIVATE: "ar_activate";
17
+ readonly FULLSCREEN: "fullscreen";
18
+ readonly CONFIG: "config";
19
+ readonly ANNOTATION_TOGGLE: "annotation_toggle";
20
+ readonly FOCUS: "focus";
21
+ readonly PRICING: "pricing";
22
+ readonly QR_SHOWN: "qr_shown";
23
+ readonly SYNC: "sync";
24
+ };
25
+ interface MutationDto {
26
+ readonly name: string;
27
+ readonly value: string;
28
+ }
29
+ interface MutationResult {
30
+ readonly name: string;
31
+ readonly value: string;
32
+ readonly success: boolean;
33
+ readonly error?: string;
34
+ }
35
+ /**
36
+ * The folded family state riding along on a live sync channel broadcast
37
+ * (ar-session-handoff, sync-channel.md): the same shape the public
38
+ * session-state read returns. Authoritative self-heal input; `mutations`
39
+ * on the same message is only the delta that triggered the broadcast.
40
+ */
41
+ interface SyncStateSnapshot {
42
+ readonly license: string;
43
+ readonly model: string;
44
+ readonly attributes: Record<string, string>;
45
+ }
46
+ interface MutationMessage {
47
+ readonly type: typeof MESSAGE_TYPES.MUTATION;
48
+ readonly mutations: MutationDto[];
49
+ readonly isInitial: boolean;
50
+ /**
51
+ * Live sync channel extras (ar-session-handoff, sync-channel.md), present
52
+ * only on a mutation the viewer engine's sync module received over the SSE
53
+ * live-state channel; a MutationMessage posted locally by the client never
54
+ * carries these. `origin` is the sender's page session id: its presence
55
+ * marks the mutation as remote, so the tracker must never record it again
56
+ * (the no-re-track loop guard, decision 15), and the sync module drops it
57
+ * when it equals the receiving engine's own session id (the echo filter).
58
+ * `seq` is the sender's per-session sequence number, used by the sync
59
+ * module to track how far the channel has progressed. `state` is the
60
+ * authoritative folded family state: the sync module diffs it against the
61
+ * channel's last-seen state and applies the difference, so a catch-up
62
+ * (empty `mutations`) or a broadcast the hub dropped still converges the
63
+ * subscriber. None of these fields are read by `MutationRouter`.
64
+ */
65
+ readonly origin?: string;
66
+ readonly seq?: number;
67
+ readonly state?: SyncStateSnapshot;
68
+ }
69
+ /**
70
+ * Sync channel status (server → client, ar-session-handoff, sync-channel.md):
71
+ * whether the viewer engine's live-state channel is currently connected to its
72
+ * hand-off family. One-way, latest-wins: no history, no buffering. The public
73
+ * client exposes it as `onSyncChange`; the studio composes nothing, so its
74
+ * engine never sends this (it never opens the channel).
75
+ */
76
+ interface SyncMessage {
77
+ readonly type: typeof MESSAGE_TYPES.SYNC;
78
+ readonly connected: boolean;
79
+ }
80
+ interface MutationResultMessage {
81
+ readonly type: typeof MESSAGE_TYPES.MUTATION;
82
+ readonly results: MutationResult[];
83
+ readonly isInitial: boolean;
84
+ }
85
+ /**
86
+ * Viewer settings from model metadata (server → client via MAPPING).
87
+ * Customer-configured in the portal.
88
+ */
89
+ interface ViewerSettings$1 {
90
+ readonly ar?: boolean;
91
+ readonly fullscreen?: boolean;
92
+ readonly screenshot?: boolean;
93
+ readonly cameraControls?: boolean;
94
+ readonly annotations?: boolean;
95
+ readonly language?: string;
96
+ /** Where the attribute controls sit: in-frame bottom-left (default) or a docked right panel. */
97
+ readonly configuratorLayout?: 'inframe-bottom-left' | 'sidebar-right' | 'hidden';
98
+ }
99
+ /**
100
+ * Camera movement limits sent to the client via CONFIG.
101
+ * Used to disable camera control buttons when a limit is reached.
102
+ * Units match the camera position: zoom = percentage (0–200), tilt/rotate = degrees.
103
+ */
104
+ interface CameraLimits {
105
+ readonly minZoom?: number;
106
+ readonly maxZoom?: number;
107
+ readonly minTilt?: number;
108
+ readonly maxTilt?: number;
109
+ readonly minRotate?: number;
110
+ readonly maxRotate?: number;
111
+ }
112
+ interface CameraCommand {
113
+ readonly action: 'rotate' | 'tilt' | 'zoom' | 'reset';
114
+ readonly value?: number;
115
+ }
116
+ interface CameraParameter {
117
+ readonly type: 'rotation' | 'tilt' | 'zoom';
118
+ readonly value: number;
119
+ }
120
+ interface CameraCommandMessage {
121
+ readonly type: typeof MESSAGE_TYPES.CAMERA;
122
+ readonly commands: CameraCommand[];
123
+ }
124
+ interface CameraPositionMessage {
125
+ readonly type: typeof MESSAGE_TYPES.CAMERA;
126
+ readonly transforms: CameraParameter[];
127
+ }
128
+ interface SnapshotRequestMessage {
129
+ readonly type: typeof MESSAGE_TYPES.SNAPSHOT;
130
+ readonly filename: string;
131
+ }
132
+ interface SnapshotResultMessage {
133
+ readonly type: typeof MESSAGE_TYPES.SNAPSHOT;
134
+ readonly filename: string;
135
+ readonly data?: string;
136
+ readonly error?: string;
137
+ }
138
+ interface AttributeDefinition {
139
+ readonly name: string;
140
+ readonly values: string[];
141
+ readonly defaultValue?: string;
142
+ }
143
+ interface MappingMessage {
144
+ readonly type: typeof MESSAGE_TYPES.MAPPING;
145
+ readonly attributes: AttributeDefinition[];
146
+ }
147
+ /**
148
+ * Loading indicator (server → client): progress percentage, phase label, or error.
149
+ * Sent during model download and parse so the client loading indicator can update.
150
+ */
151
+ interface LoadingIndicatorMessage {
152
+ readonly type: typeof MESSAGE_TYPES.LOADING_INDICATOR;
153
+ /** 0–100 */
154
+ readonly progress: number;
155
+ /** Human-readable phase label, e.g. "downloading" */
156
+ readonly phase: string;
157
+ /** Set when loading failed; client shows error state instead of hiding the overlay. */
158
+ readonly error?: string;
159
+ }
160
+ /** AR activate (client → server): user clicked the AR button. Server handles activation. */
161
+ interface ArActivateMessage {
162
+ readonly type: typeof MESSAGE_TYPES.AR_ACTIVATE;
163
+ }
164
+ /**
165
+ * Fullscreen (client → server): the user entered fullscreen via the client's
166
+ * toolbar button. Fullscreen itself is handled client-side; this only lets the
167
+ * server record the engagement against the tracking session it owns.
168
+ */
169
+ interface FullscreenMessage {
170
+ readonly type: typeof MESSAGE_TYPES.FULLSCREEN;
171
+ }
172
+ /**
173
+ * QR shown (client → server): the desktop QR hand-off modal (ar-session-handoff)
174
+ * was displayed to the shopper. The QR itself is generated client-side; this
175
+ * only lets the server record the engagement (`qr_shown`, the funnel's
176
+ * denominator) against the tracking session it owns. One-way, fire-and-forget,
177
+ * nothing to act on, same shape as FULLSCREEN.
178
+ */
179
+ interface QrShownMessage {
180
+ readonly type: typeof MESSAGE_TYPES.QR_SHOWN;
181
+ }
182
+ /**
183
+ * Config (server → client): viewer settings and camera limits from model metadata.
184
+ * Sent early — before MAPPING — so the client can show/hide controls and disable
185
+ * camera buttons at limits as soon as metadata is available.
186
+ */
187
+ interface ConfigMessage {
188
+ readonly type: typeof MESSAGE_TYPES.CONFIG;
189
+ readonly viewerSettings: ViewerSettings$1;
190
+ readonly cameraLimits?: CameraLimits;
191
+ }
192
+ interface HandshakeMessage {
193
+ readonly type: typeof MESSAGE_TYPES.HANDSHAKE;
194
+ }
195
+ /**
196
+ * Session (server → client): the tracking session id, announced once the viewer
197
+ * has established it (the id is generated in the iframe, so this is how the
198
+ * client learns it). The client exposes it so a host page can correlate its own
199
+ * actions (e.g. a configurator conversion) with the viewer session.
200
+ */
201
+ interface SessionMessage {
202
+ readonly type: typeof MESSAGE_TYPES.SESSION;
203
+ readonly sessionId: string;
204
+ }
205
+ /**
206
+ * Annotation toggle (parent → server): show or hide annotation markers.
207
+ * Only works when annotations exist in metadata (grand switch).
208
+ */
209
+ interface AnnotationToggleMessage {
210
+ readonly type: typeof MESSAGE_TYPES.ANNOTATION_TOGGLE;
211
+ readonly enabled: boolean;
212
+ }
213
+ /** Focus actions (client → server). */
214
+ declare const FOCUS_ACTIONS: {
215
+ readonly FOCUS: "focus";
216
+ readonly UNFOCUS: "unfocus";
217
+ };
218
+ /** Focus states (server → client). */
219
+ declare const FOCUS_STATES: {
220
+ readonly FOCUSED: "focused";
221
+ readonly UNFOCUSED: "unfocused";
222
+ };
223
+ type FocusAction = typeof FOCUS_ACTIONS[keyof typeof FOCUS_ACTIONS];
224
+ type FocusStateValue = typeof FOCUS_STATES[keyof typeof FOCUS_STATES];
225
+ /**
226
+ * Focus (client → server): isolate and frame a component, or return to overview.
227
+ * Programmatic — the API, the (future) component menu, and the portal editors all
228
+ * send this. `component` is a name from the `components` metadata, or a raw mesh
229
+ * name as a fallback (resolved server-side); required when action is `focus`.
230
+ */
231
+ interface FocusMessage {
232
+ readonly type: typeof MESSAGE_TYPES.FOCUS;
233
+ readonly action: FocusAction;
234
+ readonly component?: string;
235
+ }
236
+ /**
237
+ * Focus state (server → client): confirms the focus state after the camera and
238
+ * opacity transition. `attributes` lists which attributes are relevant to the
239
+ * focused component, so a custom UI can filter its selectors.
240
+ */
241
+ interface FocusStateMessage {
242
+ readonly type: typeof MESSAGE_TYPES.FOCUS;
243
+ readonly state: FocusStateValue;
244
+ readonly component?: string;
245
+ readonly attributes?: string[];
246
+ }
247
+ /**
248
+ * What the configurator does with a matched configuration. Mirrors the backend
249
+ * `availability` enum (pricing rule provenance lives on `PricingRule.source`).
250
+ */
251
+ declare const AVAILABILITY$1: {
252
+ /** Price and submit button both shown (default). */
253
+ readonly AVAILABLE: "available";
254
+ /** Price shown, submit hidden (sold out / coming soon). */
255
+ readonly VIEW_ONLY: "view_only";
256
+ /** Behaves exactly like an unpriced combination: no price, no button. */
257
+ readonly HIDDEN: "hidden";
258
+ };
259
+ type Availability$1 = typeof AVAILABILITY$1[keyof typeof AVAILABILITY$1];
260
+ /**
261
+ * One pricing rule: a subset of attribute values mapped to a reference + price.
262
+ * Matches the backend pricing read contract (`GET /api/public/models/.../pricing`).
263
+ */
264
+ interface PricingRule$1 {
265
+ /**
266
+ * The attribute name→value pairs this rule names. `{}` is a catch-all;
267
+ * omitted names are wildcards. A rule matches when every pair equals the
268
+ * current selection; the rule naming the most attributes wins.
269
+ */
270
+ readonly attributes: Record<string, string>;
271
+ /** The customer's own identifier (SKU, EAN, artikelnummer, name code). */
272
+ readonly reference: string;
273
+ /** Price in minor units (cents), exactly as the source displays it. */
274
+ readonly priceCents: number;
275
+ readonly availability: Availability$1;
276
+ /** Provenance (manual | imported); audit-only, unused by the resolver. */
277
+ readonly source: 'manual' | 'imported';
278
+ }
279
+ /**
280
+ * A model's pricing document. The viewer's fetcher merges it into the model
281
+ * metadata and the server forwards it to the client via {@link PricingMessage}.
282
+ */
283
+ interface PricingDocument$1 {
284
+ /** Optimistic-concurrency version from the editor; unused by the viewer. */
285
+ readonly version: number;
286
+ /** Customer-authored CTA label; null when the adapter's defaultLabel fills in. */
287
+ readonly buttonLabel: string | null;
288
+ /** ISO 4217 currency code for price formatting. */
289
+ readonly currency: string;
290
+ /** VAT label for bare embeds, e.g. "incl. btw". */
291
+ readonly vatDisplay: string;
292
+ readonly rules: PricingRule$1[];
293
+ }
294
+ /**
295
+ * Pricing (server → client): the model's pricing document. Sent once at startup
296
+ * after MAPPING so the client's resolver can price the current configuration.
297
+ */
298
+ interface PricingMessage {
299
+ readonly type: typeof MESSAGE_TYPES.PRICING;
300
+ readonly pricing: PricingDocument$1;
301
+ }
302
+ type Message = HandshakeMessage | SessionMessage | MutationMessage | MutationResultMessage | CameraCommandMessage | CameraPositionMessage | SnapshotRequestMessage | SnapshotResultMessage | MappingMessage | LoadingIndicatorMessage | ArActivateMessage | FullscreenMessage | ConfigMessage | AnnotationToggleMessage | FocusMessage | FocusStateMessage | PricingMessage | QrShownMessage | SyncMessage;
303
+ type MessageCallback = (message: Message) => void | Promise<void>;
304
+
305
+ /**
306
+ * Options for IframeMessenger
307
+ */
308
+ interface MessengerOptions {
309
+ /**
310
+ * Buffer incoming messages until markConsumerReady().
311
+ * When false (default), incoming messages are processed immediately.
312
+ */
313
+ bufferIncoming?: boolean;
314
+ /**
315
+ * When buffering, messages where this returns false bypass the buffer
316
+ * and are routed to handlers immediately. Used for urgent messages
317
+ * like CONFIG that must be handled before the model loads.
318
+ */
319
+ shouldBuffer?: (message: Message) => boolean;
320
+ /**
321
+ * Restrict postMessage to a specific origin (e.g. 'https://example.com').
322
+ * Defaults to '*' when not set. Set this when the target origin is known
323
+ * to prevent messages from being intercepted by unintended recipients.
324
+ */
325
+ targetOrigin?: string;
326
+ /**
327
+ * Which protocol to validate incoming messages against — not whether to validate.
328
+ * Both sides always reject browser-extension noise and unknown message types. The
329
+ * default (`isValidMessageData`) accepts the public protocol; the studio side passes
330
+ * `isValidStudioMessageData`, which accepts the public protocol plus the studio
331
+ * messages (e.g. annotation placement), so the public bundle stays lean.
332
+ */
333
+ isValid?: (data: unknown) => boolean;
334
+ }
335
+ /**
336
+ * Bidirectional iframe message port
337
+ *
338
+ * Handles sending and receiving messages via postMessage between
339
+ * a parent page and an iframe. Both client and server use this —
340
+ * the only difference is the target window.
341
+ *
342
+ * Features:
343
+ * - Publisher queue: outgoing messages buffered until markReady()
344
+ * - Consumer queue: incoming messages buffered until markConsumerReady() (opt-in)
345
+ * - Handler registration: route incoming messages by type
346
+ * - Echo prevention: tags outgoing messages to filter self-delivery
347
+ * - Cleanup: removes listener on destroy
348
+ *
349
+ * @example Client (parent page — publisher queue only)
350
+ * ```ts
351
+ * const messenger = new IframeMessenger(() => iframe.contentWindow);
352
+ * messenger.on('mutation', handleMutationResult);
353
+ * messenger.on('mapping', handleMapping);
354
+ * messenger.start();
355
+ * // When iframe loads:
356
+ * messenger.markReady();
357
+ * messenger.send({ type: 'mutation', mutations, isInitial: true });
358
+ * ```
359
+ *
360
+ * @example Server (inside iframe — both queues, CONFIG bypasses consumer)
361
+ * ```ts
362
+ * const messenger = new IframeMessenger(
363
+ * () => window.parent !== window ? window.parent : null,
364
+ * { bufferIncoming: true, shouldBuffer: (msg) => msg.type !== 'config' },
365
+ * );
366
+ * messenger.on('mutation', processMutation);
367
+ * messenger.on('config', applyConfig);
368
+ * messenger.start();
369
+ * messenger.markReady();
370
+ * // When model loads:
371
+ * messenger.markConsumerReady();
372
+ * ```
373
+ */
374
+ declare class IframeMessenger {
375
+ private readonly getTarget;
376
+ private readonly handlers;
377
+ private readonly publisher;
378
+ private readonly consumer;
379
+ private readonly shouldBuffer;
380
+ private listener;
381
+ private readonly sourceId;
382
+ private targetOrigin;
383
+ private readonly isValid;
384
+ constructor(getTarget: () => Window | null, options?: MessengerOptions);
385
+ /**
386
+ * Register a handler for a specific message type
387
+ */
388
+ on(type: string, handler: MessageCallback): void;
389
+ /**
390
+ * Remove a handler for a specific message type
391
+ */
392
+ off(type: string): void;
393
+ /**
394
+ * Update the target origin for outgoing postMessage calls.
395
+ * Use when the target origin is discovered at runtime (e.g. from the first incoming message).
396
+ */
397
+ setTargetOrigin(origin: string): void;
398
+ /**
399
+ * Send a message to the target window
400
+ * Queued until markReady() is called
401
+ */
402
+ send(message: Message): void;
403
+ /**
404
+ * Feed a message through the same routing path as one that just arrived via
405
+ * postMessage, without an actual postMessage round-trip: still validated
406
+ * against `isValid`, and still respects the consumer queue's buffering and
407
+ * the `shouldBuffer` bypass. For a transport that delivers messages some
408
+ * other way than the iframe channel this messenger already listens on (the
409
+ * viewer engine's SSE-sourced live sync channel, ar-session-handoff) and
410
+ * needs them to land exactly where a postMessage would.
411
+ */
412
+ receive(message: Message): void;
413
+ /**
414
+ * Enable sending and flush queued outgoing messages
415
+ */
416
+ markReady(): void;
417
+ /**
418
+ * Enable processing of buffered incoming messages
419
+ * Only relevant when constructed with bufferIncoming: true
420
+ */
421
+ markConsumerReady(): void;
422
+ /**
423
+ * Check if an incoming message of a specific type is pending in the consumer buffer
424
+ */
425
+ consumerHasPending(type: string): boolean;
426
+ /**
427
+ * Start listening for incoming messages
428
+ */
429
+ start(): void;
430
+ /**
431
+ * Stop listening and clean up all state
432
+ */
433
+ destroy(): void;
434
+ /**
435
+ * Whether the messenger is ready to send
436
+ */
437
+ get ready(): boolean;
438
+ /**
439
+ * Number of messages waiting in the outgoing queue
440
+ */
441
+ get pendingCount(): number;
442
+ private post;
443
+ private routeToHandler;
444
+ }
445
+
446
+ type SendFn$1 = (message: Message) => void;
447
+ /**
448
+ * Annotation display controls for the viewer: show or hide existing markers.
449
+ *
450
+ * Placement (click-to-place for the editor) is a studio capability and lives in
451
+ * `@virtualdisplay.io/client-studio`, never in the public client or its engine surface.
452
+ */
453
+ declare class Annotations {
454
+ private readonly send;
455
+ constructor(send: SendFn$1);
456
+ setEnabled(enabled: boolean): void;
457
+ }
458
+
459
+ /**
460
+ * Camera control API for positioning and zoom
461
+ * Uses chaining pattern for building commands
462
+ */
463
+ declare class Camera {
464
+ private readonly send;
465
+ private commands;
466
+ constructor(send: MessageCallback);
467
+ /**
468
+ * Rotate camera horizontally
469
+ * @param degrees - Absolute rotation in degrees (0 = front view)
470
+ * @throws VirtualdisplayError if value is not a finite number
471
+ */
472
+ rotate(degrees: number): this;
473
+ /**
474
+ * Tilt camera vertically
475
+ * @param degrees - Absolute tilt in degrees (0 = top view, 90 = horizontal)
476
+ * @throws VirtualdisplayError if value is not a finite number
477
+ */
478
+ tilt(degrees: number): this;
479
+ /**
480
+ * Zoom camera
481
+ * @param percentage - Zoom percentage (100 = normal, >100 = zoom in, <100 = zoom out)
482
+ * @throws VirtualdisplayError if value is not a finite number
483
+ */
484
+ zoom(percentage: number): this;
485
+ /**
486
+ * Execute accumulated camera commands
487
+ * @throws VirtualdisplayError if no commands have been accumulated
488
+ */
489
+ set(): void;
490
+ private validateFiniteNumber;
491
+ /**
492
+ * Reset camera to initial position
493
+ * This is not chainable and executes immediately
494
+ */
495
+ reset(): void;
496
+ }
497
+
498
+ /**
499
+ * Camera position state object
500
+ */
501
+ interface CameraPositionState {
502
+ rotation: number;
503
+ tilt: number;
504
+ zoom: number;
505
+ }
506
+ /**
507
+ * Camera position state
508
+ * Tracks current rotation, tilt, zoom, and movement limits from server updates.
509
+ */
510
+ declare class CameraPosition {
511
+ private readonly state;
512
+ private limitsState;
513
+ private readonly changeCallbacks;
514
+ constructor();
515
+ /**
516
+ * Get current rotation in degrees
517
+ */
518
+ get rotation(): number;
519
+ /**
520
+ * Get current tilt in degrees
521
+ */
522
+ get tilt(): number;
523
+ /**
524
+ * Get current zoom percentage
525
+ */
526
+ get zoom(): number;
527
+ /**
528
+ * Subscribe to camera position changes.
529
+ * Also fires when limits are applied via applyLimits() so components
530
+ * can re-evaluate disabled states without a separate callback.
531
+ */
532
+ onChange(callback: (position: CameraPositionState) => void): void;
533
+ /**
534
+ * Current camera movement limits (set from CONFIG message).
535
+ * Undefined until the server sends limits in its CONFIG payload.
536
+ */
537
+ get limits(): CameraLimits | undefined;
538
+ /**
539
+ * Apply camera limits received from the server CONFIG message.
540
+ * Fires onChange callbacks so button disabled states update immediately.
541
+ */
542
+ applyLimits(limits: CameraLimits | undefined): void;
543
+ /**
544
+ * Handle incoming camera position message from server
545
+ */
546
+ handlePositionUpdate(message: CameraPositionMessage): void;
547
+ /**
548
+ * Update camera position from server transforms
549
+ */
550
+ update(transforms: CameraParameter[]): void;
551
+ }
552
+
553
+ /** The buyer's current configuration as attribute name→value pairs. */
554
+ type Selection = Record<string, string>;
555
+
556
+ /**
557
+ * The buyer's current configuration once it resolves to a price. `availability`
558
+ * is `available` (price + submit) or `view_only` (price, no submit); a matched
559
+ * `hidden` rule and a no-match both surface as `undefined` instead.
560
+ */
561
+ interface ResolvedConfiguration {
562
+ readonly attributes: Record<string, string>;
563
+ readonly reference: string;
564
+ /** Price in minor units (cents), exactly as the source displays it. */
565
+ readonly price: number;
566
+ readonly currency: string;
567
+ readonly availability: 'available' | 'view_only';
568
+ }
569
+ /**
570
+ * What submit hands to the host: the buyer's selection and the reference to act
571
+ * on. Deliberately carries no price — the shop's checkout price is binding and
572
+ * ours is display-only, so handing it into the cart hand-off would only invite a
573
+ * wrong-price error. Price stays on the display side (`current`, `onChange`).
574
+ */
575
+ interface SubmittedConfiguration {
576
+ readonly attributes: Record<string, string>;
577
+ readonly reference: string;
578
+ }
579
+ type ChangeCallback = (configuration: ResolvedConfiguration | undefined) => void;
580
+ type SubmitCallback = (configuration: SubmittedConfiguration) => void | Promise<unknown>;
581
+ type Dispatch<T> = (detail: T) => void;
582
+ /** Options for {@link Configuration.onSubmit}. */
583
+ interface SubmitOptions {
584
+ /** Fallback button label when the customer authored none (the adapter brings it). */
585
+ readonly defaultLabel?: string;
586
+ }
587
+ /**
588
+ * Configuration API — resolves the buyer's selection to a reference + price and
589
+ * carries it to the host page.
590
+ *
591
+ * Follows the existing facade style (`client.camera`, `client.focus`). It holds
592
+ * the pricing document and the latest selection, re-resolving on every change.
593
+ * `onChange` fires with the resolved configuration (or `undefined` when nothing
594
+ * matches); alongside it the container dispatches a bubbling `vd:configuration-change`
595
+ * CustomEvent so a page reacts without registering a callback. `onSubmit` registers
596
+ * the host handler that the CTA hands the configuration to.
597
+ */
598
+ declare class Configuration {
599
+ private readonly dispatchChange;
600
+ private readonly dispatchSubmit;
601
+ private pricing;
602
+ private selection;
603
+ private resolved;
604
+ private readonly changeCallbacks;
605
+ private readonly renderCallbacks;
606
+ private submitCallback;
607
+ private submitDefaultLabel;
608
+ constructor(dispatchChange: Dispatch<ResolvedConfiguration | undefined>, dispatchSubmit: Dispatch<SubmittedConfiguration>);
609
+ /** The resolved configuration, or undefined when nothing matches. */
610
+ get current(): ResolvedConfiguration | undefined;
611
+ /** The VAT label for bare embeds (e.g. "incl. btw"), or undefined without pricing. */
612
+ get vatDisplay(): string | undefined;
613
+ /**
614
+ * The CTA label: the customer's `button_label`, or the adapter's `defaultLabel`
615
+ * when that is empty. Undefined when neither is set — a button must know what
616
+ * it says, so the CTA does not render.
617
+ */
618
+ get buttonLabel(): string | undefined;
619
+ /**
620
+ * Whether the current configuration may be submitted: a handler is registered
621
+ * and the configuration is either price-less or an `available` priced one. The
622
+ * CTA's visibility ({@link submitEnabled}) layers a label requirement on top of
623
+ * this; a programmatic `submit()` does not need a label.
624
+ */
625
+ private get submittable();
626
+ /**
627
+ * Whether the CTA should render: the configuration is submittable and a label
628
+ * exists. A button must say something, so an empty label hides the CTA rather
629
+ * than rendering blank.
630
+ */
631
+ get submitEnabled(): boolean;
632
+ /**
633
+ * Subscribe to configuration changes. Fires after every selection change with
634
+ * the resolved configuration, including undefined when nothing matches.
635
+ */
636
+ onChange(callback: ChangeCallback): void;
637
+ /**
638
+ * Register the submit handler the CTA hands the configuration to. Registering
639
+ * one is what switches the button on (capability driven). `defaultLabel` fills
640
+ * in when the customer authored no button label.
641
+ */
642
+ onSubmit(callback: SubmitCallback, options?: SubmitOptions): void;
643
+ /**
644
+ * Subscribe the built-in UI to re-render the price + CTA. Fires on every
645
+ * resolve and when a submit handler is registered (which can switch the button
646
+ * on without a configuration change). Separate from `onChange`, which stays the
647
+ * host-facing, mutation-driven contract. Returns a disposer.
648
+ */
649
+ onRender(callback: () => void): () => void;
650
+ /**
651
+ * Hand the current configuration to the registered handler and fire the
652
+ * bubbling `vd:configuration-submit` event. No-op without a handler or a
653
+ * resolved configuration. Returns the handler's promise so the caller can
654
+ * reflect the result (bezig / gelukt / mislukt).
655
+ */
656
+ submit(): Promise<void>;
657
+ /** Called by the client when a PRICING message arrives from the viewer. */
658
+ handlePricingReceived(pricing: PricingDocument$1): void;
659
+ /** Called by the client after every mutation result, with the full selection. */
660
+ handleSelectionChanged(selection: Selection): void;
661
+ private recompute;
662
+ private notifyRender;
663
+ private resolve;
664
+ }
665
+
666
+ /** The focus state reported to `onChange` and the `vd:focus-change` event. */
667
+ interface FocusState {
668
+ readonly component: string;
669
+ readonly attributes: string[];
670
+ }
671
+ type FocusChange = FocusState | null;
672
+ type FocusChangeCallback = (state: FocusChange) => void;
673
+ type DispatchFn = (state: FocusChange) => void;
674
+ /**
675
+ * Focus control API — isolate and frame a component, or return to overview.
676
+ *
677
+ * Programmatic by design: `set('Seat')` needs no user selection, which is how the
678
+ * (future) component menu and the portal/admin editors drive it over the same
679
+ * message bus. Alongside `onChange`, the container dispatches a bubbling
680
+ * `vd:focus-change` CustomEvent so a page can react without registering a callback.
681
+ */
682
+ declare class Focus {
683
+ private readonly send;
684
+ private readonly dispatch;
685
+ private focused;
686
+ private attributes;
687
+ private readonly changeCallbacks;
688
+ constructor(send: MessageCallback, dispatch: DispatchFn);
689
+ /** The focused component name, or null in overview. */
690
+ get current(): string | null;
691
+ /**
692
+ * Focus a component by name. Sends a focus message to the viewer.
693
+ * @throws VirtualdisplayError if component is not a non-empty string
694
+ */
695
+ set(component: string): void;
696
+ /** Return to the overview — unfocus the current component. */
697
+ clear(): void;
698
+ /**
699
+ * Subscribe to focus changes. The callback fires with the focus state when a
700
+ * component is focused, and with null when returning to overview.
701
+ */
702
+ onChange(callback: FocusChangeCallback): void;
703
+ /** Called by the client when a FocusStateMessage is received from the viewer. */
704
+ handleStateReceived(message: FocusStateMessage): void;
705
+ }
706
+
707
+ /**
708
+ * Error codes for Virtualdisplay client errors
709
+ */
710
+ declare const ERROR_CODES: {
711
+ readonly NO_MAPPING: "NO_MAPPING";
712
+ readonly ATTRIBUTE_NOT_FOUND: "ATTRIBUTE_NOT_FOUND";
713
+ readonly VALUE_NOT_FOUND: "VALUE_NOT_FOUND";
714
+ readonly PARENT_NOT_FOUND: "PARENT_NOT_FOUND";
715
+ readonly INVALID_PARAMETER: "INVALID_PARAMETER";
716
+ readonly SNAPSHOT_FAILED: "SNAPSHOT_FAILED";
717
+ };
718
+ type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];
719
+ /**
720
+ * Custom error class for Virtualdisplay client errors
721
+ */
722
+ declare class VirtualdisplayError extends Error {
723
+ readonly code: ErrorCode;
724
+ constructor(message: string, code: ErrorCode);
725
+ static valueNotFound(attributeName: string, valueName: string): VirtualdisplayError;
726
+ static parentNotFound(selector: string): VirtualdisplayError;
727
+ static invalidParameter(reason: string): VirtualdisplayError;
728
+ }
729
+
730
+ interface PhotoData {
731
+ readonly filename: string;
732
+ readonly data: string;
733
+ }
734
+ /**
735
+ * Represents a photo being captured
736
+ * Pure domain object that manages callbacks for when the photo is developed
737
+ */
738
+ declare class Photo {
739
+ private readonly filename;
740
+ private readonly developedCallbacks;
741
+ private readonly errorCallbacks;
742
+ private developed;
743
+ private failed;
744
+ private photoData;
745
+ constructor(filename: string);
746
+ /**
747
+ * Register a callback for when the photo is developed
748
+ * If the photo is already developed, the callback is called immediately
749
+ *
750
+ * @param callback Function to call with photo data
751
+ * @returns This Photo instance for chaining
752
+ */
753
+ onDeveloped(callback: (photo: PhotoData) => void): this;
754
+ /**
755
+ * Register a callback for when the photo capture fails
756
+ *
757
+ * @param callback Function to call with error
758
+ * @returns This Photo instance for chaining
759
+ */
760
+ onError(callback: (error: VirtualdisplayError) => void): this;
761
+ /**
762
+ * Get the filename for this photo
763
+ */
764
+ getFilename(): string;
765
+ /**
766
+ * Mark the photo as developed and notify all callbacks
767
+ * This method is called by Snapshot when the photo data arrives from the server
768
+ */
769
+ develop(photoData: PhotoData): void;
770
+ /**
771
+ * Mark the photo as failed and notify error callbacks
772
+ */
773
+ fail(error: VirtualdisplayError): void;
774
+ }
775
+
776
+ /**
777
+ * API for taking screenshots of the 3D model
778
+ * Following the camera analogy: take() triggers capture, onDeveloped() gets result
779
+ */
780
+ declare class Snapshot {
781
+ private readonly send;
782
+ private readonly validExtensions;
783
+ private readonly pendingPhotos;
784
+ constructor(send: MessageCallback);
785
+ /**
786
+ * Take a snapshot of the current 3D model view
787
+ *
788
+ * @param filename The filename for the snapshot (must include valid extension)
789
+ * @returns Photo object for registering callbacks
790
+ * @throws VirtualdisplayError if filename is invalid
791
+ */
792
+ take(filename: string): Photo;
793
+ /**
794
+ * Handle incoming snapshot result from server
795
+ * Matches the message to a pending photo by filename, then develops or fails it
796
+ */
797
+ handleSnapshotResult(message: SnapshotResultMessage): void;
798
+ /**
799
+ * Fail any pending photos and forget them. Called from client destroy
800
+ * so callers awaiting onError() see a deterministic outcome instead of
801
+ * leaking captures whose results will never arrive.
802
+ */
803
+ clear(): void;
804
+ /**
805
+ * Validate filename has a valid image extension
806
+ */
807
+ private validateFilename;
808
+ }
809
+
810
+ /**
811
+ * Pure domain entity representing a single attribute value
812
+ * No side effects - only state
813
+ */
814
+ declare class AttributeValue {
815
+ readonly value: string;
816
+ readonly attribute: string;
817
+ private isSelectedValue;
818
+ constructor(value: string, attribute: string);
819
+ get isSelected(): boolean;
820
+ setSelected(selected: boolean): void;
821
+ }
822
+
823
+ interface AttributeOptions {
824
+ readonly name: string;
825
+ readonly values: string[];
826
+ readonly onSelect: (name: string, value: string) => void;
827
+ readonly defaultValue?: string;
828
+ }
829
+ /**
830
+ * Represents a single attribute with its available values
831
+ * Flat API - no source/target translation, works with server values directly
832
+ */
833
+ declare class Attribute {
834
+ readonly name: string;
835
+ readonly defaultValue: string | undefined;
836
+ private readonly values;
837
+ private readonly onSelect;
838
+ private onChangeCallback?;
839
+ constructor(options: AttributeOptions);
840
+ /**
841
+ * Select a value for this attribute
842
+ * @throws {VirtualdisplayError} When value not found
843
+ */
844
+ select(value: string): void;
845
+ getValues(): AttributeValue[];
846
+ getValue(value: string): AttributeValue | undefined;
847
+ /** The currently selected value, or undefined before any selection lands. */
848
+ get selected(): string | undefined;
849
+ /**
850
+ * Register callback that fires when selection changes
851
+ * Receives the newly selected AttributeValue
852
+ */
853
+ onChange(callback: (selectedValue: AttributeValue) => void): void;
854
+ /**
855
+ * Update selection state from server mutation result
856
+ * Direct value comparison - no source/target translation
857
+ */
858
+ handleResult(serverValue: string): void;
859
+ private createValues;
860
+ }
861
+
862
+ /**
863
+ * Configuration options for VirtualdisplayClient
864
+ */
865
+ interface ClientOptions {
866
+ readonly parent: string | HTMLElement;
867
+ readonly model: string;
868
+ readonly license: string;
869
+ readonly debug?: boolean;
870
+ readonly language?: string;
871
+ /** W3C traceparent string to propagate to the viewer iframe for end-to-end tracing. */
872
+ readonly traceparent?: string;
873
+ /**
874
+ * A source viewer session id to restore into this mount (ar-session-handoff): the
875
+ * iframe's metadata fetch merges that session's latest configuration into the
876
+ * attribute defaults, so the viewer boots straight into it. Internal option, used
877
+ * by the AR landing, not a customer-facing setting.
878
+ */
879
+ readonly session?: string;
880
+ /**
881
+ * Override viewer settings from metadata. Omitted fields use the metadata value.
882
+ * A warning is logged when an override differs from the metadata value.
883
+ */
884
+ readonly viewerSettings?: Partial<ViewerSettings$1>;
885
+ }
886
+
887
+ /**
888
+ * The Virtualdisplay client's DOM event names, in one registry.
889
+ *
890
+ * - `ui:*` events dispatch on the container (non-bubbling). They are the typed
891
+ * surface of `client.on(...)` — payloads in `VirtualdisplayUiEventMap` (./types).
892
+ * - `vd:*` events bubble into the customer's page, so a page can react with
893
+ * `addEventListener` without registering a client callback.
894
+ */
895
+ declare const VD_EVENTS: {
896
+ /** Attribute selection requested (cancelable). `detail`: `{ attribute, value }`. */
897
+ readonly SELECT: "ui:select";
898
+ /** Attribute selection applied. `detail`: `{ attribute, value }`. */
899
+ readonly SELECTED: "ui:selected";
900
+ /** Focus changed (bubbles). `detail`: `{ component, attributes }`, or `null` for the overview. */
901
+ readonly FOCUS_CHANGE: "vd:focus-change";
902
+ /** Resolved configuration changed (bubbles). `detail`: the resolved configuration, or `null` when nothing matches. */
903
+ readonly CONFIGURATION_CHANGE: "vd:configuration-change";
904
+ /** Configuration submitted (bubbles). `detail`: the resolved configuration handed to the host. */
905
+ readonly CONFIGURATION_SUBMIT: "vd:configuration-submit";
906
+ };
907
+ type VdEvent = typeof VD_EVENTS[keyof typeof VD_EVENTS];
908
+
909
+ interface MutationFailedEvent {
910
+ readonly attribute: string;
911
+ readonly value: string;
912
+ }
913
+ interface VirtualdisplaySelectEvent {
914
+ readonly attribute: string;
915
+ readonly value: string;
916
+ preventDefault(): void;
917
+ }
918
+ interface VirtualdisplaySelectedEvent {
919
+ readonly attribute: string;
920
+ readonly value: string;
921
+ }
922
+ type VirtualdisplayUiEventMap = {
923
+ [VD_EVENTS.SELECT]: VirtualdisplaySelectEvent;
924
+ [VD_EVENTS.SELECTED]: VirtualdisplaySelectedEvent;
925
+ };
926
+ type VirtualdisplayUiEventType = keyof VirtualdisplayUiEventMap;
927
+
928
+ type SendFn = (message: Message) => void;
929
+ /**
930
+ * The seam for the AR flow behind the standard interface's AR button. The base
931
+ * only routes to it: the implementation (mobile AR_ACTIVATE, desktop hand-off
932
+ * QR) lives in the public client package (`@virtualdisplay.io/client`), which
933
+ * composes it via {@link BaseClientExtensions.createArActivation}.
934
+ */
935
+ interface ArFlow {
936
+ /** The AR button handler. */
937
+ activate(): void;
938
+ /** The engine-announced tracking session id (SESSION), which the flow may wait on. */
939
+ onSession(sessionId: string): void;
940
+ destroy(): void;
941
+ }
942
+ /**
943
+ * The seam for the live sync channel's connection status (ar-session-handoff,
944
+ * sync-channel.md). The base only routes SYNC messages to it; the concrete
945
+ * tracker (callback list + `onSyncChange`) lives in the public client package,
946
+ * composed via {@link BaseClientExtensions.createSyncTracker}. The studio
947
+ * composes nothing, so `onSyncChange` does not exist on the studio client
948
+ * (its engine never opens the channel, so it never sends SYNC either).
949
+ */
950
+ interface SyncTracker {
951
+ /** Latest-wins, no buffering: the channel's current connection state. */
952
+ handleSync(connected: boolean): void;
953
+ }
954
+ /**
955
+ * Extensions a subclass (the studio client) layers onto the base composition without
956
+ * re-implementing it: a custom incoming-message validator and extra message handlers.
957
+ */
958
+ interface BaseClientExtensions {
959
+ readonly messengerOptions?: MessengerOptions;
960
+ readonly registerHandlers?: (messenger: IframeMessenger, send: SendFn) => void;
961
+ /**
962
+ * The server host this client's viewer iframe targets — a fixed fact per client
963
+ * type, injected like the validator, not a per-instance setting. The studio client
964
+ * fixes it to the studio viewer build; the public client leaves it unset and uses
965
+ * the default public viewer. Both clients are bundled into one portal/admin build,
966
+ * so the host cannot be a single build-time env shared by both.
967
+ */
968
+ readonly serverHost?: string;
969
+ /**
970
+ * The AR flow behind the standard interface's AR button. The public client
971
+ * composes its `ArActivation` here; the studio composes nothing (the editor
972
+ * has no AR use case), so the base wires no AR button at all.
973
+ */
974
+ readonly createArActivation?: (send: SendFn) => ArFlow;
975
+ /**
976
+ * The live sync channel's connection tracker (ar-session-handoff). The
977
+ * public client composes a tracker and exposes `onSyncChange`; the studio
978
+ * composes nothing, so SYNC messages (which its own engine never sends
979
+ * anyway) are simply dropped.
980
+ */
981
+ readonly createSyncTracker?: () => SyncTracker;
982
+ }
983
+ /**
984
+ * The shared client composition root. Wires the engine (transport, facades, UI) and exposes
985
+ * the buyer-facing API. The public `VirtualdisplayClient` is this base as-is; the studio
986
+ * client extends it with the studio facades via {@link BaseClientExtensions}.
987
+ */
988
+ declare class BaseClient {
989
+ private readonly attributes;
990
+ private readonly readyStateTracker;
991
+ private readonly messenger;
992
+ private readonly cameraApi;
993
+ private readonly cameraPositionApi;
994
+ private readonly focusApi;
995
+ private readonly configurationApi;
996
+ private readonly snapshotApi;
997
+ private readonly annotationsApi;
998
+ private readonly eventBus;
999
+ private readonly ui;
1000
+ private readonly options;
1001
+ private readonly serverHost;
1002
+ /** The composed AR flow, or undefined in builds without one (the studio). */
1003
+ private readonly arActivation;
1004
+ /** The composed sync tracker, or undefined in builds without one (the studio). */
1005
+ private readonly syncTracker;
1006
+ private iframe;
1007
+ private container;
1008
+ private readyCallbacks;
1009
+ private readonly mutationFailedCallbacks;
1010
+ /** The viewer's tracking session id, once the server announces it (SESSION). */
1011
+ private viewerSessionId;
1012
+ constructor(options: ClientOptions, extensions?: BaseClientExtensions);
1013
+ /** Wire the inbound viewer protocol to the facades. */
1014
+ private wireIncomingMessages;
1015
+ /** Store the tracking session id and hand it to the AR flow, which waits on it for the QR. */
1016
+ private handleSession;
1017
+ /** Forward a failed mutation to every registered `onMutationFailed` callback. */
1018
+ private notifyMutationFailed;
1019
+ /** Flush the queued `onReady` callbacks once, then let the standard interface know. */
1020
+ private notifyReady;
1021
+ /**
1022
+ * Dispatch a bubbling `vd:focus-change` event on the container so a page can
1023
+ * react to focus without registering an `onChange` callback.
1024
+ */
1025
+ private dispatchFocusChange;
1026
+ /**
1027
+ * Dispatch a bubbling `vd:configuration-change` event on the container so a
1028
+ * page or tag manager can react to price changes without a client callback.
1029
+ */
1030
+ private dispatchConfigurationChange;
1031
+ /** Dispatch a bubbling `vd:configuration-submit` event when the CTA is used. */
1032
+ private dispatchConfigurationSubmit;
1033
+ private handleConfig;
1034
+ /** Tell the viewer the user entered fullscreen, so it records the engagement. */
1035
+ private trackFullscreen;
1036
+ private initializeLogging;
1037
+ private createAndConnectIframe;
1038
+ select(name: string, value: string): void;
1039
+ getValues(name: string): AttributeValue[];
1040
+ getValue(name: string, value: string): AttributeValue | undefined;
1041
+ onChange(name: string, callback: (selectedValue: AttributeValue) => void): void;
1042
+ getAttribute(name: string): Attribute | undefined;
1043
+ get isReady(): boolean;
1044
+ onReady(callback: (client: this) => void): void;
1045
+ onMutationFailed(callback: (event: MutationFailedEvent) => void): void;
1046
+ /**
1047
+ * Trigger the composed AR flow directly (ar-session-handoff task 035): a host
1048
+ * page that hides the built-in corner button (`viewerSettings.ar = false`,
1049
+ * e.g. the AR landing, which owns a single custom AR affordance) still needs
1050
+ * an entry point to the same flow that button would have called. A no-op
1051
+ * where no AR flow is composed (the studio client has none).
1052
+ */
1053
+ activateAr(): void;
1054
+ on<T extends VirtualdisplayUiEventType>(type: T, callback: (event: VirtualdisplayUiEventMap[T]) => void): void;
1055
+ get annotations(): Annotations;
1056
+ get camera(): Camera;
1057
+ get focus(): Focus;
1058
+ get configuration(): Configuration;
1059
+ get cameraPosition(): CameraPosition;
1060
+ get snapshot(): Snapshot;
1061
+ /** The viewer's tracking session id, for a host page to correlate its own actions. */
1062
+ get sessionId(): string | undefined;
1063
+ get viewerSettings(): ViewerSettings$1;
1064
+ destroy(): void;
1065
+ }
1066
+
1067
+ type Attrs = Record<string, unknown> | unknown;
1068
+ declare let logger: {
1069
+ debug: (msg: string, attrs?: Attrs) => void;
1070
+ info: (msg: string, attrs?: Attrs) => void;
1071
+ warn: (msg: string, attrs?: Attrs) => void;
1072
+ error: (msg: string, attrs?: Attrs) => void;
1073
+ };
1074
+ declare function enableDebugMode(): void;
1075
+ declare function isDebugMode(): boolean;
1076
+
1077
+ /**
1078
+ * The public Virtualdisplay client: the shared base composition (transport, facades, the
1079
+ * standard interface) with no studio capability. This is the buyer-facing SDK published as
1080
+ * `@virtualdisplay.io/client`. The internal studio client adds the editing facades over the
1081
+ * same base in `@virtualdisplay.io/client-studio`.
1082
+ *
1083
+ * The AR flow (mobile AR_ACTIVATE, desktop hand-off QR) is composed here: it is a shopper
1084
+ * feature, so the studio client does not compose it and renders no AR button. The live sync
1085
+ * channel's connection status (`onSyncChange`) is composed the same way: the studio client
1086
+ * has no such method, its engine never opens the channel.
1087
+ */
1088
+ declare class VirtualdisplayClient extends BaseClient {
1089
+ private readonly syncStatusTracker;
1090
+ constructor(options: ClientOptions);
1091
+ /** Subscribe to the live sync channel's connection state (ar-session-handoff). Latest-wins, no buffering. */
1092
+ onSyncChange(callback: (connected: boolean) => void): void;
1093
+ }
1094
+
1095
+ /**
1096
+ * Viewer settings from model metadata (server → client via MAPPING).
1097
+ * Customer-configured in the portal.
1098
+ */
1099
+ interface ViewerSettings {
1100
+ readonly ar?: boolean;
1101
+ readonly fullscreen?: boolean;
1102
+ readonly screenshot?: boolean;
1103
+ readonly cameraControls?: boolean;
1104
+ readonly annotations?: boolean;
1105
+ readonly language?: string;
1106
+ /** Where the attribute controls sit: in-frame bottom-left (default) or a docked right panel. */
1107
+ readonly configuratorLayout?: 'inframe-bottom-left' | 'sidebar-right' | 'hidden';
1108
+ }
1109
+ /**
1110
+ * What the configurator does with a matched configuration. Mirrors the backend
1111
+ * `availability` enum (pricing rule provenance lives on `PricingRule.source`).
1112
+ */
1113
+ declare const AVAILABILITY: {
1114
+ /** Price and submit button both shown (default). */
1115
+ readonly AVAILABLE: "available";
1116
+ /** Price shown, submit hidden (sold out / coming soon). */
1117
+ readonly VIEW_ONLY: "view_only";
1118
+ /** Behaves exactly like an unpriced combination: no price, no button. */
1119
+ readonly HIDDEN: "hidden";
1120
+ };
1121
+ type Availability = typeof AVAILABILITY[keyof typeof AVAILABILITY];
1122
+ /**
1123
+ * One pricing rule: a subset of attribute values mapped to a reference + price.
1124
+ * Matches the backend pricing read contract (`GET /api/public/models/.../pricing`).
1125
+ */
1126
+ interface PricingRule {
1127
+ /**
1128
+ * The attribute name→value pairs this rule names. `{}` is a catch-all;
1129
+ * omitted names are wildcards. A rule matches when every pair equals the
1130
+ * current selection; the rule naming the most attributes wins.
1131
+ */
1132
+ readonly attributes: Record<string, string>;
1133
+ /** The customer's own identifier (SKU, EAN, artikelnummer, name code). */
1134
+ readonly reference: string;
1135
+ /** Price in minor units (cents), exactly as the source displays it. */
1136
+ readonly priceCents: number;
1137
+ readonly availability: Availability;
1138
+ /** Provenance (manual | imported); audit-only, unused by the resolver. */
1139
+ readonly source: 'manual' | 'imported';
1140
+ }
1141
+ /**
1142
+ * A model's pricing document. The viewer's fetcher merges it into the model
1143
+ * metadata and the server forwards it to the client via {@link PricingMessage}.
1144
+ */
1145
+ interface PricingDocument {
1146
+ /** Optimistic-concurrency version from the editor; unused by the viewer. */
1147
+ readonly version: number;
1148
+ /** Customer-authored CTA label; null when the adapter's defaultLabel fills in. */
1149
+ readonly buttonLabel: string | null;
1150
+ /** ISO 4217 currency code for price formatting. */
1151
+ readonly currency: string;
1152
+ /** VAT label for bare embeds, e.g. "incl. btw". */
1153
+ readonly vatDisplay: string;
1154
+ readonly rules: PricingRule[];
1155
+ }
1156
+
1157
+ export { Annotations, Attribute, AttributeValue, Camera, Configuration, ERROR_CODES, Focus, Photo, Snapshot, VD_EVENTS, VirtualdisplayClient, VirtualdisplayError, enableDebugMode, isDebugMode, logger };
1158
+ export type { Availability, ClientOptions, ErrorCode, FocusState, MutationFailedEvent, PhotoData, PricingDocument, PricingRule, ResolvedConfiguration, SubmitOptions, SubmittedConfiguration, VdEvent, ViewerSettings, VirtualdisplaySelectEvent, VirtualdisplaySelectedEvent, VirtualdisplayUiEventMap, VirtualdisplayUiEventType };