@urbicon-ui/blocks 6.7.2 → 6.8.1
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/components/Guide/GuideProvider.svelte +4 -3
- package/dist/components/Guide/index.d.ts +8 -1
- package/dist/i18n/index.d.ts +2 -342
- package/dist/utils/guide.svelte.d.ts +55 -0
- package/dist/utils/guide.svelte.js +272 -8
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/package.json +3 -3
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
import type { GuideProviderProps } from './index';
|
|
4
4
|
import { setGuideContext } from './guide.context';
|
|
5
5
|
|
|
6
|
-
let { storage, controller, children }: GuideProviderProps = $props();
|
|
6
|
+
let { storage, navigate, controller, children }: GuideProviderProps = $props();
|
|
7
7
|
|
|
8
8
|
// Use a consumer-supplied controller (for programmatic access from outside the provider)
|
|
9
9
|
// or create one. Built once from the initial props — a stable controller/adapter is
|
|
10
10
|
// expected, so later prop changes are intentionally ignored. SSR-safe: the default
|
|
11
|
-
// adapter only touches localStorage inside load/save.
|
|
11
|
+
// adapter only touches localStorage inside load/save. `storage`/`navigate` are ignored
|
|
12
|
+
// when a `controller` is supplied (it already carries them).
|
|
12
13
|
// svelte-ignore state_referenced_locally
|
|
13
|
-
const instance = controller ?? new GuideController({ storage });
|
|
14
|
+
const instance = controller ?? new GuideController({ storage, navigate });
|
|
14
15
|
setGuideContext(instance);
|
|
15
16
|
</script>
|
|
16
17
|
|
|
@@ -30,10 +30,17 @@ import type { GuideArticleSlots, GuideBeaconSlots, GuideBeaconVariants, GuideHin
|
|
|
30
30
|
export interface GuideProviderProps {
|
|
31
31
|
/** Persistence adapter for "seen" state (tours/hints). @default localStorage-backed adapter */
|
|
32
32
|
storage?: GuideStorageAdapter;
|
|
33
|
+
/**
|
|
34
|
+
* Navigation hook for declarative cross-route tours, forwarded to the internally created
|
|
35
|
+
* controller. A SvelteKit consumer wires `(route) => goto(route)`; a step's `route` then
|
|
36
|
+
* navigates before its spotlight. Ignored when a `controller` is supplied (set it there).
|
|
37
|
+
* @default undefined
|
|
38
|
+
*/
|
|
39
|
+
navigate?: (route: string) => void | Promise<void>;
|
|
33
40
|
/**
|
|
34
41
|
* Supply a pre-created `GuideController` for programmatic access from outside the provider
|
|
35
42
|
* (start tours, open the panel, query `hasSeen`). When omitted, the provider creates one
|
|
36
|
-
* internally and shares it via context. When supplied, `storage`
|
|
43
|
+
* internally and shares it via context. When supplied, `storage` and `navigate` are ignored.
|
|
37
44
|
*/
|
|
38
45
|
controller?: GuideController;
|
|
39
46
|
/** App subtree wired to the Guide context. */
|
package/dist/i18n/index.d.ts
CHANGED
|
@@ -343,177 +343,7 @@ declare const blocksTranslations: {
|
|
|
343
343
|
};
|
|
344
344
|
};
|
|
345
345
|
};
|
|
346
|
-
export declare const blocksI18n: import("@urbicon-ui/i18n").PackageI18n<
|
|
347
|
-
readonly accessibility: {
|
|
348
|
-
readonly avatar: "Avatar";
|
|
349
|
-
readonly clearInput: "Clear input";
|
|
350
|
-
readonly clearSearch: "Clear search";
|
|
351
|
-
readonly clearSelection: "Clear selection";
|
|
352
|
-
readonly closeDialog: "Close dialog";
|
|
353
|
-
readonly closeDrawer: "Close drawer";
|
|
354
|
-
readonly dismiss: "Dismiss";
|
|
355
|
-
readonly fileUpload: "File upload";
|
|
356
|
-
readonly loading: "Loading";
|
|
357
|
-
readonly maximum: "Maximum";
|
|
358
|
-
readonly minimum: "Minimum";
|
|
359
|
-
readonly pagination: "Page navigation";
|
|
360
|
-
readonly progress: "Progress";
|
|
361
|
-
readonly slider: "Slider";
|
|
362
|
-
readonly toggle: "Toggle";
|
|
363
|
-
readonly toggleOptions: "Toggle options";
|
|
364
|
-
readonly removableBadge: "Removable badge";
|
|
365
|
-
readonly removeBadge: "Remove badge";
|
|
366
|
-
readonly removeFile: "Remove {{name}}";
|
|
367
|
-
};
|
|
368
|
-
readonly button: {
|
|
369
|
-
readonly close: "Close";
|
|
370
|
-
readonly apply: "Apply";
|
|
371
|
-
readonly cancel: "Cancel";
|
|
372
|
-
readonly confirm: "Confirm";
|
|
373
|
-
};
|
|
374
|
-
readonly menu: {
|
|
375
|
-
readonly placeholder: "Select an option...";
|
|
376
|
-
};
|
|
377
|
-
readonly common: {
|
|
378
|
-
readonly loading: "Loading...";
|
|
379
|
-
};
|
|
380
|
-
readonly languages: {
|
|
381
|
-
readonly de: "Deutsch";
|
|
382
|
-
readonly en: "English";
|
|
383
|
-
readonly es: "Español";
|
|
384
|
-
readonly fr: "Français";
|
|
385
|
-
readonly it: "Italiano";
|
|
386
|
-
readonly nl: "Nederlands";
|
|
387
|
-
};
|
|
388
|
-
readonly localeSwitcher: {
|
|
389
|
-
readonly ariaLabel: "Language selection";
|
|
390
|
-
readonly placeholder: "Select language...";
|
|
391
|
-
};
|
|
392
|
-
readonly pagination: {
|
|
393
|
-
readonly next: "Next";
|
|
394
|
-
readonly first: "First";
|
|
395
|
-
readonly last: "Last";
|
|
396
|
-
readonly page: "Page";
|
|
397
|
-
readonly previous: "Previous";
|
|
398
|
-
};
|
|
399
|
-
readonly calendar: {
|
|
400
|
-
readonly recurring: "Recurring event";
|
|
401
|
-
readonly previousMonth: "Previous month";
|
|
402
|
-
readonly nextMonth: "Next month";
|
|
403
|
-
readonly previousWeek: "Previous week";
|
|
404
|
-
readonly nextWeek: "Next week";
|
|
405
|
-
readonly previousDay: "Previous day";
|
|
406
|
-
readonly nextDay: "Next day";
|
|
407
|
-
readonly previousYear: "Previous year";
|
|
408
|
-
readonly nextYear: "Next year";
|
|
409
|
-
readonly today: "Today";
|
|
410
|
-
readonly calendarGrid: "Calendar";
|
|
411
|
-
readonly weekdays: "Weekdays";
|
|
412
|
-
readonly weekNumber: "Week number";
|
|
413
|
-
readonly events: "Events";
|
|
414
|
-
readonly noEvents: "No events";
|
|
415
|
-
readonly showMore: "{{count}} more";
|
|
416
|
-
readonly legend: "Legend";
|
|
417
|
-
readonly yearView: "Year overview";
|
|
418
|
-
readonly weekView: "Week view";
|
|
419
|
-
readonly dayView: "Day view";
|
|
420
|
-
readonly monthView: "Month view";
|
|
421
|
-
readonly viewMonth: "Month";
|
|
422
|
-
readonly viewYear: "Year";
|
|
423
|
-
readonly viewWeek: "Week";
|
|
424
|
-
readonly viewDay: "Day";
|
|
425
|
-
readonly multiDayLabel: "Day {{current}} of {{total}}";
|
|
426
|
-
readonly agendaView: "Agenda view";
|
|
427
|
-
readonly viewAgenda: "Agenda";
|
|
428
|
-
readonly viewSwitcher: "View mode";
|
|
429
|
-
};
|
|
430
|
-
readonly planner: {
|
|
431
|
-
readonly previousWeek: "Previous week";
|
|
432
|
-
readonly nextWeek: "Next week";
|
|
433
|
-
readonly previousMonth: "Previous month";
|
|
434
|
-
readonly nextMonth: "Next month";
|
|
435
|
-
readonly previousRange: "Previous range";
|
|
436
|
-
readonly nextRange: "Next range";
|
|
437
|
-
readonly today: "Today";
|
|
438
|
-
readonly grid: "Planner";
|
|
439
|
-
readonly weekNumber: "Week number";
|
|
440
|
-
readonly itemCount: "{{count}} items";
|
|
441
|
-
};
|
|
442
|
-
readonly commandPalette: {
|
|
443
|
-
readonly noResults: "No results found.";
|
|
444
|
-
readonly search: "Search...";
|
|
445
|
-
readonly hints: {
|
|
446
|
-
readonly navigate: "Navigate";
|
|
447
|
-
readonly select: "Select";
|
|
448
|
-
readonly close: "Close";
|
|
449
|
-
};
|
|
450
|
-
};
|
|
451
|
-
readonly compositionBar: {
|
|
452
|
-
readonly total: "Total";
|
|
453
|
-
readonly summary: "Composition: {{total}} ({{count}} shares) — {{parts}}";
|
|
454
|
-
readonly share: "Share";
|
|
455
|
-
readonly value: "Value";
|
|
456
|
-
readonly percent: "Percent";
|
|
457
|
-
readonly remaining: "remaining";
|
|
458
|
-
};
|
|
459
|
-
readonly guide: {
|
|
460
|
-
readonly next: "Next";
|
|
461
|
-
readonly previous: "Back";
|
|
462
|
-
readonly skip: "Skip tour";
|
|
463
|
-
readonly done: "Done";
|
|
464
|
-
readonly step: "Step {{current}} of {{total}}";
|
|
465
|
-
readonly tour: "Guided tour";
|
|
466
|
-
readonly close: "Close";
|
|
467
|
-
readonly openHelp: "Help";
|
|
468
|
-
readonly backToList: "All topics";
|
|
469
|
-
readonly info: "More information";
|
|
470
|
-
readonly infoAbout: "More information about {{label}}";
|
|
471
|
-
readonly dismiss: "Dismiss hint";
|
|
472
|
-
readonly startTour: "Start the guided tour";
|
|
473
|
-
readonly actionRequired: "Complete the highlighted action to continue";
|
|
474
|
-
readonly filterPlaceholder: "Filter topics…";
|
|
475
|
-
readonly noResults: "No matching topics";
|
|
476
|
-
};
|
|
477
|
-
readonly datepicker: {
|
|
478
|
-
readonly placeholder: "Select a date...";
|
|
479
|
-
readonly rangePlaceholder: "Select a date range...";
|
|
480
|
-
readonly clear: "Clear selection";
|
|
481
|
-
readonly openCalendar: "Open calendar";
|
|
482
|
-
readonly invalidDate: "Invalid date";
|
|
483
|
-
readonly outOfRange: "Date is outside the allowed range";
|
|
484
|
-
readonly invalidRange: "Invalid date range";
|
|
485
|
-
};
|
|
486
|
-
readonly fileUpload: {
|
|
487
|
-
readonly exists: "File already added";
|
|
488
|
-
readonly invalidType: "File type {{type}} is not allowed";
|
|
489
|
-
readonly tooLarge: "File exceeds {{size}} limit";
|
|
490
|
-
readonly tooMany: "Maximum {{count}} files allowed";
|
|
491
|
-
readonly tooSmall: "File must be at least {{size}}";
|
|
492
|
-
};
|
|
493
|
-
readonly sankey: {
|
|
494
|
-
readonly summary: "Sankey diagram: {{nodes}} nodes, {{links}} links — {{flows}}";
|
|
495
|
-
readonly source: "Source";
|
|
496
|
-
readonly target: "Target";
|
|
497
|
-
readonly value: "Value";
|
|
498
|
-
};
|
|
499
|
-
readonly slider: {
|
|
500
|
-
readonly rangeStatus: {
|
|
501
|
-
readonly insideRecommended: "In recommended range";
|
|
502
|
-
readonly insideValid: "In valid range";
|
|
503
|
-
readonly insideValidOnly: "Outside recommended range, but valid";
|
|
504
|
-
readonly outsideValid: "Outside valid range";
|
|
505
|
-
};
|
|
506
|
-
};
|
|
507
|
-
readonly themeSwitcher: {
|
|
508
|
-
readonly lightMode: "Light mode";
|
|
509
|
-
readonly darkMode: "Dark mode";
|
|
510
|
-
readonly systemTheme: "System theme";
|
|
511
|
-
};
|
|
512
|
-
readonly time: {
|
|
513
|
-
readonly ago: "{{value}} {{unit}} ago";
|
|
514
|
-
readonly now: "now";
|
|
515
|
-
};
|
|
516
|
-
}>;
|
|
346
|
+
export declare const blocksI18n: import("@urbicon-ui/i18n").PackageI18n<import("@urbicon-ui/i18n").Translations>;
|
|
517
347
|
/**
|
|
518
348
|
* Context-scoped translation hook for the blocks package. Call during component
|
|
519
349
|
* initialisation, then use the returned `t` (conventionally aliased `bt`):
|
|
@@ -526,177 +356,7 @@ export declare const blocksI18n: import("@urbicon-ui/i18n").PackageI18n<{
|
|
|
526
356
|
* Resolves against the nearest `<I18nProvider>`'s locale (or the base locale
|
|
527
357
|
* when none is mounted) and re-renders reactively on locale change.
|
|
528
358
|
*/
|
|
529
|
-
export declare const useBlocksI18n:
|
|
530
|
-
readonly accessibility: {
|
|
531
|
-
readonly avatar: "Avatar";
|
|
532
|
-
readonly clearInput: "Clear input";
|
|
533
|
-
readonly clearSearch: "Clear search";
|
|
534
|
-
readonly clearSelection: "Clear selection";
|
|
535
|
-
readonly closeDialog: "Close dialog";
|
|
536
|
-
readonly closeDrawer: "Close drawer";
|
|
537
|
-
readonly dismiss: "Dismiss";
|
|
538
|
-
readonly fileUpload: "File upload";
|
|
539
|
-
readonly loading: "Loading";
|
|
540
|
-
readonly maximum: "Maximum";
|
|
541
|
-
readonly minimum: "Minimum";
|
|
542
|
-
readonly pagination: "Page navigation";
|
|
543
|
-
readonly progress: "Progress";
|
|
544
|
-
readonly slider: "Slider";
|
|
545
|
-
readonly toggle: "Toggle";
|
|
546
|
-
readonly toggleOptions: "Toggle options";
|
|
547
|
-
readonly removableBadge: "Removable badge";
|
|
548
|
-
readonly removeBadge: "Remove badge";
|
|
549
|
-
readonly removeFile: "Remove {{name}}";
|
|
550
|
-
};
|
|
551
|
-
readonly button: {
|
|
552
|
-
readonly close: "Close";
|
|
553
|
-
readonly apply: "Apply";
|
|
554
|
-
readonly cancel: "Cancel";
|
|
555
|
-
readonly confirm: "Confirm";
|
|
556
|
-
};
|
|
557
|
-
readonly menu: {
|
|
558
|
-
readonly placeholder: "Select an option...";
|
|
559
|
-
};
|
|
560
|
-
readonly common: {
|
|
561
|
-
readonly loading: "Loading...";
|
|
562
|
-
};
|
|
563
|
-
readonly languages: {
|
|
564
|
-
readonly de: "Deutsch";
|
|
565
|
-
readonly en: "English";
|
|
566
|
-
readonly es: "Español";
|
|
567
|
-
readonly fr: "Français";
|
|
568
|
-
readonly it: "Italiano";
|
|
569
|
-
readonly nl: "Nederlands";
|
|
570
|
-
};
|
|
571
|
-
readonly localeSwitcher: {
|
|
572
|
-
readonly ariaLabel: "Language selection";
|
|
573
|
-
readonly placeholder: "Select language...";
|
|
574
|
-
};
|
|
575
|
-
readonly pagination: {
|
|
576
|
-
readonly next: "Next";
|
|
577
|
-
readonly first: "First";
|
|
578
|
-
readonly last: "Last";
|
|
579
|
-
readonly page: "Page";
|
|
580
|
-
readonly previous: "Previous";
|
|
581
|
-
};
|
|
582
|
-
readonly calendar: {
|
|
583
|
-
readonly recurring: "Recurring event";
|
|
584
|
-
readonly previousMonth: "Previous month";
|
|
585
|
-
readonly nextMonth: "Next month";
|
|
586
|
-
readonly previousWeek: "Previous week";
|
|
587
|
-
readonly nextWeek: "Next week";
|
|
588
|
-
readonly previousDay: "Previous day";
|
|
589
|
-
readonly nextDay: "Next day";
|
|
590
|
-
readonly previousYear: "Previous year";
|
|
591
|
-
readonly nextYear: "Next year";
|
|
592
|
-
readonly today: "Today";
|
|
593
|
-
readonly calendarGrid: "Calendar";
|
|
594
|
-
readonly weekdays: "Weekdays";
|
|
595
|
-
readonly weekNumber: "Week number";
|
|
596
|
-
readonly events: "Events";
|
|
597
|
-
readonly noEvents: "No events";
|
|
598
|
-
readonly showMore: "{{count}} more";
|
|
599
|
-
readonly legend: "Legend";
|
|
600
|
-
readonly yearView: "Year overview";
|
|
601
|
-
readonly weekView: "Week view";
|
|
602
|
-
readonly dayView: "Day view";
|
|
603
|
-
readonly monthView: "Month view";
|
|
604
|
-
readonly viewMonth: "Month";
|
|
605
|
-
readonly viewYear: "Year";
|
|
606
|
-
readonly viewWeek: "Week";
|
|
607
|
-
readonly viewDay: "Day";
|
|
608
|
-
readonly multiDayLabel: "Day {{current}} of {{total}}";
|
|
609
|
-
readonly agendaView: "Agenda view";
|
|
610
|
-
readonly viewAgenda: "Agenda";
|
|
611
|
-
readonly viewSwitcher: "View mode";
|
|
612
|
-
};
|
|
613
|
-
readonly planner: {
|
|
614
|
-
readonly previousWeek: "Previous week";
|
|
615
|
-
readonly nextWeek: "Next week";
|
|
616
|
-
readonly previousMonth: "Previous month";
|
|
617
|
-
readonly nextMonth: "Next month";
|
|
618
|
-
readonly previousRange: "Previous range";
|
|
619
|
-
readonly nextRange: "Next range";
|
|
620
|
-
readonly today: "Today";
|
|
621
|
-
readonly grid: "Planner";
|
|
622
|
-
readonly weekNumber: "Week number";
|
|
623
|
-
readonly itemCount: "{{count}} items";
|
|
624
|
-
};
|
|
625
|
-
readonly commandPalette: {
|
|
626
|
-
readonly noResults: "No results found.";
|
|
627
|
-
readonly search: "Search...";
|
|
628
|
-
readonly hints: {
|
|
629
|
-
readonly navigate: "Navigate";
|
|
630
|
-
readonly select: "Select";
|
|
631
|
-
readonly close: "Close";
|
|
632
|
-
};
|
|
633
|
-
};
|
|
634
|
-
readonly compositionBar: {
|
|
635
|
-
readonly total: "Total";
|
|
636
|
-
readonly summary: "Composition: {{total}} ({{count}} shares) — {{parts}}";
|
|
637
|
-
readonly share: "Share";
|
|
638
|
-
readonly value: "Value";
|
|
639
|
-
readonly percent: "Percent";
|
|
640
|
-
readonly remaining: "remaining";
|
|
641
|
-
};
|
|
642
|
-
readonly guide: {
|
|
643
|
-
readonly next: "Next";
|
|
644
|
-
readonly previous: "Back";
|
|
645
|
-
readonly skip: "Skip tour";
|
|
646
|
-
readonly done: "Done";
|
|
647
|
-
readonly step: "Step {{current}} of {{total}}";
|
|
648
|
-
readonly tour: "Guided tour";
|
|
649
|
-
readonly close: "Close";
|
|
650
|
-
readonly openHelp: "Help";
|
|
651
|
-
readonly backToList: "All topics";
|
|
652
|
-
readonly info: "More information";
|
|
653
|
-
readonly infoAbout: "More information about {{label}}";
|
|
654
|
-
readonly dismiss: "Dismiss hint";
|
|
655
|
-
readonly startTour: "Start the guided tour";
|
|
656
|
-
readonly actionRequired: "Complete the highlighted action to continue";
|
|
657
|
-
readonly filterPlaceholder: "Filter topics…";
|
|
658
|
-
readonly noResults: "No matching topics";
|
|
659
|
-
};
|
|
660
|
-
readonly datepicker: {
|
|
661
|
-
readonly placeholder: "Select a date...";
|
|
662
|
-
readonly rangePlaceholder: "Select a date range...";
|
|
663
|
-
readonly clear: "Clear selection";
|
|
664
|
-
readonly openCalendar: "Open calendar";
|
|
665
|
-
readonly invalidDate: "Invalid date";
|
|
666
|
-
readonly outOfRange: "Date is outside the allowed range";
|
|
667
|
-
readonly invalidRange: "Invalid date range";
|
|
668
|
-
};
|
|
669
|
-
readonly fileUpload: {
|
|
670
|
-
readonly exists: "File already added";
|
|
671
|
-
readonly invalidType: "File type {{type}} is not allowed";
|
|
672
|
-
readonly tooLarge: "File exceeds {{size}} limit";
|
|
673
|
-
readonly tooMany: "Maximum {{count}} files allowed";
|
|
674
|
-
readonly tooSmall: "File must be at least {{size}}";
|
|
675
|
-
};
|
|
676
|
-
readonly sankey: {
|
|
677
|
-
readonly summary: "Sankey diagram: {{nodes}} nodes, {{links}} links — {{flows}}";
|
|
678
|
-
readonly source: "Source";
|
|
679
|
-
readonly target: "Target";
|
|
680
|
-
readonly value: "Value";
|
|
681
|
-
};
|
|
682
|
-
readonly slider: {
|
|
683
|
-
readonly rangeStatus: {
|
|
684
|
-
readonly insideRecommended: "In recommended range";
|
|
685
|
-
readonly insideValid: "In valid range";
|
|
686
|
-
readonly insideValidOnly: "Outside recommended range, but valid";
|
|
687
|
-
readonly outsideValid: "Outside valid range";
|
|
688
|
-
};
|
|
689
|
-
};
|
|
690
|
-
readonly themeSwitcher: {
|
|
691
|
-
readonly lightMode: "Light mode";
|
|
692
|
-
readonly darkMode: "Dark mode";
|
|
693
|
-
readonly systemTheme: "System theme";
|
|
694
|
-
};
|
|
695
|
-
readonly time: {
|
|
696
|
-
readonly ago: "{{value}} {{unit}} ago";
|
|
697
|
-
readonly now: "now";
|
|
698
|
-
};
|
|
699
|
-
}>;
|
|
359
|
+
export declare const useBlocksI18n: any;
|
|
700
360
|
export declare const hasBlocksTranslation: (key: string) => boolean, getBlocksLocales: () => import("@urbicon-ui/i18n").Locale[];
|
|
701
361
|
export type BlocksTranslationKey = keyof typeof enTranslations;
|
|
702
362
|
export { blocksTranslations };
|
|
@@ -15,6 +15,18 @@ export interface GuideTopicMeta {
|
|
|
15
15
|
export interface GuideStep {
|
|
16
16
|
/** `data-guide` id of the element to anchor to. Omit for a centered, target-less step. */
|
|
17
17
|
target?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Optional route this step lives on — declarative cross-route touring. When set and different
|
|
20
|
+
* from the current location, the controller calls its injected `navigate` hook to go there
|
|
21
|
+
* **before** the spotlight, then re-anchors once the target appears on the new page (via the
|
|
22
|
+
* surface's existing `reapplyStepHighlight`). Such a tour-internal navigation keeps the tour
|
|
23
|
+
* running; a *foreign* navigation (the user leaving on their own) still stops it. Compared
|
|
24
|
+
* against `window.location.pathname` by default, so use a normalized path (no query/hash);
|
|
25
|
+
* inject a {@link GuideControllerOptions.navigationSource} for a custom router or base path.
|
|
26
|
+
* A `route` step with no `navigate` hook wired logs a DEV warning and stays put (no crash).
|
|
27
|
+
* @see GuideControllerOptions.navigate
|
|
28
|
+
*/
|
|
29
|
+
route?: string;
|
|
18
30
|
/** Step heading. */
|
|
19
31
|
title?: string;
|
|
20
32
|
/** Step body text. */
|
|
@@ -103,12 +115,48 @@ export interface GuideOverlayStackLike {
|
|
|
103
115
|
/** Number of modal overlays currently open — drives the non-modal hide (§3.4). */
|
|
104
116
|
readonly depth: number;
|
|
105
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Source of the current route and navigation notifications the controller needs for cross-route
|
|
120
|
+
* tours (injectable for tests and custom routers). The default {@link createBrowserNavigationSource}
|
|
121
|
+
* reads `window.location.pathname` and subscribes via the Navigation API, falling back to `popstate`.
|
|
122
|
+
*/
|
|
123
|
+
export interface GuideNavigationSource {
|
|
124
|
+
/** The current path, compared against a step's `route` (e.g. `"/expenses"`). `''` on the server. */
|
|
125
|
+
current(): string;
|
|
126
|
+
/**
|
|
127
|
+
* Subscribe to navigations; `onNavigate` receives the new path. Returns an unsubscribe. The
|
|
128
|
+
* controller subscribes only while a tour with at least one `route` step is active, so a tour
|
|
129
|
+
* that never declares a route observes no navigation (preserving the manual-`goto` recipe).
|
|
130
|
+
* `onNavigate` may be invoked **synchronously, re-entrant** during the controller's own `navigate`
|
|
131
|
+
* hook (a SvelteKit `afterNavigate` source fires inside `goto`); the controller recognizes that as
|
|
132
|
+
* the tour's own navigation and keeps it running, so the source needs no `queueMicrotask` deferral.
|
|
133
|
+
*/
|
|
134
|
+
subscribe(onNavigate: (path: string) => void): () => void;
|
|
135
|
+
}
|
|
106
136
|
/** Options for {@link GuideController}; every dependency is injectable for testing. */
|
|
107
137
|
export interface GuideControllerOptions {
|
|
108
138
|
/** Persistence adapter. @default localStorage-backed adapter */
|
|
109
139
|
storage?: GuideStorageAdapter;
|
|
110
140
|
/** Overlay stack to integrate with. @default the shared `overlayStack` singleton */
|
|
111
141
|
overlayStack?: GuideOverlayStackLike;
|
|
142
|
+
/**
|
|
143
|
+
* Navigation hook for declarative cross-route tours. When a step's `route` differs from the
|
|
144
|
+
* current location, the controller calls this to navigate there before spotlighting the step.
|
|
145
|
+
* Framework-agnostic by injection — a SvelteKit consumer wires `(route) => goto(route)`. May be
|
|
146
|
+
* sync or async; a thrown/rejected navigation is swallowed (DEV-warned). A `route` step with no
|
|
147
|
+
* hook wired logs a DEV warning and stays on the current route (no crash). @default undefined
|
|
148
|
+
*/
|
|
149
|
+
navigate?: (route: string) => void | Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Source for the current path + navigation events, used to decide whether a step's `route`
|
|
152
|
+
* needs navigating and to tell a tour-internal navigation from a foreign one (which stops the
|
|
153
|
+
* tour). @default a browser source reading `window.location.pathname` and listening via the
|
|
154
|
+
* Navigation API. Where that API is unavailable it falls back to `popstate` (back/forward only,
|
|
155
|
+
* not `pushState`), so a foreign *forward* navigation may go unobserved — inject a router-backed
|
|
156
|
+
* source (SvelteKit: `afterNavigate` + the `page` store; see docs/GUIDE.md §9) for reliable
|
|
157
|
+
* detection, or to handle a configured base path / custom router.
|
|
158
|
+
*/
|
|
159
|
+
navigationSource?: GuideNavigationSource;
|
|
112
160
|
/** Force DEV-mode warnings on/off. @default `import.meta.env?.DEV ?? false` */
|
|
113
161
|
dev?: boolean;
|
|
114
162
|
}
|
|
@@ -117,6 +165,13 @@ export interface GuideControllerOptions {
|
|
|
117
165
|
* resilient to private-mode / quota failures (mirrors `persistent-state`).
|
|
118
166
|
*/
|
|
119
167
|
export declare function createLocalStorageAdapter(key?: string): GuideStorageAdapter;
|
|
168
|
+
/**
|
|
169
|
+
* The default {@link GuideNavigationSource}: reads `window.location.pathname` and observes SPA
|
|
170
|
+
* navigations through the Navigation API when available — catching link clicks, programmatic
|
|
171
|
+
* navigation (`goto`), and back/forward — falling back to `popstate` (back/forward only) where it
|
|
172
|
+
* is not. SSR-safe: `current()` returns `''` and `subscribe()` is a no-op without a `window`.
|
|
173
|
+
*/
|
|
174
|
+
export declare function createBrowserNavigationSource(): GuideNavigationSource;
|
|
120
175
|
/**
|
|
121
176
|
* Headless engine for the Guide system — the UI-free state machine behind every
|
|
122
177
|
* Guide surface (Panel, Marker, Mention, Hint, Tour).
|
|
@@ -42,6 +42,71 @@ export function createLocalStorageAdapter(key = SEEN_STORAGE_KEY) {
|
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The default {@link GuideNavigationSource}: reads `window.location.pathname` and observes SPA
|
|
47
|
+
* navigations through the Navigation API when available — catching link clicks, programmatic
|
|
48
|
+
* navigation (`goto`), and back/forward — falling back to `popstate` (back/forward only) where it
|
|
49
|
+
* is not. SSR-safe: `current()` returns `''` and `subscribe()` is a no-op without a `window`.
|
|
50
|
+
*/
|
|
51
|
+
export function createBrowserNavigationSource() {
|
|
52
|
+
let warnedFallback = false;
|
|
53
|
+
return {
|
|
54
|
+
current() {
|
|
55
|
+
return BROWSER ? window.location.pathname : '';
|
|
56
|
+
},
|
|
57
|
+
subscribe(onNavigate) {
|
|
58
|
+
if (!BROWSER)
|
|
59
|
+
return () => { };
|
|
60
|
+
const nav = window.navigation;
|
|
61
|
+
if (nav && typeof nav.addEventListener === 'function') {
|
|
62
|
+
const handler = (event) => {
|
|
63
|
+
if (event.downloadRequest != null)
|
|
64
|
+
return; // a download is not a route change
|
|
65
|
+
const url = event.destination?.url;
|
|
66
|
+
if (typeof url !== 'string')
|
|
67
|
+
return;
|
|
68
|
+
let path;
|
|
69
|
+
try {
|
|
70
|
+
path = new URL(url).pathname;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return; // opaque/cross-origin destination — not a same-app route
|
|
74
|
+
}
|
|
75
|
+
onNavigate(path);
|
|
76
|
+
};
|
|
77
|
+
nav.addEventListener('navigate', handler);
|
|
78
|
+
return () => nav.removeEventListener('navigate', handler);
|
|
79
|
+
}
|
|
80
|
+
// `popstate` only catches back/forward, not `pushState` (a SvelteKit `goto` / intercepted
|
|
81
|
+
// link click), so a foreign *forward* navigation goes unobserved and the tour won't stop.
|
|
82
|
+
// Warn once so a degraded-detection browser is diagnosable; inject a router-backed
|
|
83
|
+
// navigationSource for reliable detection (docs/GUIDE.md §9).
|
|
84
|
+
if (import.meta.env?.DEV && !warnedFallback) {
|
|
85
|
+
warnedFallback = true;
|
|
86
|
+
console.warn('[Guide] the Navigation API is unavailable; cross-route foreign-navigation detection falls back to popstate and will miss pushState navigations (link clicks, goto). Inject a router-backed navigationSource for reliable detection — see docs/GUIDE.md §9.');
|
|
87
|
+
}
|
|
88
|
+
const onPop = () => onNavigate(window.location.pathname);
|
|
89
|
+
window.addEventListener('popstate', onPop);
|
|
90
|
+
return () => window.removeEventListener('popstate', onPop);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** DEV-only: grace period for a navigated-to `route` step's target before warning it never showed. */
|
|
95
|
+
const ROUTE_TARGET_TIMEOUT_MS = 4000;
|
|
96
|
+
/**
|
|
97
|
+
* Heuristic: does `landed` look like a router-normalized form of `route` — a trailing slash
|
|
98
|
+
* (`/expenses/`) or a base/locale prefix (`/de/expenses`) — rather than an unrelated redirect target
|
|
99
|
+
* (`/login`)? Used ONLY to decide whether a *synchronous* own-navigation landing on a non-exact path
|
|
100
|
+
* deserves a DEV diagnostic, never to change tour behavior, so a wrong guess merely over- or
|
|
101
|
+
* under-warns in DEV. `route` is a normalized pathname (leading `/`), so the suffix test lands on a
|
|
102
|
+
* real segment boundary — `/myexpenses` does not end with `/expenses`.
|
|
103
|
+
*/
|
|
104
|
+
function looksLikeNormalizedRoute(landed, route) {
|
|
105
|
+
const stripSlash = (s) => (s.length > 1 && s.endsWith('/') ? s.slice(0, -1) : s);
|
|
106
|
+
const l = stripSlash(landed);
|
|
107
|
+
const r = stripSlash(route);
|
|
108
|
+
return l === r || l.endsWith(r);
|
|
109
|
+
}
|
|
45
110
|
let tourIdCounter = 0;
|
|
46
111
|
/** Opaque id for overlay-stack registration. Never SSR-rendered, so no hydration risk. */
|
|
47
112
|
function nextOverlayId() {
|
|
@@ -101,6 +166,32 @@ export class GuideController {
|
|
|
101
166
|
// independently of #activeTour (today they always change in lockstep).
|
|
102
167
|
#tourOverlayId = $state(null);
|
|
103
168
|
#tourUnregister = null;
|
|
169
|
+
// ── Cross-route touring ─────────────────────────────────
|
|
170
|
+
#navigate;
|
|
171
|
+
#navSource;
|
|
172
|
+
/** The path a tour-internal navigation is heading to; distinguishes our own nav from a foreign one. */
|
|
173
|
+
#expectedRoute = null;
|
|
174
|
+
/**
|
|
175
|
+
* `true` only while our own injected `#navigate()` call is on the stack. A `navigationSource` that
|
|
176
|
+
* reports its location change **synchronously, re-entrant** during that call (SvelteKit's
|
|
177
|
+
* `afterNavigate` fires inside `goto`) IS that very navigation — never a foreign one. This is the
|
|
178
|
+
* exact signal; the `path === #expectedRoute` check in `#onLocationChange` is only an async-source
|
|
179
|
+
* proxy for the same thing and breaks the moment the router reports a normalized path
|
|
180
|
+
* (trailing slash, base/locale prefix) that doesn't equal the raw `step.route`.
|
|
181
|
+
*
|
|
182
|
+
* A boolean (not a depth counter) is enough: the sole re-entrant callee of a sync source is
|
|
183
|
+
* `#onLocationChange`, which never calls `#maybeNavigate`. A `navigate` hook that *synchronously*
|
|
184
|
+
* re-enters `next`/`prev`/`startTour` is out of contract — its inner `#maybeNavigate` `finally`
|
|
185
|
+
* would clear this early; that only ever fails *closed* (a later own report read as foreign → stop),
|
|
186
|
+
* never open, so it can't keep a genuinely foreign navigation alive.
|
|
187
|
+
*/
|
|
188
|
+
#selfNavigating = false;
|
|
189
|
+
/** Last path the tour is known to be on — set at start, updated on each handled navigation. */
|
|
190
|
+
#knownPath = '';
|
|
191
|
+
/** Unsubscribe from the navigation source; non-null only while a route-using tour runs. */
|
|
192
|
+
#navUnsub = null;
|
|
193
|
+
/** DEV-only timer that warns when a navigated-to step's target never appears. */
|
|
194
|
+
#routeTargetTimer = null;
|
|
104
195
|
// ── Highlight (shared by tour steps + Mention→UI, Direction B) ──
|
|
105
196
|
#highlightedId = $state(null);
|
|
106
197
|
// ── Panel state (UI in Phase 3) ─────────────────────────
|
|
@@ -112,6 +203,8 @@ export class GuideController {
|
|
|
112
203
|
constructor(options = {}) {
|
|
113
204
|
this.#storage = options.storage ?? createLocalStorageAdapter();
|
|
114
205
|
this.#overlayStack = options.overlayStack ?? overlayStack;
|
|
206
|
+
this.#navigate = options.navigate;
|
|
207
|
+
this.#navSource = options.navigationSource ?? createBrowserNavigationSource();
|
|
115
208
|
// `import.meta.env?.DEV` is `boolean | undefined` (undefined outside Vite);
|
|
116
209
|
// `?? false` keeps `#dev` a strict boolean and means non-Vite consumers
|
|
117
210
|
// simply get no dev warnings (rather than a crash).
|
|
@@ -240,8 +333,14 @@ export class GuideController {
|
|
|
240
333
|
this.#stepIndex = 0;
|
|
241
334
|
this.#tourOverlayId = nextOverlayId();
|
|
242
335
|
this.#tourUnregister = this.#overlayStack.register(this.#tourOverlayId, () => this.skip());
|
|
243
|
-
|
|
244
|
-
|
|
336
|
+
// Observe navigation only for tours that declare a step route. This keeps the manual-`goto`
|
|
337
|
+
// recipe (consumer navigates in `onStep`, no `step.route`) untouched, where a navigation is
|
|
338
|
+
// the consumer's own and must NOT be read as a foreign one that stops the tour.
|
|
339
|
+
this.#knownPath = this.#navSource.current();
|
|
340
|
+
if (tour.steps.some((s) => typeof s.route === 'string')) {
|
|
341
|
+
this.#navUnsub = this.#navSource.subscribe((path) => this.#onLocationChange(path));
|
|
342
|
+
}
|
|
343
|
+
this.#activateStep('start');
|
|
245
344
|
return true;
|
|
246
345
|
}
|
|
247
346
|
/** Advance to the next step, or finish the tour if on the last step. */
|
|
@@ -253,16 +352,14 @@ export class GuideController {
|
|
|
253
352
|
return;
|
|
254
353
|
}
|
|
255
354
|
this.#stepIndex += 1;
|
|
256
|
-
this.#
|
|
257
|
-
this.#emitStep('next');
|
|
355
|
+
this.#activateStep('next');
|
|
258
356
|
}
|
|
259
357
|
/** Go back one step. No-op on the first step. */
|
|
260
358
|
prev() {
|
|
261
359
|
if (!this.#activeTour || this.isFirstStep)
|
|
262
360
|
return;
|
|
263
361
|
this.#stepIndex -= 1;
|
|
264
|
-
this.#
|
|
265
|
-
this.#emitStep('prev');
|
|
362
|
+
this.#activateStep('prev');
|
|
266
363
|
}
|
|
267
364
|
/** Dismiss the tour without completing it — marks it seen, fires `onSkip`. */
|
|
268
365
|
skip() {
|
|
@@ -304,6 +401,163 @@ export class GuideController {
|
|
|
304
401
|
if (this.#activeTour)
|
|
305
402
|
this.#applyStepHighlight();
|
|
306
403
|
}
|
|
404
|
+
// ─── Cross-route touring ──────────────────────────────────────────────────
|
|
405
|
+
/**
|
|
406
|
+
* Make the now-current step active: trigger its cross-route navigation (if any), apply the
|
|
407
|
+
* highlight, then fire `onStep`. Shared by `startTour`/`next`/`prev`, so every entry point
|
|
408
|
+
* navigates symmetrically — including `prev()` stepping back across a route boundary.
|
|
409
|
+
*/
|
|
410
|
+
#activateStep(via) {
|
|
411
|
+
this.#clearRouteTargetWarning();
|
|
412
|
+
this.#maybeNavigate();
|
|
413
|
+
this.#applyStepHighlight();
|
|
414
|
+
this.#emitStep(via);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* If the active step lives on a different route, navigate there via the injected hook and record
|
|
418
|
+
* it as the `expectedRoute`, so the resulting navigation is recognized as the tour's own rather
|
|
419
|
+
* than a foreign one. Already on the route → nothing to do. No hook wired → DEV warning, stay put.
|
|
420
|
+
*/
|
|
421
|
+
#maybeNavigate() {
|
|
422
|
+
// Each step starts with a clean slate — no navigation is pending until we trigger one below.
|
|
423
|
+
// (A step that doesn't navigate must not inherit the previous step's `expectedRoute`.)
|
|
424
|
+
this.#expectedRoute = null;
|
|
425
|
+
const route = this.currentStep?.route;
|
|
426
|
+
if (route == null)
|
|
427
|
+
return;
|
|
428
|
+
if (route === this.#navSource.current())
|
|
429
|
+
return;
|
|
430
|
+
if (!this.#navigate) {
|
|
431
|
+
if (this.#dev) {
|
|
432
|
+
console.warn(`[Guide] tour "${this.#activeTour?.id}" step ${this.#stepIndex}: step has route "${route}" but no navigate hook was provided to the GuideController — staying on the current route.`);
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
this.#expectedRoute = route;
|
|
437
|
+
this.#scheduleRouteTargetWarning();
|
|
438
|
+
// Mark the window during which a synchronous (re-entrant) location report from the
|
|
439
|
+
// navigationSource is unambiguously our own navigation. Reset in `finally` the instant the
|
|
440
|
+
// (sync part of the) hook returns — an *async* report (the default Navigation-API source, or a
|
|
441
|
+
// microtask-deferred one) then falls through to the normal `#expectedRoute` matching, so the
|
|
442
|
+
// async path is unchanged.
|
|
443
|
+
this.#selfNavigating = true;
|
|
444
|
+
try {
|
|
445
|
+
const result = this.#navigate(route);
|
|
446
|
+
if (result && typeof result.then === 'function') {
|
|
447
|
+
result.catch((err) => {
|
|
448
|
+
if (this.#dev)
|
|
449
|
+
console.warn('[Guide] navigate hook rejected:', err);
|
|
450
|
+
this.#abortPendingNavigation(route);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
if (this.#dev)
|
|
456
|
+
console.warn('[Guide] navigate hook threw:', err);
|
|
457
|
+
this.#abortPendingNavigation(route);
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
this.#selfNavigating = false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Roll back the bookkeeping for a navigation that failed (hook threw/rejected) — but only if it
|
|
465
|
+
* is still the pending one, so a newer step's navigation (the user clicked Next while this hook
|
|
466
|
+
* was in flight) is never clobbered by a late rejection. Clears the now-meaningless "target never
|
|
467
|
+
* appeared" timer too, so it can't later fire a misleading warning for a navigation that never ran.
|
|
468
|
+
*/
|
|
469
|
+
#abortPendingNavigation(route) {
|
|
470
|
+
if (this.#expectedRoute !== route)
|
|
471
|
+
return;
|
|
472
|
+
this.#expectedRoute = null;
|
|
473
|
+
this.#clearRouteTargetWarning();
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* React to a navigation observed while a route-using tour runs. A navigation that is the tour's own
|
|
477
|
+
* — reported synchronously during our `#navigate()` call (`#selfNavigating`), or matching the
|
|
478
|
+
* pending `#expectedRoute` — keeps it running (the ring lands via the surface's
|
|
479
|
+
* `reapplyStepHighlight`). Any other navigation is foreign — the user left, or a
|
|
480
|
+
* youngest-gesture-wins race — and tears the tour down, analytics-silent (`stopTour`).
|
|
481
|
+
*/
|
|
482
|
+
#onLocationChange(path) {
|
|
483
|
+
if (!this.#activeTour)
|
|
484
|
+
return;
|
|
485
|
+
if (path === this.#knownPath)
|
|
486
|
+
return; // no pathname change (e.g. a hash/query update) — ignore
|
|
487
|
+
this.#knownPath = path;
|
|
488
|
+
const matchesExpected = this.#expectedRoute !== null && path === this.#expectedRoute;
|
|
489
|
+
// A location report is the tour's OWN navigation in two cases: it matches the pending
|
|
490
|
+
// `#expectedRoute` (the async-source proxy, for a report landing a tick after the hook returns),
|
|
491
|
+
// or it arrives synchronously, re-entrant, during our injected `#navigate()` call
|
|
492
|
+
// (`#selfNavigating`). The latter is a *causal* signal — our navigate produced this report,
|
|
493
|
+
// whatever path it carries — so it covers a router-normalized landing (trailing slash,
|
|
494
|
+
// base/locale prefix) that an exact match would miss. Either keeps the tour running.
|
|
495
|
+
if (this.#selfNavigating || matchesExpected) {
|
|
496
|
+
// A *synchronous* report on a path that isn't the expected route is still causally ours, but it
|
|
497
|
+
// landed somewhere unexpected: a normalized variant of `step.route` (fine — the target anchors
|
|
498
|
+
// once it mounts) or a genuine redirect off the route (an auth guard → `/login`). We keep
|
|
499
|
+
// running either way — stopping would re-break the normalized case this fix exists for — but a
|
|
500
|
+
// landing that doesn't even look like a normalized variant is most likely a redirect, so
|
|
501
|
+
// surface it in DEV. (A targetless step has no "target never appeared" timer, so an off-route
|
|
502
|
+
// bubble is otherwise invisible in both DEV and PROD.)
|
|
503
|
+
if (this.#dev &&
|
|
504
|
+
this.#selfNavigating &&
|
|
505
|
+
this.#expectedRoute !== null &&
|
|
506
|
+
!matchesExpected &&
|
|
507
|
+
!looksLikeNormalizedRoute(path, this.#expectedRoute)) {
|
|
508
|
+
console.warn(`[Guide] tour "${this.#activeTour?.id}" navigated toward "${this.#expectedRoute}" but synchronously landed on "${path}" — keeping the tour running as its own navigation. If this was a redirect off the step's route (not a normalized form of it), stop the tour explicitly from your router guard.`);
|
|
509
|
+
}
|
|
510
|
+
// For a *targeted* step, keep `#expectedRoute` set until the target settles
|
|
511
|
+
// (`#applyStepHighlight`) so a redirecting / multi-hop source (the Navigation API emitting one
|
|
512
|
+
// event per hop) firing a *second* event for the redirect target is still recognized as a
|
|
513
|
+
// mismatch (and DEV-warned) instead of silently stopping. A *targetless* route step has no
|
|
514
|
+
// target to wait for — it is settled on landing, so clear now; otherwise a later foreign
|
|
515
|
+
// navigation would misfire the "navigated toward …" warning. (`#maybeNavigate` also resets it
|
|
516
|
+
// on the next step. Clearing here can't move into `#applyStepHighlight`, which runs
|
|
517
|
+
// synchronously in `#activateStep` before the nav lands — it would mis-read our own nav.)
|
|
518
|
+
if (this.currentStep?.target == null)
|
|
519
|
+
this.#expectedRoute = null;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
// Any other navigation stops the tour (analytics-silent). When one was pending, a landed path
|
|
523
|
+
// that doesn't match is most often a normalized `step.route` (trailing slash, base/locale
|
|
524
|
+
// prefix) rather than a genuine foreign nav — so surface it instead of a silent teardown on a
|
|
525
|
+
// one-character mismatch. (The strict stop is kept either way: better than running on a wrong page.)
|
|
526
|
+
if (this.#dev && this.#expectedRoute !== null) {
|
|
527
|
+
console.warn(`[Guide] tour "${this.#activeTour?.id}" navigated toward "${this.#expectedRoute}" but landed on "${path}" — stopping as a foreign navigation. If "${path}" is just a normalized form of the step's route (trailing slash, base/locale prefix), set step.route to the router's actual path.`);
|
|
528
|
+
}
|
|
529
|
+
this.#expectedRoute = null;
|
|
530
|
+
this.stopTour();
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* DEV-only: after navigating for a `route` step, warn if its target never appears so the spotlight
|
|
534
|
+
* can't land (the step still renders, centered over the scrim — it never hangs). Browser-gated, so
|
|
535
|
+
* no timer is ever scheduled under SSR or in node tests.
|
|
536
|
+
*/
|
|
537
|
+
#scheduleRouteTargetWarning() {
|
|
538
|
+
if (!this.#dev || !BROWSER)
|
|
539
|
+
return;
|
|
540
|
+
const step = this.currentStep;
|
|
541
|
+
if (!step?.target)
|
|
542
|
+
return;
|
|
543
|
+
this.#clearRouteTargetWarning();
|
|
544
|
+
const index = this.#stepIndex;
|
|
545
|
+
const targetId = step.target;
|
|
546
|
+
const route = step.route;
|
|
547
|
+
this.#routeTargetTimer = setTimeout(() => {
|
|
548
|
+
this.#routeTargetTimer = null;
|
|
549
|
+
if (this.#stepIndex === index && !this.resolveTarget(targetId)) {
|
|
550
|
+
console.warn(`[Guide] tour "${this.#activeTour?.id}" step ${index}: navigated to "${route}" but target "${targetId}" never appeared — showing the step without a spotlight ring.`);
|
|
551
|
+
}
|
|
552
|
+
}, ROUTE_TARGET_TIMEOUT_MS);
|
|
553
|
+
}
|
|
554
|
+
/** Cancel a pending route-target warning (target appeared, the step changed, or teardown). */
|
|
555
|
+
#clearRouteTargetWarning() {
|
|
556
|
+
if (this.#routeTargetTimer !== null) {
|
|
557
|
+
clearTimeout(this.#routeTargetTimer);
|
|
558
|
+
this.#routeTargetTimer = null;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
307
561
|
#endTour() {
|
|
308
562
|
const tour = this.#activeTour;
|
|
309
563
|
this.#teardownTour();
|
|
@@ -313,8 +567,12 @@ export class GuideController {
|
|
|
313
567
|
if (tour && tour.once !== false)
|
|
314
568
|
this.markSeen(tour.id);
|
|
315
569
|
}
|
|
316
|
-
/** Reset all tour state and release the overlay-stack
|
|
570
|
+
/** Reset all tour state and release the overlay-stack + navigation registrations. No persistence. */
|
|
317
571
|
#teardownTour() {
|
|
572
|
+
this.#clearRouteTargetWarning();
|
|
573
|
+
this.#navUnsub?.();
|
|
574
|
+
this.#navUnsub = null;
|
|
575
|
+
this.#expectedRoute = null;
|
|
318
576
|
this.clearHighlight();
|
|
319
577
|
this.#tourUnregister?.();
|
|
320
578
|
this.#tourUnregister = null;
|
|
@@ -364,10 +622,16 @@ export class GuideController {
|
|
|
364
622
|
// Unlike Direction-B hover (which never scrolls), a tour drives the viewport —
|
|
365
623
|
// the scroll is reduced-motion-aware inside `highlight`.
|
|
366
624
|
this.highlight(step.target, { scroll: true });
|
|
625
|
+
this.#clearRouteTargetWarning(); // target landed → no "never appeared" warning needed
|
|
626
|
+
this.#expectedRoute = null; // the navigated-to target resolved → navigation fully settled
|
|
367
627
|
}
|
|
368
628
|
else {
|
|
369
629
|
this.clearHighlight();
|
|
370
|
-
|
|
630
|
+
// A step bound to another route legitimately has no target on the current page — during the
|
|
631
|
+
// navigation gap, or when no navigate hook is wired — so don't warn "not found" there; the
|
|
632
|
+
// missing-hook and never-appeared paths own those diagnostics instead.
|
|
633
|
+
const offRoute = step.route != null && step.route !== this.#navSource.current();
|
|
634
|
+
if (this.#dev && !offRoute) {
|
|
371
635
|
console.warn(`[Guide] tour "${this.#activeTour?.id}" step ${this.#stepIndex}: target "${step.target}" not found in DOM.`);
|
|
372
636
|
}
|
|
373
637
|
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export * from './date.js';
|
|
|
2
2
|
export * from './draggable.js';
|
|
3
3
|
export * from './figma-token-export.js';
|
|
4
4
|
export { arrow, autoUpdate, type ComputePositionReturn, computePosition, flip, type Middleware, offset, type Placement, type Strategy, shift, size } from './floating.js';
|
|
5
|
-
export { createLocalStorageAdapter, GuideController, type GuideControllerOptions, type GuideDirection, type GuideEndEvent, type GuideOverlayStackLike, type GuideStep, type GuideStepEvent, type GuideStorageAdapter, type GuideTopicMeta, type GuideTour } from './guide.svelte.js';
|
|
5
|
+
export { createBrowserNavigationSource, createLocalStorageAdapter, GuideController, type GuideControllerOptions, type GuideDirection, type GuideEndEvent, type GuideNavigationSource, type GuideOverlayStackLike, type GuideStep, type GuideStepEvent, type GuideStorageAdapter, type GuideTopicMeta, type GuideTour } from './guide.svelte.js';
|
|
6
6
|
export * from './id.js';
|
|
7
7
|
export { observeTargetResolution } from './observe-target.js';
|
|
8
8
|
export { createOptionalContext } from './optional-context.js';
|
package/dist/utils/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export * from './date.js';
|
|
|
2
2
|
export * from './draggable.js';
|
|
3
3
|
export * from './figma-token-export.js';
|
|
4
4
|
export { arrow, autoUpdate, computePosition, flip, offset, shift, size } from './floating.js';
|
|
5
|
-
export { createLocalStorageAdapter, GuideController } from './guide.svelte.js';
|
|
5
|
+
export { createBrowserNavigationSource, createLocalStorageAdapter, GuideController } from './guide.svelte.js';
|
|
6
6
|
export * from './id.js';
|
|
7
7
|
export { observeTargetResolution } from './observe-target.js';
|
|
8
8
|
export { createOptionalContext } from './optional-context.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/blocks",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.1",
|
|
4
4
|
"description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"@sveltejs/package": "^2.5.8",
|
|
92
92
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
93
93
|
"@tailwindcss/vite": "^4.3.1",
|
|
94
|
-
"@urbicon-ui/i18n": "6.
|
|
95
|
-
"@urbicon-ui/shared-types": "6.
|
|
94
|
+
"@urbicon-ui/i18n": "6.8.1",
|
|
95
|
+
"@urbicon-ui/shared-types": "6.8.1",
|
|
96
96
|
"prettier": "^3.8.4",
|
|
97
97
|
"prettier-plugin-svelte": "^4.1.1",
|
|
98
98
|
"prettier-plugin-tailwindcss": "^0.8.0",
|