@glowhop/core-tour 1.0.0

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.
@@ -0,0 +1,999 @@
1
+ // packages/core/src/definition/step-props.ts
2
+ function cloneStepProps(props) {
3
+ return {
4
+ title: props.title,
5
+ content: props.content,
6
+ data: props.data === undefined ? undefined : structuredClone(props.data),
7
+ overlay: props.overlay && {
8
+ ...props.overlay,
9
+ animation: props.overlay.animation && { ...props.overlay.animation }
10
+ },
11
+ popover: props.popover && {
12
+ ...props.popover,
13
+ animation: props.popover.animation && { ...props.popover.animation },
14
+ arrow: props.popover.arrow && { ...props.popover.arrow },
15
+ keyboardShortcuts: props.popover.keyboardShortcuts && {
16
+ previous: props.popover.keyboardShortcuts.previous && [
17
+ ...props.popover.keyboardShortcuts.previous
18
+ ],
19
+ advance: props.popover.keyboardShortcuts.advance && [
20
+ ...props.popover.keyboardShortcuts.advance
21
+ ],
22
+ cancel: props.popover.keyboardShortcuts.cancel && [
23
+ ...props.popover.keyboardShortcuts.cancel
24
+ ]
25
+ },
26
+ placementTryOrder: props.popover.placementTryOrder && [...props.popover.placementTryOrder]
27
+ },
28
+ indicator: props.indicator && {
29
+ ...props.indicator,
30
+ animation: props.indicator.animation && { ...props.indicator.animation },
31
+ placementTryOrder: props.indicator.placementTryOrder && [
32
+ ...props.indicator.placementTryOrder
33
+ ]
34
+ }
35
+ };
36
+ }
37
+ function freezeStepProps(props) {
38
+ const cloned = cloneStepProps(props);
39
+ if (cloned.data)
40
+ Object.freeze(cloned.data);
41
+ if (cloned.overlay?.animation)
42
+ Object.freeze(cloned.overlay.animation);
43
+ if (cloned.overlay)
44
+ Object.freeze(cloned.overlay);
45
+ if (cloned.popover?.animation)
46
+ Object.freeze(cloned.popover.animation);
47
+ if (cloned.popover?.arrow)
48
+ Object.freeze(cloned.popover.arrow);
49
+ if (cloned.popover?.keyboardShortcuts?.previous)
50
+ Object.freeze(cloned.popover.keyboardShortcuts.previous);
51
+ if (cloned.popover?.keyboardShortcuts?.advance)
52
+ Object.freeze(cloned.popover.keyboardShortcuts.advance);
53
+ if (cloned.popover?.keyboardShortcuts?.cancel)
54
+ Object.freeze(cloned.popover.keyboardShortcuts.cancel);
55
+ if (cloned.popover?.keyboardShortcuts)
56
+ Object.freeze(cloned.popover.keyboardShortcuts);
57
+ if (cloned.popover?.placementTryOrder)
58
+ Object.freeze(cloned.popover.placementTryOrder);
59
+ if (cloned.popover)
60
+ Object.freeze(cloned.popover);
61
+ if (cloned.indicator?.animation)
62
+ Object.freeze(cloned.indicator.animation);
63
+ if (cloned.indicator?.placementTryOrder)
64
+ Object.freeze(cloned.indicator.placementTryOrder);
65
+ if (cloned.indicator)
66
+ Object.freeze(cloned.indicator);
67
+ return Object.freeze(cloned);
68
+ }
69
+ // packages/core/src/definition/workflow-definition.ts
70
+ function freezeRecord(value) {
71
+ return Object.freeze(value);
72
+ }
73
+ function freezeAnimation(options) {
74
+ return options && freezeRecord({ ...options });
75
+ }
76
+ function freezeOverlay(options) {
77
+ return options && freezeRecord({
78
+ ...options,
79
+ animation: freezeAnimation(options.animation)
80
+ });
81
+ }
82
+ function freezePopover(options) {
83
+ return options && freezeRecord({
84
+ ...options,
85
+ animation: freezeAnimation(options.animation),
86
+ arrow: options.arrow && freezeRecord({ ...options.arrow }),
87
+ keyboardShortcuts: options.keyboardShortcuts && freezeRecord({
88
+ previous: options.keyboardShortcuts.previous && freezeRecord([...options.keyboardShortcuts.previous]),
89
+ advance: options.keyboardShortcuts.advance && freezeRecord([...options.keyboardShortcuts.advance]),
90
+ cancel: options.keyboardShortcuts.cancel && freezeRecord([...options.keyboardShortcuts.cancel])
91
+ }),
92
+ placementTryOrder: options.placementTryOrder && freezeRecord([...options.placementTryOrder])
93
+ });
94
+ }
95
+ function freezeIndicator(options) {
96
+ return options && freezeRecord({
97
+ ...options,
98
+ animation: freezeAnimation(options.animation),
99
+ placementTryOrder: options.placementTryOrder && freezeRecord([...options.placementTryOrder])
100
+ });
101
+ }
102
+ function freezeStep(draft) {
103
+ return freezeRecord({
104
+ id: draft.id,
105
+ target: draft.target,
106
+ resetPropsOnEnter: draft.resetPropsOnEnter,
107
+ props: freezeStepProps(draft.props),
108
+ behavior: draft.behavior && freezeRecord({
109
+ ...draft.behavior,
110
+ scroll: draft.behavior.scroll && freezeRecord({ ...draft.behavior.scroll })
111
+ }),
112
+ actions: freezeRecord(draft.actions.map((action) => action)),
113
+ eventHandlers: freezeRecord(draft.eventHandlers.map((handler) => freezeRecord({ ...handler }))),
114
+ advanceAction: draft.advanceAction,
115
+ previousAction: draft.previousAction,
116
+ cancelAction: draft.cancelAction
117
+ });
118
+ }
119
+ function freezeOptions(options) {
120
+ return freezeRecord({
121
+ ...options,
122
+ overlay: freezeOverlay(options.overlay),
123
+ popover: freezePopover(options.popover),
124
+ indicator: freezeIndicator(options.indicator),
125
+ behavior: options.behavior && freezeRecord({
126
+ ...options.behavior,
127
+ scroll: options.behavior.scroll && freezeRecord({ ...options.behavior.scroll })
128
+ })
129
+ });
130
+ }
131
+ function cloneWorkflowStepDraft(definition) {
132
+ return {
133
+ ...definition,
134
+ props: cloneStepProps(definition.props),
135
+ actions: definition.actions.map((action) => action),
136
+ eventHandlers: [...definition.eventHandlers]
137
+ };
138
+ }
139
+ function assertUniqueStepIds(name, drafts) {
140
+ const seen = new Map;
141
+ for (const [index, draft] of drafts.entries()) {
142
+ const label = `step ${index}${draft.props.title ? ` ("${String(draft.props.title)}")` : ""}`;
143
+ if (typeof draft.id !== "string" || draft.id.length === 0) {
144
+ throw new Error(`Workflow "${name}": ${label} is missing a non-empty "id".`);
145
+ }
146
+ const duplicate = seen.get(draft.id);
147
+ if (duplicate !== undefined) {
148
+ throw new Error(`Workflow "${name}": ${label} reuses the id "${draft.id}" already used by step ${duplicate}. Step ids must be unique.`);
149
+ }
150
+ seen.set(draft.id, index);
151
+ }
152
+ }
153
+ function createWorkflowDefinition(name, options, drafts) {
154
+ assertUniqueStepIds(name, drafts);
155
+ return freezeRecord({
156
+ name,
157
+ options: freezeOptions(options),
158
+ steps: freezeRecord(drafts.map(freezeStep))
159
+ });
160
+ }
161
+ // packages/core/src/runtime/abort.ts
162
+ function abortError() {
163
+ return new DOMException("The operation was aborted", "AbortError");
164
+ }
165
+ function abortableDelay(delay, signal) {
166
+ if (signal.aborted)
167
+ throw abortError();
168
+ return new Promise((resolve, reject) => {
169
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
170
+ const onAbort = () => {
171
+ clearTimeout(timeoutId);
172
+ cleanup();
173
+ reject(abortError());
174
+ };
175
+ const timeoutId = setTimeout(() => {
176
+ cleanup();
177
+ resolve();
178
+ }, delay);
179
+ signal.addEventListener("abort", onAbort, { once: true });
180
+ });
181
+ }
182
+
183
+ // packages/core/src/builder/index.ts
184
+ var DEFAULT_WAIT_INTERVAL = 16;
185
+ var DEFAULT_WAIT_TIMEOUT = 3000;
186
+ var INACTIVE_STEP_ERROR = "WorkflowStepBuilder is no longer active";
187
+ var STEP_BUILDER_INTERNAL = Symbol("WorkflowStepBuilder.internal");
188
+ function assertTimingValue(name, value) {
189
+ if (!Number.isFinite(value) || value < 0) {
190
+ throw new TypeError(`${name} must be a finite non-negative number`);
191
+ }
192
+ }
193
+ function throwIfAborted(signal) {
194
+ if (signal.aborted)
195
+ throw abortError();
196
+ }
197
+ function waitTimeoutError(timeout) {
198
+ return new Error(`waitUntil timed out after ${timeout}ms`);
199
+ }
200
+ function waitForPredicate(predicate, remainingTime, timeout, signal) {
201
+ throwIfAborted(signal);
202
+ return new Promise((resolve, reject) => {
203
+ let settled = false;
204
+ const cleanup = () => {
205
+ clearTimeout(timeoutId);
206
+ signal.removeEventListener("abort", onAbort);
207
+ };
208
+ const settle = (callback) => {
209
+ if (settled)
210
+ return;
211
+ settled = true;
212
+ cleanup();
213
+ callback();
214
+ };
215
+ const onAbort = () => settle(() => reject(abortError()));
216
+ const timeoutId = setTimeout(() => settle(() => reject(waitTimeoutError(timeout))), remainingTime);
217
+ signal.addEventListener("abort", onAbort, { once: true });
218
+ Promise.resolve().then(predicate).then((result) => settle(() => resolve(result)), (error) => settle(() => reject(error)));
219
+ });
220
+ }
221
+ function waitOptions(options = {}) {
222
+ const timeout = options.timeout ?? DEFAULT_WAIT_TIMEOUT;
223
+ const interval = options.interval ?? DEFAULT_WAIT_INTERVAL;
224
+ if (!Number.isFinite(timeout) || timeout < 0) {
225
+ throw new TypeError("wait timeout must be a finite non-negative number");
226
+ }
227
+ if (!Number.isFinite(interval) || interval <= 0) {
228
+ throw new TypeError("wait interval must be a finite positive number");
229
+ }
230
+ return { interval, timeout };
231
+ }
232
+
233
+ class WorkflowBuilder {
234
+ name;
235
+ options;
236
+ steps = [];
237
+ currentStep = null;
238
+ definition = null;
239
+ constructor(name, options = {}) {
240
+ this.name = name;
241
+ this.options = options;
242
+ }
243
+ step(options) {
244
+ this.assertBuilding();
245
+ this.commitCurrentStep();
246
+ this.currentStep = new WorkflowStepBuilder(this, {
247
+ id: options.id,
248
+ target: options.target,
249
+ resetPropsOnEnter: options.resetPropsOnEnter,
250
+ props: {
251
+ title: options.title,
252
+ content: options.content,
253
+ data: cloneStepProps(options).data,
254
+ overlay: options.overlay,
255
+ popover: options.popover,
256
+ indicator: options.indicator
257
+ },
258
+ behavior: options.behavior,
259
+ actions: [],
260
+ eventHandlers: [],
261
+ advanceAction: null,
262
+ previousAction: null,
263
+ cancelAction: null
264
+ });
265
+ return this.currentStep;
266
+ }
267
+ append(workflow) {
268
+ this.assertBuilding();
269
+ const drafts = workflow.steps.map(cloneWorkflowStepDraft);
270
+ const lastStep = drafts.at(-1);
271
+ if (!lastStep)
272
+ throw new Error("Cannot append a workflow without steps");
273
+ this.commitCurrentStep();
274
+ this.steps.push(...drafts.slice(0, -1));
275
+ this.currentStep = new WorkflowStepBuilder(this, lastStep);
276
+ return this.currentStep;
277
+ }
278
+ build() {
279
+ if (this.definition)
280
+ return this.definition;
281
+ this.commitCurrentStep();
282
+ this.definition = createWorkflowDefinition(this.name, this.options, this.steps);
283
+ return this.definition;
284
+ }
285
+ assertBuilding() {
286
+ if (this.definition)
287
+ throw new Error("WorkflowBuilder is already finished");
288
+ }
289
+ commitCurrentStep() {
290
+ if (!this.currentStep)
291
+ return;
292
+ this.steps.push(this.currentStep[STEP_BUILDER_INTERNAL]());
293
+ this.currentStep = null;
294
+ }
295
+ }
296
+
297
+ class WorkflowStepBuilder {
298
+ owner;
299
+ draft;
300
+ active = true;
301
+ constructor(owner, draft) {
302
+ this.owner = owner;
303
+ this.draft = draft;
304
+ }
305
+ step(options) {
306
+ this.assertActive();
307
+ return this.owner.step(options);
308
+ }
309
+ append(workflow) {
310
+ this.assertActive();
311
+ return this.owner.append(workflow);
312
+ }
313
+ build() {
314
+ this.assertActive();
315
+ return this.owner.build();
316
+ }
317
+ clickTarget() {
318
+ return this.do(({ target }) => {
319
+ target?.click();
320
+ return true;
321
+ });
322
+ }
323
+ focusTarget() {
324
+ return this.do(({ target }) => {
325
+ target?.focus();
326
+ return true;
327
+ });
328
+ }
329
+ wait(timeMs) {
330
+ this.assertActive();
331
+ assertTimingValue("timeMs", timeMs);
332
+ this.draft.actions.push(timeMs);
333
+ return this;
334
+ }
335
+ waitUntil(predicate, options = {}) {
336
+ this.assertActive();
337
+ const { interval, timeout } = waitOptions(options);
338
+ this.draft.actions.push(async (context) => {
339
+ const startedAt = Date.now();
340
+ let attempted = false;
341
+ while (true) {
342
+ throwIfAborted(context.signal);
343
+ const elapsed = Date.now() - startedAt;
344
+ if (attempted && elapsed >= timeout)
345
+ throw waitTimeoutError(timeout);
346
+ attempted = true;
347
+ if (await waitForPredicate(() => predicate(context), Math.max(0, timeout - elapsed), timeout, context.signal))
348
+ return true;
349
+ const remainingTime = timeout - (Date.now() - startedAt);
350
+ if (remainingTime <= 0)
351
+ throw waitTimeoutError(timeout);
352
+ await abortableDelay(Math.min(interval, remainingTime), context.signal);
353
+ }
354
+ });
355
+ return this;
356
+ }
357
+ waitUntilElement(selector, options) {
358
+ if (selector.length === 0)
359
+ throw new TypeError("selector must not be empty");
360
+ return this.waitUntil((context) => context.target.ownerDocument.querySelector(selector) !== null, options);
361
+ }
362
+ do(callback) {
363
+ this.assertActive();
364
+ this.draft.actions.push(callback);
365
+ return this;
366
+ }
367
+ beforeAdvance(callback) {
368
+ this.assertActive();
369
+ this.draft.advanceAction = callback;
370
+ return this;
371
+ }
372
+ beforePrevious(callback) {
373
+ this.assertActive();
374
+ this.draft.previousAction = callback;
375
+ return this;
376
+ }
377
+ beforeCancel(callback) {
378
+ this.assertActive();
379
+ this.draft.cancelAction = callback;
380
+ return this;
381
+ }
382
+ onTargetEvent(eventOrEvents, callback) {
383
+ this.assertActive();
384
+ const events = typeof eventOrEvents === "string" ? [eventOrEvents] : eventOrEvents;
385
+ if (events.length === 0)
386
+ throw new TypeError("events must not be empty");
387
+ for (const event of events) {
388
+ if (event.length === 0)
389
+ throw new TypeError("event name must not be empty");
390
+ this.draft.eventHandlers.push({
391
+ event,
392
+ callback
393
+ });
394
+ }
395
+ return this;
396
+ }
397
+ [STEP_BUILDER_INTERNAL]() {
398
+ this.active = false;
399
+ return cloneWorkflowStepDraft(this.draft);
400
+ }
401
+ assertActive() {
402
+ if (!this.active)
403
+ throw new Error(INACTIVE_STEP_ERROR);
404
+ }
405
+ }
406
+
407
+ // packages/core/src/config/types.ts
408
+ class ConfigValidationError extends Error {
409
+ issues;
410
+ constructor(issues) {
411
+ const summary = issues.map((issue) => ` - ${issue.path}: ${issue.message}`).join(`
412
+ `);
413
+ super(`Invalid workflow config (${issues.length} issue(s)):
414
+ ${summary}`);
415
+ this.name = "ConfigValidationError";
416
+ this.issues = issues;
417
+ }
418
+ }
419
+
420
+ // packages/core/src/config/validate.ts
421
+ var TOP_LEVEL_KEYS = [
422
+ "name",
423
+ "cancellable",
424
+ "allowScroll",
425
+ "overlay",
426
+ "popover",
427
+ "indicator",
428
+ "animated",
429
+ "behavior",
430
+ "onStart",
431
+ "onCancel",
432
+ "onFinish",
433
+ "steps"
434
+ ];
435
+ var STEP_KEYS = [
436
+ "id",
437
+ "target",
438
+ "resetPropsOnEnter",
439
+ "overlay",
440
+ "popover",
441
+ "indicator",
442
+ "behavior",
443
+ "title",
444
+ "content",
445
+ "data",
446
+ "actions",
447
+ "eventHandlers",
448
+ "advanceAction",
449
+ "previousAction",
450
+ "cancelAction"
451
+ ];
452
+ var EVENT_HANDLER_KEYS = ["event", "action"];
453
+ var ANIMATION_KEYS = ["duration", "easing"];
454
+ var OVERLAY_KEYS = ["animated", "animation", "color", "opacity", "padding", "radius"];
455
+ var INDICATOR_KEYS = ["animated", "animation", "disabled", "gap", "placementTryOrder"];
456
+ var POPOVER_KEYS = [
457
+ "animated",
458
+ "animation",
459
+ "placementTryOrder",
460
+ "arrow",
461
+ "hideFooter",
462
+ "disablePreviousButton",
463
+ "hidePreviousButton",
464
+ "disableAdvanceButton",
465
+ "hideAdvanceButton",
466
+ "gap",
467
+ "keyboardShortcuts"
468
+ ];
469
+ var POPOVER_ARROW_KEYS = [
470
+ "disabled",
471
+ "color",
472
+ "size",
473
+ "borderWidth",
474
+ "borderRadius",
475
+ "edgePadding",
476
+ "styleNonce",
477
+ "disableAutoStyles"
478
+ ];
479
+ var KEYBOARD_SHORTCUT_KEYS = ["previous", "advance", "cancel"];
480
+ var BEHAVIOR_KEYS = [
481
+ "allowInteraction",
482
+ "disableAutoFocus",
483
+ "disableAutoScroll",
484
+ "missingTargetStrategy",
485
+ "scroll",
486
+ "targetTimeout",
487
+ "overlayClick"
488
+ ];
489
+ var SCROLL_KEYS = ["behavior", "block", "inline"];
490
+ var BUILTIN_ACTION_KEYS = {
491
+ wait: ["type", "ms"],
492
+ waitUntilElement: ["type", "selector", "interval", "timeout"],
493
+ clickTarget: ["type"],
494
+ focusTarget: ["type"]
495
+ };
496
+ function isPlainObject(value) {
497
+ return typeof value === "object" && value !== null && !Array.isArray(value);
498
+ }
499
+ function defaultContentValidator(value) {
500
+ return typeof value === "string" ? null : "must be a string";
501
+ }
502
+ function validateWorkflowConfig(config, options = {}) {
503
+ const validateContent = options.validateContent ?? defaultContentValidator;
504
+ const issues = [];
505
+ validateWorkflowConfigShape(config, issues, validateContent);
506
+ if (issues.length > 0)
507
+ throw new ConfigValidationError(issues);
508
+ return config;
509
+ }
510
+ function validateWorkflowConfigShape(value, issues, validateContent) {
511
+ if (!isPlainObject(value)) {
512
+ issues.push({ path: "", message: "Workflow config must be a plain object" });
513
+ return;
514
+ }
515
+ assertNoUnknownKeys(value, TOP_LEVEL_KEYS, "", issues);
516
+ if (typeof value.name !== "string" || value.name.length === 0) {
517
+ issues.push({ path: "name", message: "name must be a non-empty string" });
518
+ }
519
+ validateOptionalBoolean("cancellable", value.cancellable, issues);
520
+ validateOptionalBoolean("allowScroll", value.allowScroll, issues);
521
+ validateOptionalBoolean("animated", value.animated, issues);
522
+ validateOverlayShape("overlay", value.overlay, issues);
523
+ validatePopoverShape("popover", value.popover, issues);
524
+ validateIndicatorShape("indicator", value.indicator, issues);
525
+ validateBehaviorShape("behavior", value.behavior, issues);
526
+ validateLifecycleActionRefShape("onStart", value.onStart, issues);
527
+ validateLifecycleActionRefShape("onCancel", value.onCancel, issues);
528
+ validateLifecycleActionRefShape("onFinish", value.onFinish, issues);
529
+ if (!Array.isArray(value.steps)) {
530
+ issues.push({ path: "steps", message: "steps must be an array" });
531
+ return;
532
+ }
533
+ const seenIds = new Map;
534
+ for (const [index, step] of value.steps.entries()) {
535
+ validateStepConfigShape(step, `steps[${index}]`, issues, validateContent);
536
+ if (isPlainObject(step) && typeof step.id === "string" && step.id.length > 0) {
537
+ const duplicate = seenIds.get(step.id);
538
+ if (duplicate === undefined)
539
+ seenIds.set(step.id, index);
540
+ else {
541
+ issues.push({
542
+ path: `steps[${index}].id`,
543
+ message: `id "${step.id}" is already used by steps[${duplicate}]. Step ids must be unique.`
544
+ });
545
+ }
546
+ }
547
+ }
548
+ }
549
+ function validateStepConfigShape(value, path, issues, validateContent) {
550
+ if (!isPlainObject(value)) {
551
+ issues.push({ path, message: "Step must be a plain object" });
552
+ return;
553
+ }
554
+ assertNoUnknownKeys(value, STEP_KEYS, path, issues);
555
+ if (typeof value.id !== "string" || value.id.length === 0) {
556
+ issues.push({
557
+ path: `${path}.id`,
558
+ message: "id must be a non-empty string, unique within the workflow"
559
+ });
560
+ }
561
+ if (typeof value.target !== "string" || value.target.length === 0) {
562
+ issues.push({
563
+ path: `${path}.target`,
564
+ message: "target must be a non-empty CSS selector string"
565
+ });
566
+ }
567
+ const titleError = validateContent(value.title, `${path}.title`);
568
+ if (titleError)
569
+ issues.push({ path: `${path}.title`, message: titleError });
570
+ const contentError = validateContent(value.content, `${path}.content`);
571
+ if (contentError)
572
+ issues.push({ path: `${path}.content`, message: contentError });
573
+ validateOptionalBoolean(`${path}.resetPropsOnEnter`, value.resetPropsOnEnter, issues);
574
+ validateOverlayShape(`${path}.overlay`, value.overlay, issues);
575
+ validatePopoverShape(`${path}.popover`, value.popover, issues);
576
+ validateIndicatorShape(`${path}.indicator`, value.indicator, issues);
577
+ validateBehaviorShape(`${path}.behavior`, value.behavior, issues);
578
+ validateDataShape(`${path}.data`, value.data, issues);
579
+ if (value.actions !== undefined) {
580
+ if (!Array.isArray(value.actions)) {
581
+ issues.push({ path: `${path}.actions`, message: "actions must be an array" });
582
+ } else {
583
+ for (const [index, action] of value.actions.entries()) {
584
+ validateStepActionRefShape(action, `${path}.actions[${index}]`, issues);
585
+ }
586
+ }
587
+ }
588
+ if (value.eventHandlers !== undefined) {
589
+ if (!Array.isArray(value.eventHandlers)) {
590
+ issues.push({ path: `${path}.eventHandlers`, message: "eventHandlers must be an array" });
591
+ } else {
592
+ for (const [index, handler] of value.eventHandlers.entries()) {
593
+ validateEventHandlerConfigShape(handler, `${path}.eventHandlers[${index}]`, issues);
594
+ }
595
+ }
596
+ }
597
+ validateTransitionActionRefShape(`${path}.advanceAction`, value.advanceAction, issues);
598
+ validateTransitionActionRefShape(`${path}.previousAction`, value.previousAction, issues);
599
+ validateTransitionActionRefShape(`${path}.cancelAction`, value.cancelAction, issues);
600
+ }
601
+ function validateEventHandlerConfigShape(value, path, issues) {
602
+ if (!isPlainObject(value)) {
603
+ issues.push({ path, message: "Event handler must be a plain object" });
604
+ return;
605
+ }
606
+ assertNoUnknownKeys(value, EVENT_HANDLER_KEYS, path, issues);
607
+ const event = value.event;
608
+ const eventIsValid = typeof event === "string" && event.length > 0 || Array.isArray(event) && event.length > 0 && event.every((entry) => typeof entry === "string" && entry.length > 0);
609
+ if (!eventIsValid) {
610
+ issues.push({
611
+ path: `${path}.event`,
612
+ message: "event must be a non-empty string or a non-empty array of non-empty strings"
613
+ });
614
+ }
615
+ validateStepActionRefShape(value.action, `${path}.action`, issues);
616
+ }
617
+ function validateStepActionRefShape(value, path, issues) {
618
+ if (typeof value === "function")
619
+ return;
620
+ if (isPlainObject(value) && typeof value.type === "string") {
621
+ validateBuiltinActionShape(value, path, issues);
622
+ return;
623
+ }
624
+ issues.push({
625
+ path,
626
+ message: 'must be a function, or a built-in action object with a known "type"'
627
+ });
628
+ }
629
+ function validateTransitionActionRefShape(path, value, issues) {
630
+ if (value === undefined)
631
+ return;
632
+ if (typeof value === "function")
633
+ return;
634
+ issues.push({
635
+ path,
636
+ message: "must be a function; built-in actions are not supported in this slot"
637
+ });
638
+ }
639
+ function validateLifecycleActionRefShape(path, value, issues) {
640
+ validateTransitionActionRefShape(path, value, issues);
641
+ }
642
+ function validateBuiltinActionShape(value, path, issues) {
643
+ const type = value.type;
644
+ const allowedKeys = BUILTIN_ACTION_KEYS[type];
645
+ if (!allowedKeys) {
646
+ issues.push({ path: `${path}.type`, message: `Unknown built-in action type: ${type}` });
647
+ return;
648
+ }
649
+ assertNoUnknownKeys(value, allowedKeys, path, issues);
650
+ switch (type) {
651
+ case "wait":
652
+ validateFiniteNonNegative(`${path}.ms`, value.ms, issues);
653
+ break;
654
+ case "waitUntilElement":
655
+ if (typeof value.selector !== "string" || value.selector.length === 0) {
656
+ issues.push({ path: `${path}.selector`, message: "selector must be a non-empty string" });
657
+ }
658
+ validateOptionalFiniteNonNegative(`${path}.interval`, value.interval, issues);
659
+ validateOptionalFiniteNonNegative(`${path}.timeout`, value.timeout, issues);
660
+ break;
661
+ case "clickTarget":
662
+ case "focusTarget":
663
+ break;
664
+ }
665
+ }
666
+ function validateFiniteNonNegative(path, value, issues) {
667
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
668
+ issues.push({ path, message: "must be a finite non-negative number" });
669
+ }
670
+ }
671
+ function validateOptionalFiniteNonNegative(path, value, issues) {
672
+ if (value === undefined)
673
+ return;
674
+ validateFiniteNonNegative(path, value, issues);
675
+ }
676
+ function validateOptionalBoolean(path, value, issues) {
677
+ if (value !== undefined && typeof value !== "boolean") {
678
+ issues.push({ path, message: "must be a boolean" });
679
+ }
680
+ }
681
+ function validateOptionalString(path, value, issues) {
682
+ if (value !== undefined && typeof value !== "string") {
683
+ issues.push({ path, message: "must be a string" });
684
+ }
685
+ }
686
+ function validateOptionalEnum(path, value, allowed, issues) {
687
+ if (value !== undefined && (typeof value !== "string" || !allowed.includes(value))) {
688
+ issues.push({ path, message: `must be one of: ${allowed.join(", ")}` });
689
+ }
690
+ }
691
+ function validateOptionalStringArray(path, value, issues, allowed) {
692
+ if (value === undefined)
693
+ return;
694
+ if (!Array.isArray(value)) {
695
+ issues.push({ path, message: "must be an array" });
696
+ return;
697
+ }
698
+ for (const [index, entry] of value.entries()) {
699
+ if (typeof entry !== "string" || allowed !== undefined && !allowed.includes(entry)) {
700
+ issues.push({
701
+ path: `${path}[${index}]`,
702
+ message: allowed ? `must be one of: ${allowed.join(", ")}` : "must be a string"
703
+ });
704
+ }
705
+ }
706
+ }
707
+ function validateAnimationShape(path, value, issues) {
708
+ if (value === undefined)
709
+ return;
710
+ if (!isPlainObject(value)) {
711
+ issues.push({ path, message: "must be an object" });
712
+ return;
713
+ }
714
+ assertNoUnknownKeys(value, ANIMATION_KEYS, path, issues);
715
+ validateFiniteNonNegative(`${path}.duration`, value.duration, issues);
716
+ if (typeof value.easing !== "string") {
717
+ issues.push({ path: `${path}.easing`, message: "must be a string" });
718
+ }
719
+ }
720
+ function validateBaseOptionsShape(path, value, issues) {
721
+ validateOptionalBoolean(`${path}.animated`, value.animated, issues);
722
+ validateAnimationShape(`${path}.animation`, value.animation, issues);
723
+ }
724
+ function validateOverlayShape(path, value, issues) {
725
+ if (value === undefined)
726
+ return;
727
+ if (!isPlainObject(value)) {
728
+ issues.push({ path, message: "must be an object" });
729
+ return;
730
+ }
731
+ assertNoUnknownKeys(value, OVERLAY_KEYS, path, issues);
732
+ validateBaseOptionsShape(path, value, issues);
733
+ validateOptionalString(`${path}.color`, value.color, issues);
734
+ if (value.opacity !== undefined && (typeof value.opacity !== "number" || !Number.isFinite(value.opacity) || value.opacity < 0 || value.opacity > 1)) {
735
+ issues.push({ path: `${path}.opacity`, message: "must be a finite number between 0 and 1" });
736
+ }
737
+ validateOptionalFiniteNonNegative(`${path}.padding`, value.padding, issues);
738
+ validateOptionalFiniteNonNegative(`${path}.radius`, value.radius, issues);
739
+ }
740
+ function validateIndicatorShape(path, value, issues) {
741
+ if (value === undefined)
742
+ return;
743
+ if (!isPlainObject(value)) {
744
+ issues.push({ path, message: "must be an object" });
745
+ return;
746
+ }
747
+ assertNoUnknownKeys(value, INDICATOR_KEYS, path, issues);
748
+ validateBaseOptionsShape(path, value, issues);
749
+ validateOptionalBoolean(`${path}.disabled`, value.disabled, issues);
750
+ validateOptionalFiniteNonNegative(`${path}.gap`, value.gap, issues);
751
+ validateOptionalStringArray(`${path}.placementTryOrder`, value.placementTryOrder, issues, [
752
+ "top",
753
+ "bottom",
754
+ "left",
755
+ "right"
756
+ ]);
757
+ }
758
+ function validatePopoverArrowShape(path, value, issues) {
759
+ if (value === undefined)
760
+ return;
761
+ if (!isPlainObject(value)) {
762
+ issues.push({ path, message: "must be an object" });
763
+ return;
764
+ }
765
+ assertNoUnknownKeys(value, POPOVER_ARROW_KEYS, path, issues);
766
+ validateOptionalBoolean(`${path}.disabled`, value.disabled, issues);
767
+ validateOptionalString(`${path}.color`, value.color, issues);
768
+ validateOptionalFiniteNonNegative(`${path}.size`, value.size, issues);
769
+ validateOptionalFiniteNonNegative(`${path}.borderWidth`, value.borderWidth, issues);
770
+ validateOptionalFiniteNonNegative(`${path}.borderRadius`, value.borderRadius, issues);
771
+ validateOptionalFiniteNonNegative(`${path}.edgePadding`, value.edgePadding, issues);
772
+ validateOptionalString(`${path}.styleNonce`, value.styleNonce, issues);
773
+ validateOptionalBoolean(`${path}.disableAutoStyles`, value.disableAutoStyles, issues);
774
+ }
775
+ function validateKeyboardShortcutsShape(path, value, issues) {
776
+ if (value === undefined)
777
+ return;
778
+ if (!isPlainObject(value)) {
779
+ issues.push({ path, message: "must be an object" });
780
+ return;
781
+ }
782
+ assertNoUnknownKeys(value, KEYBOARD_SHORTCUT_KEYS, path, issues);
783
+ for (const key of KEYBOARD_SHORTCUT_KEYS) {
784
+ validateOptionalStringArray(`${path}.${key}`, value[key], issues);
785
+ }
786
+ }
787
+ function validatePopoverShape(path, value, issues) {
788
+ if (value === undefined)
789
+ return;
790
+ if (!isPlainObject(value)) {
791
+ issues.push({ path, message: "must be an object" });
792
+ return;
793
+ }
794
+ assertNoUnknownKeys(value, POPOVER_KEYS, path, issues);
795
+ validateBaseOptionsShape(path, value, issues);
796
+ validateOptionalStringArray(`${path}.placementTryOrder`, value.placementTryOrder, issues, [
797
+ "top",
798
+ "bottom",
799
+ "left",
800
+ "right"
801
+ ]);
802
+ validatePopoverArrowShape(`${path}.arrow`, value.arrow, issues);
803
+ validateOptionalBoolean(`${path}.hideFooter`, value.hideFooter, issues);
804
+ validateOptionalBoolean(`${path}.disablePreviousButton`, value.disablePreviousButton, issues);
805
+ validateOptionalBoolean(`${path}.hidePreviousButton`, value.hidePreviousButton, issues);
806
+ validateOptionalBoolean(`${path}.disableAdvanceButton`, value.disableAdvanceButton, issues);
807
+ validateOptionalBoolean(`${path}.hideAdvanceButton`, value.hideAdvanceButton, issues);
808
+ validateOptionalFiniteNonNegative(`${path}.gap`, value.gap, issues);
809
+ validateKeyboardShortcutsShape(`${path}.keyboardShortcuts`, value.keyboardShortcuts, issues);
810
+ }
811
+ function validateScrollShape(path, value, issues) {
812
+ if (value === undefined)
813
+ return;
814
+ if (!isPlainObject(value)) {
815
+ issues.push({ path, message: "must be an object" });
816
+ return;
817
+ }
818
+ assertNoUnknownKeys(value, SCROLL_KEYS, path, issues);
819
+ validateOptionalEnum(`${path}.behavior`, value.behavior, ["auto", "smooth"], issues);
820
+ validateOptionalEnum(`${path}.block`, value.block, ["start", "center", "end", "nearest"], issues);
821
+ validateOptionalEnum(`${path}.inline`, value.inline, ["start", "center", "end", "nearest"], issues);
822
+ }
823
+ function validateBehaviorShape(path, value, issues) {
824
+ if (value === undefined)
825
+ return;
826
+ if (!isPlainObject(value)) {
827
+ issues.push({ path, message: "must be an object" });
828
+ return;
829
+ }
830
+ assertNoUnknownKeys(value, BEHAVIOR_KEYS, path, issues);
831
+ validateOptionalBoolean(`${path}.allowInteraction`, value.allowInteraction, issues);
832
+ validateOptionalBoolean(`${path}.disableAutoFocus`, value.disableAutoFocus, issues);
833
+ validateOptionalBoolean(`${path}.disableAutoScroll`, value.disableAutoScroll, issues);
834
+ validateOptionalEnum(`${path}.missingTargetStrategy`, value.missingTargetStrategy, ["wait", "skip", "error"], issues);
835
+ validateScrollShape(`${path}.scroll`, value.scroll, issues);
836
+ validateOptionalFiniteNonNegative(`${path}.targetTimeout`, value.targetTimeout, issues);
837
+ validateOptionalEnum(`${path}.overlayClick`, value.overlayClick, ["none", "advance", "cancel"], issues);
838
+ }
839
+ function validateDataShape(path, value, issues) {
840
+ if (value === undefined)
841
+ return;
842
+ if (!isPlainObject(value)) {
843
+ issues.push({ path, message: "data must be an object" });
844
+ return;
845
+ }
846
+ for (const [key, entry] of Object.entries(value)) {
847
+ const isPrimitive = entry === null || typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean";
848
+ if (!isPrimitive) {
849
+ issues.push({
850
+ path: `${path}.${key}`,
851
+ message: "data values must be a string, number, boolean, or null"
852
+ });
853
+ }
854
+ }
855
+ }
856
+ function assertNoUnknownKeys(value, allowedKeys, path, issues) {
857
+ for (const key of Object.keys(value)) {
858
+ if (!allowedKeys.includes(key)) {
859
+ issues.push({ path: path ? `${path}.${key}` : key, message: `Unknown key: ${key}` });
860
+ }
861
+ }
862
+ }
863
+
864
+ // packages/core/src/config/from-config.ts
865
+ function createWorkflowFromConfig(config, options = {}) {
866
+ const validated = validateWorkflowConfig(config, options);
867
+ const builder = new WorkflowBuilder(validated.name, {
868
+ cancellable: validated.cancellable,
869
+ allowScroll: validated.allowScroll,
870
+ overlay: validated.overlay,
871
+ popover: validated.popover,
872
+ indicator: validated.indicator,
873
+ animated: validated.animated,
874
+ behavior: validated.behavior,
875
+ onStart: validated.onStart,
876
+ onCancel: validated.onCancel,
877
+ onFinish: validated.onFinish
878
+ });
879
+ for (const [index, stepConfig] of validated.steps.entries()) {
880
+ applyStepConfig(builder, stepConfig, `steps[${index}]`);
881
+ }
882
+ const definition = builder.build();
883
+ return Object.freeze({ ...definition, source: deepFreezeClone(validated) });
884
+ }
885
+ function applyStepConfig(builder, stepConfig, path) {
886
+ const step = builder.step({
887
+ id: stepConfig.id,
888
+ target: stepConfig.target,
889
+ resetPropsOnEnter: stepConfig.resetPropsOnEnter,
890
+ overlay: stepConfig.overlay,
891
+ popover: stepConfig.popover,
892
+ indicator: stepConfig.indicator,
893
+ behavior: stepConfig.behavior,
894
+ title: stepConfig.title,
895
+ content: stepConfig.content,
896
+ data: stepConfig.data
897
+ });
898
+ for (const [index, actionRef] of (stepConfig.actions ?? []).entries()) {
899
+ applyStepActionRef(step, actionRef, `${path}.actions[${index}]`);
900
+ }
901
+ for (const [index, handler] of (stepConfig.eventHandlers ?? []).entries()) {
902
+ applyEventHandler(step, handler, `${path}.eventHandlers[${index}]`);
903
+ }
904
+ if (stepConfig.advanceAction)
905
+ step.beforeAdvance(stepConfig.advanceAction);
906
+ if (stepConfig.previousAction)
907
+ step.beforePrevious(stepConfig.previousAction);
908
+ if (stepConfig.cancelAction)
909
+ step.beforeCancel(stepConfig.cancelAction);
910
+ }
911
+ function applyStepActionRef(step, ref, path) {
912
+ if (typeof ref === "function") {
913
+ step.do(ref);
914
+ return;
915
+ }
916
+ applyBuiltinAction(step, ref, path);
917
+ }
918
+ function applyBuiltinAction(step, builtin, path) {
919
+ switch (builtin.type) {
920
+ case "wait":
921
+ step.wait(builtin.ms);
922
+ return;
923
+ case "waitUntilElement":
924
+ step.waitUntilElement(builtin.selector, {
925
+ interval: builtin.interval,
926
+ timeout: builtin.timeout
927
+ });
928
+ return;
929
+ case "clickTarget":
930
+ step.clickTarget();
931
+ return;
932
+ case "focusTarget":
933
+ step.focusTarget();
934
+ return;
935
+ default:
936
+ throw new Error(`Unknown built-in action type at ${path}`);
937
+ }
938
+ }
939
+ function applyEventHandler(step, handler, path) {
940
+ const action = resolveEventHandlerAction(handler.action, `${path}.action`);
941
+ const events = typeof handler.event === "string" ? [handler.event] : handler.event;
942
+ for (const event of events) {
943
+ step.onTargetEvent(event, async (_event, context) => {
944
+ await action(context);
945
+ });
946
+ }
947
+ }
948
+ function resolveEventHandlerAction(ref, path) {
949
+ if (typeof ref === "function")
950
+ return ref;
951
+ const scratch = new WorkflowBuilder("__config_event_handler_scratch__");
952
+ const scratchStep = scratch.step({ id: "scratch", target: "*", title: "", content: "" });
953
+ applyBuiltinAction(scratchStep, ref, path);
954
+ const [instruction] = scratch.build().steps[0]?.actions ?? [];
955
+ if (instruction === undefined) {
956
+ throw new Error(`Failed to resolve built-in action at ${path}`);
957
+ }
958
+ if (typeof instruction === "number") {
959
+ const delayMs = instruction;
960
+ return async (context) => {
961
+ await abortableDelay(delayMs, context.signal);
962
+ return true;
963
+ };
964
+ }
965
+ return instruction;
966
+ }
967
+ function deepFreezeClone(value, clones = new WeakMap, preserveReference = false) {
968
+ if (preserveReference)
969
+ return value;
970
+ if (Array.isArray(value)) {
971
+ const existing2 = clones.get(value);
972
+ if (existing2)
973
+ return existing2;
974
+ const clone2 = [];
975
+ clones.set(value, clone2);
976
+ for (const entry of value)
977
+ clone2.push(deepFreezeClone(entry, clones));
978
+ return Object.freeze(clone2);
979
+ }
980
+ if (value === null || typeof value !== "object")
981
+ return value;
982
+ const prototype = Object.getPrototypeOf(value);
983
+ if (prototype !== null && prototype !== Object.prototype)
984
+ return value;
985
+ const existing = clones.get(value);
986
+ if (existing)
987
+ return existing;
988
+ const clone = Object.create(prototype);
989
+ clones.set(value, clone);
990
+ for (const [key, entry] of Object.entries(value)) {
991
+ clone[key] = deepFreezeClone(entry, clones, key === "title" || key === "content");
992
+ }
993
+ return Object.freeze(clone);
994
+ }
995
+ export {
996
+ validateWorkflowConfig,
997
+ createWorkflowFromConfig,
998
+ ConfigValidationError
999
+ };