@marianmeres/stuic 3.65.0 → 3.66.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/API.md
CHANGED
|
@@ -1482,6 +1482,7 @@ Multi-step onboarding tour built on the spotlight primitive. Define steps centra
|
|
|
1482
1482
|
| `borderRadius`| `number` | Cutout border radius (px) |
|
|
1483
1483
|
| `onEnter` | `() => void` | Called when entering step |
|
|
1484
1484
|
| `onLeave` | `() => void` | Called when leaving step |
|
|
1485
|
+
| `selector` | `string` | CSS selector to find the target element (alternative to `use:tourStep`) |
|
|
1485
1486
|
|
|
1486
1487
|
**`tourStep` action:** `use:tourStep={[tour, stepId]}`
|
|
1487
1488
|
|
|
@@ -1504,6 +1505,29 @@ Multi-step onboarding tour built on the spotlight primitive. Define steps centra
|
|
|
1504
1505
|
<button onclick={tour.start}>Start Tour</button>
|
|
1505
1506
|
```
|
|
1506
1507
|
|
|
1508
|
+
**Selector-based targeting:** Steps can target elements by CSS selector instead of `use:tourStep`. Useful when the target lives inside a reusable component that shouldn't know about the tour:
|
|
1509
|
+
|
|
1510
|
+
```svelte
|
|
1511
|
+
<!-- ReusableComponent.svelte — no tour knowledge -->
|
|
1512
|
+
<button data-tour-id="download">Download</button>
|
|
1513
|
+
|
|
1514
|
+
<!-- Tour config -->
|
|
1515
|
+
<script>
|
|
1516
|
+
const tour = createTour({
|
|
1517
|
+
steps: [
|
|
1518
|
+
{
|
|
1519
|
+
id: "dl-step",
|
|
1520
|
+
title: "Download",
|
|
1521
|
+
content: "Click here to download.",
|
|
1522
|
+
selector: '[data-tour-id="download"]',
|
|
1523
|
+
},
|
|
1524
|
+
],
|
|
1525
|
+
});
|
|
1526
|
+
</script>
|
|
1527
|
+
```
|
|
1528
|
+
|
|
1529
|
+
When a step has `selector`, the tour uses `document.querySelector(selector)` to find the target element. If the element isn't in the DOM yet, the tour polls periodically until `waitForElement` ms elapse (same timeout as `use:tourStep`). Steps without `selector` continue to use `use:tourStep` as before — both mechanisms coexist freely.
|
|
1530
|
+
|
|
1507
1531
|
---
|
|
1508
1532
|
|
|
1509
1533
|
## Utilities
|
|
@@ -30,6 +30,12 @@ export interface TourStepDef {
|
|
|
30
30
|
onEnter?: () => void;
|
|
31
31
|
/** Called when tour leaves this step */
|
|
32
32
|
onLeave?: () => void;
|
|
33
|
+
/**
|
|
34
|
+
* CSS selector to find the target element in the DOM.
|
|
35
|
+
* If provided, the tour will use `document.querySelector(selector)`
|
|
36
|
+
* instead of waiting for a `use:tourStep` registration.
|
|
37
|
+
*/
|
|
38
|
+
selector?: string;
|
|
33
39
|
}
|
|
34
40
|
/**
|
|
35
41
|
* Default label overrides for the navigation shell buttons.
|
|
@@ -147,6 +153,7 @@ export declare function createTour(options: TourOptions): {
|
|
|
147
153
|
reset: () => void;
|
|
148
154
|
_register: (id: string, el: HTMLElement) => void;
|
|
149
155
|
_unregister: (id: string) => void;
|
|
156
|
+
_registerAction: (id: string) => void;
|
|
150
157
|
_isCurrentStep: (id: string) => boolean;
|
|
151
158
|
_getShellContent: (id: string) => THC;
|
|
152
159
|
};
|
|
@@ -36,6 +36,8 @@ export function createTour(options) {
|
|
|
36
36
|
: null;
|
|
37
37
|
// Element registry: stepId -> HTMLElement
|
|
38
38
|
const registry = new Map();
|
|
39
|
+
// Steps registered via use:tourStep action (to prevent double-spotlight with selector)
|
|
40
|
+
const actionRegistered = new Set();
|
|
39
41
|
// Wait-for-element mechanism (one pending wait at a time)
|
|
40
42
|
let pendingStepId = null;
|
|
41
43
|
let pendingResolve = null;
|
|
@@ -63,7 +65,34 @@ export function createTour(options) {
|
|
|
63
65
|
return () => document.removeEventListener("keydown", handler);
|
|
64
66
|
}
|
|
65
67
|
});
|
|
68
|
+
// Spotlight for selector-based steps (no use:tourStep action to manage it)
|
|
69
|
+
$effect(() => {
|
|
70
|
+
const step = currentStep;
|
|
71
|
+
if (!active || !step?.selector)
|
|
72
|
+
return;
|
|
73
|
+
// If this step was registered via use:tourStep, let the action handle spotlight
|
|
74
|
+
if (actionRegistered.has(step.id))
|
|
75
|
+
return;
|
|
76
|
+
const el = registry.get(step.id);
|
|
77
|
+
if (!el)
|
|
78
|
+
return;
|
|
79
|
+
// spotlight() creates inner $effects; Svelte cleans them up
|
|
80
|
+
// when this outer effect re-runs (step change) or is destroyed (tour end)
|
|
81
|
+
spotlight(el, () => ({
|
|
82
|
+
open: true,
|
|
83
|
+
content: _getShellContent(step.id),
|
|
84
|
+
position: step.position ?? "bottom",
|
|
85
|
+
padding: step.padding,
|
|
86
|
+
borderRadius: step.borderRadius,
|
|
87
|
+
closeOnEscape: false,
|
|
88
|
+
closeOnBackdropClick: false,
|
|
89
|
+
scrollIntoView: true,
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
66
92
|
// -- Internal API (used by tourStep action) -----------------------------------------
|
|
93
|
+
function _registerAction(id) {
|
|
94
|
+
actionRegistered.add(id);
|
|
95
|
+
}
|
|
67
96
|
function _register(id, el) {
|
|
68
97
|
registry.set(id, el);
|
|
69
98
|
// If we were waiting for this element, resolve immediately
|
|
@@ -119,10 +148,25 @@ export function createTour(options) {
|
|
|
119
148
|
pendingResolve = null;
|
|
120
149
|
pendingStepId = null;
|
|
121
150
|
}
|
|
151
|
+
const step = options.steps.find((s) => s.id === id);
|
|
152
|
+
const selector = step?.selector;
|
|
122
153
|
return new Promise((resolve) => {
|
|
123
154
|
pendingStepId = id;
|
|
124
155
|
pendingResolve = resolve;
|
|
156
|
+
// For selector-based steps, poll the DOM periodically
|
|
157
|
+
let pollInterval = null;
|
|
158
|
+
if (selector) {
|
|
159
|
+
pollInterval = setInterval(() => {
|
|
160
|
+
const el = document.querySelector(selector);
|
|
161
|
+
if (el && pendingStepId === id) {
|
|
162
|
+
_register(id, el);
|
|
163
|
+
// _register() resolves the promise via pendingResolve
|
|
164
|
+
}
|
|
165
|
+
}, 50);
|
|
166
|
+
}
|
|
125
167
|
setTimeout(() => {
|
|
168
|
+
if (pollInterval)
|
|
169
|
+
clearInterval(pollInterval);
|
|
126
170
|
if (pendingStepId === id) {
|
|
127
171
|
console.warn(`[createTour] Step "${id}" element not found after ${options.waitForElement ?? 500}ms — skipping`);
|
|
128
172
|
pendingStepId = null;
|
|
@@ -144,6 +188,13 @@ export function createTour(options) {
|
|
|
144
188
|
// Find the nearest available step in the direction of travel
|
|
145
189
|
while (index >= 0 && index < options.steps.length) {
|
|
146
190
|
const step = options.steps[index];
|
|
191
|
+
// For selector-based steps, try to find the element in the DOM
|
|
192
|
+
if (step.selector && !registry.has(step.id)) {
|
|
193
|
+
const el = document.querySelector(step.selector);
|
|
194
|
+
if (el) {
|
|
195
|
+
_register(step.id, el);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
147
198
|
if (!registry.has(step.id)) {
|
|
148
199
|
const found = await waitForElement(step.id);
|
|
149
200
|
if (!found) {
|
|
@@ -216,6 +267,7 @@ export function createTour(options) {
|
|
|
216
267
|
// Internal — used by tourStep action
|
|
217
268
|
_register,
|
|
218
269
|
_unregister,
|
|
270
|
+
_registerAction,
|
|
219
271
|
_isCurrentStep,
|
|
220
272
|
_getShellContent,
|
|
221
273
|
};
|
|
@@ -235,6 +287,7 @@ export function tourStep(el, args) {
|
|
|
235
287
|
const [tour, id] = args;
|
|
236
288
|
if (!tour)
|
|
237
289
|
return;
|
|
290
|
+
tour._registerAction(id);
|
|
238
291
|
tour._register(id, el);
|
|
239
292
|
spotlight(el, () => {
|
|
240
293
|
const isActive = tour._isCurrentStep(id);
|
package/docs/domains/actions.md
CHANGED
|
@@ -136,7 +136,17 @@ Actions using `$effect()` accept a function returning options:
|
|
|
136
136
|
<button onclick={tour.start}>Start Tour</button>
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
Steps can also target elements by CSS **selector** instead of `use:tourStep` — useful when the target lives inside a reusable component:
|
|
140
|
+
|
|
141
|
+
```svelte
|
|
142
|
+
<!-- Component adds a stable data attribute (no tour knowledge) -->
|
|
143
|
+
<button data-tour-id="download">Download</button>
|
|
144
|
+
|
|
145
|
+
<!-- Tour config references it by selector -->
|
|
146
|
+
{ id: "dl-step", title: "Download", content: "...", selector: '[data-tour-id="download"]' }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Features: step navigation (next/prev/skip), selector-based step targeting, persistent state via `storageKey`, custom shell snippets, `confirmSkip` callback, wait-for-element mechanism, Escape key support, step lifecycle callbacks (`onEnter`/`onLeave`).
|
|
140
150
|
|
|
141
151
|
---
|
|
142
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.66.1",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist",
|
|
6
6
|
"!dist/**/*.test.*",
|
|
@@ -35,35 +35,35 @@
|
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/js": "^9.39.4",
|
|
38
|
-
"@marianmeres/random-human-readable": "^1.8.
|
|
38
|
+
"@marianmeres/random-human-readable": "^1.8.3",
|
|
39
39
|
"@sveltejs/adapter-auto": "^4.0.0",
|
|
40
|
-
"@sveltejs/kit": "^2.
|
|
40
|
+
"@sveltejs/kit": "^2.57.1",
|
|
41
41
|
"@sveltejs/package": "^2.5.7",
|
|
42
42
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
43
43
|
"@tailwindcss/cli": "^4.2.2",
|
|
44
44
|
"@tailwindcss/forms": "^0.5.11",
|
|
45
45
|
"@tailwindcss/typography": "^0.5.19",
|
|
46
46
|
"@tailwindcss/vite": "^4.2.2",
|
|
47
|
-
"@types/node": "^25.
|
|
47
|
+
"@types/node": "^25.6.0",
|
|
48
48
|
"dotenv": "^16.6.1",
|
|
49
49
|
"eslint": "^9.39.4",
|
|
50
50
|
"globals": "^16.5.0",
|
|
51
|
-
"prettier": "^3.8.
|
|
51
|
+
"prettier": "^3.8.2",
|
|
52
52
|
"prettier-plugin-svelte": "^3.5.1",
|
|
53
53
|
"publint": "^0.3.18",
|
|
54
|
-
"svelte": "^5.55.
|
|
55
|
-
"svelte-check": "^4.4.
|
|
54
|
+
"svelte": "^5.55.3",
|
|
55
|
+
"svelte-check": "^4.4.6",
|
|
56
56
|
"tailwindcss": "^4.2.2",
|
|
57
57
|
"tsx": "^4.21.0",
|
|
58
58
|
"typescript": "^5.9.3",
|
|
59
|
-
"typescript-eslint": "^8.
|
|
60
|
-
"vite": "^7.3.
|
|
59
|
+
"typescript-eslint": "^8.58.2",
|
|
60
|
+
"vite": "^7.3.2",
|
|
61
61
|
"vitest": "^3.2.4"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@marianmeres/clog": "^3.15.2",
|
|
65
65
|
"@marianmeres/cron": "^1.1.0",
|
|
66
|
-
"@marianmeres/design-tokens": "^1.3.
|
|
66
|
+
"@marianmeres/design-tokens": "^1.3.1",
|
|
67
67
|
"@marianmeres/icons-fns": "^5.0.0",
|
|
68
68
|
"@marianmeres/item-collection": "^1.3.5",
|
|
69
69
|
"@marianmeres/paging-store": "^2.0.2",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@marianmeres/ticker": "^1.16.5",
|
|
72
72
|
"@marianmeres/tree": "^2.2.5",
|
|
73
73
|
"esm-env": "^1.2.2",
|
|
74
|
-
"libphonenumber-js": "^1.12.
|
|
74
|
+
"libphonenumber-js": "^1.12.41",
|
|
75
75
|
"runed": "^0.23.4",
|
|
76
76
|
"tailwind-merge": "^3.5.0"
|
|
77
77
|
},
|