@marianmeres/stuic 3.155.0 → 3.157.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.
package/API.md CHANGED
@@ -1629,10 +1629,12 @@ Multi-step onboarding tour built on the spotlight primitive. Define steps centra
1629
1629
  | `onSkip` | `() => void` | — | Called when tour is skipped |
1630
1630
  | `onStepChange` | `(step, index) => void` | — | Called on every step change |
1631
1631
 
1632
- **Returns:** `{ start(), stop(), next(), prev(), skip(), reset(), reposition(), active, currentStep, currentIndex }`
1632
+ **Returns:** `{ start(), next(), prev(), skip(), reset(), reposition(), active, currentStep, currentIndex, seen }`
1633
1633
 
1634
1634
  `reposition()` forces the active step's spotlight to re-measure its target and re-apply the cutout/anchor. Useful after a layout shift the spotlight's auto-tracking can't observe (or when a step opted out of it).
1635
1635
 
1636
+ `reset()` clears the `storageKey` result so `start()` will run the tour again; `seen` is that flag read back. Every `start()` re-resolves `selector` steps against the current DOM, so a tour whose targets remount between runs (a lazy tab, a route, a keyed block) points at the nodes that are on screen now. Targets registered through `use:tourStep` are left to the action, which already tracks their mount and unmount.
1637
+
1636
1638
  **`TourStepDef`:**
1637
1639
 
1638
1640
  | Field | Type | Description |
@@ -0,0 +1,76 @@
1
+ <script lang="ts">
2
+ import { untrack } from "svelte";
3
+ import { createTour, tourStep } from "./onboarding.svelte.js";
4
+
5
+ let {
6
+ /** Register the target through `use:tourStep` instead of a `selector`. */
7
+ useAction = false,
8
+ /** Re-keys the target, i.e. destroys the node and mounts a fresh one at
9
+ * different coordinates — what a lazy tab or a keyed block does between
10
+ * two runs of the same tour. */
11
+ swapped = false,
12
+ storageKey = undefined,
13
+ }: {
14
+ useAction?: boolean;
15
+ swapped?: boolean;
16
+ storageKey?: string;
17
+ } = $props();
18
+
19
+ const tour = createTour({
20
+ steps: [
21
+ {
22
+ id: "one",
23
+ title: "Step one",
24
+ content: "the only step",
25
+ position: "bottom",
26
+ padding: 0,
27
+ borderRadius: 0,
28
+ // The whole point of the two modes: an action-registered step has
29
+ // no selector to be re-resolved from. untracked for the same reason
30
+ // as `storageKey` below.
31
+ selector: untrack(() => (useAction ? undefined : "[data-testid='target']")),
32
+ },
33
+ ],
34
+ // Short: the action test asserts a step is NOT skipped, and a skip costs
35
+ // this whole wait before it can be observed.
36
+ waitForElement: 300,
37
+ // untrack: read once at init, which is when `createTour` needs it — a
38
+ // tracked read here would be a reactivity warning for a prop that never
39
+ // changes after mount.
40
+ storageKey: untrack(() => storageKey),
41
+ storage: "session",
42
+ showSteps: false,
43
+ });
44
+ </script>
45
+
46
+ <!-- Above the spotlight backdrop (z-index 50), which would otherwise swallow
47
+ every click the test makes while a tour is running. -->
48
+ <div style="position: relative; z-index: 100;">
49
+ <button data-testid="start" onclick={() => tour.start()}>start</button>
50
+ <button data-testid="reset" onclick={() => tour.reset()}>reset</button>
51
+ <button data-testid="skip" onclick={() => void tour.skip()}>skip</button>
52
+ <div data-testid="active">{tour.active ? "yes" : "no"}</div>
53
+ </div>
54
+
55
+ {#key swapped}
56
+ {#if useAction}
57
+ <div
58
+ data-testid="target"
59
+ use:tourStep={[tour, "one"]}
60
+ style="position: absolute; left: {swapped ? 160 : 40}px; top: {swapped
61
+ ? 130
62
+ : 30}px; width: 60px; height: 20px;"
63
+ >
64
+ t
65
+ </div>
66
+ {:else}
67
+ <div
68
+ data-testid="target"
69
+ style="position: absolute; left: {swapped ? 160 : 40}px; top: {swapped
70
+ ? 130
71
+ : 30}px; width: 60px; height: 20px;"
72
+ >
73
+ t
74
+ </div>
75
+ {/if}
76
+ {/key}
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ useAction?: boolean;
3
+ swapped?: boolean;
4
+ storageKey?: string;
5
+ };
6
+ declare const Onboarding: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type Onboarding = ReturnType<typeof Onboarding>;
8
+ export default Onboarding;
@@ -225,12 +225,51 @@ export function createTour(options) {
225
225
  store?.set(options.storageKey, "completed");
226
226
  options.onEnd?.();
227
227
  }
228
+ /**
229
+ * Drop every SELECTOR-resolved target, so the next run resolves them against
230
+ * the DOM as it is now.
231
+ *
232
+ * The registry is a cache and `advanceTo` only queries the DOM for a step it
233
+ * does not already hold, so without this a second run re-uses the first
234
+ * run's nodes. For a tour whose targets all sit in one stable subtree that
235
+ * is harmless. For one that crosses a lazy tab, a route or a keyed block it
236
+ * is fatal, and silently: a detached node's `getBoundingClientRect()` is all
237
+ * zeroes, so the cutout collapses to 0x0 in the top-left corner and the
238
+ * annotation follows it there. Nothing throws and nothing warns.
239
+ *
240
+ * Steps registered through `use:tourStep` are deliberately SPARED. The
241
+ * action owns their lifetime — it registers on mount and unregisters on
242
+ * destroy — so their entries are never stale, and such a step has no
243
+ * `selector` to be re-resolved from: dropping one whose element is still on
244
+ * screen would leave `advanceTo` nothing to find, and it would skip the step
245
+ * after waiting `waitForElement` ms for a registration that already
246
+ * happened. `actionRegistered` is exactly the set to spare.
247
+ */
248
+ function clearResolvedTargets() {
249
+ for (const id of registry.keys()) {
250
+ if (!actionRegistered.has(id))
251
+ registry.delete(id);
252
+ }
253
+ }
228
254
  // -- Public API ---------------------------------------------------------------------
255
+ /**
256
+ * Begin the tour at its first available step.
257
+ *
258
+ * No-op while a tour is already running, and — if `storageKey` is set — for
259
+ * anyone who has already completed or skipped it. Use {@link reset} to
260
+ * clear that.
261
+ *
262
+ * Resolved targets are cleared HERE rather than in `reset()`, because the
263
+ * staleness is per-RUN and not per-persisted-flag: a tour with no
264
+ * `storageKey` is re-startable without ever calling `reset()`, and would
265
+ * otherwise walk the previous run's nodes. See {@link clearResolvedTargets}.
266
+ */
229
267
  function start() {
230
268
  if (active)
231
269
  return;
232
270
  if (store && store.has(options.storageKey))
233
271
  return;
272
+ clearResolvedTargets();
234
273
  options.onStart?.();
235
274
  advanceTo(0);
236
275
  }
@@ -248,6 +287,13 @@ export function createTour(options) {
248
287
  store?.set(options.storageKey, "skipped");
249
288
  options.onSkip?.();
250
289
  }
290
+ /**
291
+ * Forget that this tour was completed or skipped, so {@link start} runs it
292
+ * again. A no-op without `storageKey` — there is nothing persisted to clear.
293
+ *
294
+ * It does NOT touch the resolved-target cache; `start()` does, on every run.
295
+ * See {@link clearResolvedTargets} for why that is the right boundary.
296
+ */
251
297
  function reset() {
252
298
  store?.remove(options.storageKey);
253
299
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.155.0",
3
+ "version": "3.157.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -128,12 +128,12 @@
128
128
  }
129
129
  },
130
130
  "devDependencies": {
131
- "@codemirror/commands": "^6.10.4",
131
+ "@codemirror/commands": "^6.11.0",
132
132
  "@codemirror/lang-markdown": "^6.5.2",
133
133
  "@codemirror/language": "^6.12.4",
134
134
  "@codemirror/language-data": "^6.5.2",
135
135
  "@codemirror/state": "^6.7.1",
136
- "@codemirror/view": "^6.43.8",
136
+ "@codemirror/view": "^6.43.9",
137
137
  "@eslint/js": "^9.39.5",
138
138
  "@marianmeres/random-human-readable": "^1.10.2",
139
139
  "@milkdown/core": "^7.22.1",
@@ -146,7 +146,7 @@
146
146
  "@milkdown/transformer": "^7.22.1",
147
147
  "@milkdown/utils": "^7.22.1",
148
148
  "@sveltejs/adapter-auto": "^4.0.0",
149
- "@sveltejs/kit": "^2.70.2",
149
+ "@sveltejs/kit": "^2.70.3",
150
150
  "@sveltejs/package": "^2.5.8",
151
151
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
152
152
  "@tailwindcss/cli": "^4.3.3",
@@ -154,14 +154,14 @@
154
154
  "@tailwindcss/typography": "^0.5.20",
155
155
  "@tailwindcss/vite": "^4.3.3",
156
156
  "@types/node": "^25.9.5",
157
- "@vitest/browser-playwright": "^4.1.10",
157
+ "@vitest/browser-playwright": "^4.1.11",
158
158
  "dotenv": "^16.6.1",
159
159
  "eslint": "^9.39.5",
160
160
  "globals": "^16.5.0",
161
161
  "playwright": "^1.62.1",
162
162
  "prettier": "^3.9.6",
163
163
  "prettier-plugin-svelte": "^3.5.2",
164
- "publint": "^0.3.23",
164
+ "publint": "^0.3.24",
165
165
  "svelte": "^5.56.9",
166
166
  "svelte-check": "^4.7.6",
167
167
  "tailwindcss": "^4.3.3",
@@ -169,7 +169,7 @@
169
169
  "typescript": "^5.9.3",
170
170
  "typescript-eslint": "^8.67.0",
171
171
  "vite": "^7.3.6",
172
- "vitest": "^4.1.10",
172
+ "vitest": "^4.1.11",
173
173
  "vitest-browser-svelte": "^2.2.1"
174
174
  },
175
175
  "dependencies": {
@@ -177,13 +177,13 @@
177
177
  "@marianmeres/countries": "^1.1.0",
178
178
  "@marianmeres/cron-parser": "^1.0.1",
179
179
  "@marianmeres/design-tokens": "^1.17.0",
180
- "@marianmeres/icons-fns": "^5.0.0",
180
+ "@marianmeres/icons-fns": "^6.0.0",
181
181
  "@marianmeres/item-collection": "^1.4.2",
182
182
  "@marianmeres/paging-store": "^2.1.1",
183
183
  "@marianmeres/parse-boolean": "^2.1.0",
184
184
  "@marianmeres/ticker": "^1.17.1",
185
185
  "@marianmeres/tree": "^2.3.0",
186
- "libphonenumber-js": "^1.13.10",
186
+ "libphonenumber-js": "^1.13.11",
187
187
  "runed": "^0.23.4",
188
188
  "tailwind-merge": "^3.6.0"
189
189
  }