@urbicon-ui/blocks 6.8.0 → 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.
|
@@ -127,6 +127,9 @@ export interface GuideNavigationSource {
|
|
|
127
127
|
* Subscribe to navigations; `onNavigate` receives the new path. Returns an unsubscribe. The
|
|
128
128
|
* controller subscribes only while a tour with at least one `route` step is active, so a tour
|
|
129
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.
|
|
130
133
|
*/
|
|
131
134
|
subscribe(onNavigate: (path: string) => void): () => void;
|
|
132
135
|
}
|
|
@@ -93,6 +93,20 @@ export function createBrowserNavigationSource() {
|
|
|
93
93
|
}
|
|
94
94
|
/** DEV-only: grace period for a navigated-to `route` step's target before warning it never showed. */
|
|
95
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
|
+
}
|
|
96
110
|
let tourIdCounter = 0;
|
|
97
111
|
/** Opaque id for overlay-stack registration. Never SSR-rendered, so no hydration risk. */
|
|
98
112
|
function nextOverlayId() {
|
|
@@ -157,6 +171,21 @@ export class GuideController {
|
|
|
157
171
|
#navSource;
|
|
158
172
|
/** The path a tour-internal navigation is heading to; distinguishes our own nav from a foreign one. */
|
|
159
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;
|
|
160
189
|
/** Last path the tour is known to be on — set at start, updated on each handled navigation. */
|
|
161
190
|
#knownPath = '';
|
|
162
191
|
/** Unsubscribe from the navigation source; non-null only while a route-using tour runs. */
|
|
@@ -406,6 +435,12 @@ export class GuideController {
|
|
|
406
435
|
}
|
|
407
436
|
this.#expectedRoute = route;
|
|
408
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;
|
|
409
444
|
try {
|
|
410
445
|
const result = this.#navigate(route);
|
|
411
446
|
if (result && typeof result.then === 'function') {
|
|
@@ -421,6 +456,9 @@ export class GuideController {
|
|
|
421
456
|
console.warn('[Guide] navigate hook threw:', err);
|
|
422
457
|
this.#abortPendingNavigation(route);
|
|
423
458
|
}
|
|
459
|
+
finally {
|
|
460
|
+
this.#selfNavigating = false;
|
|
461
|
+
}
|
|
424
462
|
}
|
|
425
463
|
/**
|
|
426
464
|
* Roll back the bookkeeping for a navigation that failed (hook threw/rejected) — but only if it
|
|
@@ -435,9 +473,10 @@ export class GuideController {
|
|
|
435
473
|
this.#clearRouteTargetWarning();
|
|
436
474
|
}
|
|
437
475
|
/**
|
|
438
|
-
* React to a navigation observed while a route-using tour runs. A navigation
|
|
439
|
-
*
|
|
440
|
-
*
|
|
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
|
|
441
480
|
* youngest-gesture-wins race — and tears the tour down, analytics-silent (`stopTour`).
|
|
442
481
|
*/
|
|
443
482
|
#onLocationChange(path) {
|
|
@@ -446,15 +485,36 @@ export class GuideController {
|
|
|
446
485
|
if (path === this.#knownPath)
|
|
447
486
|
return; // no pathname change (e.g. a hash/query update) — ignore
|
|
448
487
|
this.#knownPath = path;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
//
|
|
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.)
|
|
458
518
|
if (this.currentStep?.target == null)
|
|
459
519
|
this.#expectedRoute = null;
|
|
460
520
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/blocks",
|
|
3
|
-
"version": "6.8.
|
|
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.8.
|
|
95
|
-
"@urbicon-ui/shared-types": "6.8.
|
|
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",
|