steppy-dev 0.3.0__py3-none-any.whl

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.
Files changed (177) hide show
  1. steppy/CHANGELOG.md +862 -0
  2. steppy/__init__.py +9 -0
  3. steppy/__main__.py +13 -0
  4. steppy/_cli_common.py +183 -0
  5. steppy/_docker/Dockerfile.installed +53 -0
  6. steppy/_parallel/__init__.py +7 -0
  7. steppy/_parallel/compose.py +349 -0
  8. steppy/_parallel/event_bus.py +418 -0
  9. steppy/_parallel/failures.py +267 -0
  10. steppy/_parallel/fingerprint.py +141 -0
  11. steppy/_parallel/image.py +380 -0
  12. steppy/_parallel/lastfailed.py +125 -0
  13. steppy/_parallel/lifecycle.py +90 -0
  14. steppy/_parallel/preauth.py +109 -0
  15. steppy/_parallel/progress.py +101 -0
  16. steppy/_parallel/queue.py +385 -0
  17. steppy/_parallel/renderer.py +489 -0
  18. steppy/_parallel/retry.py +316 -0
  19. steppy/_player_bundle/THIRD_PARTY_LICENSES.txt +158 -0
  20. steppy/_player_bundle/__init__.py +100 -0
  21. steppy/_player_bundle/steppy-cursor-legacy.js +2 -0
  22. steppy/_player_bundle/steppy-cursor-legacy.js.map +1 -0
  23. steppy/_player_bundle/steppy-player.js +5419 -0
  24. steppy/_player_bundle/steppy-player.js.map +1 -0
  25. steppy/_player_bundle/steppy-video.js +472 -0
  26. steppy/_player_bundle/steppy-video.js.map +1 -0
  27. steppy/_templating.py +117 -0
  28. steppy/assertions.py +251 -0
  29. steppy/auth.py +493 -0
  30. steppy/build.py +2798 -0
  31. steppy/build_models.py +669 -0
  32. steppy/buildid.py +134 -0
  33. steppy/bundle/__init__.py +47 -0
  34. steppy/bundle/archive.py +239 -0
  35. steppy/bundle/classify.py +329 -0
  36. steppy/bundle/create.py +490 -0
  37. steppy/bundle/hashing.py +28 -0
  38. steppy/bundle/manifest.py +334 -0
  39. steppy/bundle/results.py +546 -0
  40. steppy/bundle_cli.py +455 -0
  41. steppy/captures.py +207 -0
  42. steppy/cli.py +2489 -0
  43. steppy/cloud/__init__.py +58 -0
  44. steppy/cloud/client.py +267 -0
  45. steppy/cloud/upload.py +497 -0
  46. steppy/compat.py +109 -0
  47. steppy/config.py +231 -0
  48. steppy/config_schema.py +87 -0
  49. steppy/contracts/__init__.py +54 -0
  50. steppy/contracts/bundle.py +215 -0
  51. steppy/contracts/report.py +346 -0
  52. steppy/contracts/tours.py +505 -0
  53. steppy/coordinator.py +1352 -0
  54. steppy/coverage/__init__.py +9 -0
  55. steppy/coverage/gaps.py +190 -0
  56. steppy/coverage/manifest.py +193 -0
  57. steppy/coverage/merge.py +363 -0
  58. steppy/coverage/normalize.py +174 -0
  59. steppy/coverage/report.py +178 -0
  60. steppy/coverage/sourcemap.py +219 -0
  61. steppy/coverage/store.py +473 -0
  62. steppy/coverage/triage.py +151 -0
  63. steppy/coverage_cli.py +265 -0
  64. steppy/durations.py +48 -0
  65. steppy/exclusive.py +178 -0
  66. steppy/flaky_cli.py +169 -0
  67. steppy/generator.py +633 -0
  68. steppy/guide.py +1467 -0
  69. steppy/history.py +352 -0
  70. steppy/init_cli.py +330 -0
  71. steppy/locales.py +282 -0
  72. steppy/migrate/__init__.py +35 -0
  73. steppy/migrate/apply.py +194 -0
  74. steppy/migrate/plan.py +199 -0
  75. steppy/migrate/rules.py +465 -0
  76. steppy/migrate/verify.py +163 -0
  77. steppy/migrate_cli.py +152 -0
  78. steppy/models.py +2539 -0
  79. steppy/naming.py +37 -0
  80. steppy/parser.py +475 -0
  81. steppy/pdf.py +193 -0
  82. steppy/presets.py +284 -0
  83. steppy/record_cli.py +796 -0
  84. steppy/report/__init__.py +37 -0
  85. steppy/report/assets.py +197 -0
  86. steppy/report/build.py +261 -0
  87. steppy/report/coverage_source.py +277 -0
  88. steppy/report/model.py +423 -0
  89. steppy/report/render.py +303 -0
  90. steppy/report/source.py +238 -0
  91. steppy/report_cli.py +173 -0
  92. steppy/runmeta.py +114 -0
  93. steppy/selection.py +183 -0
  94. steppy/sitemap.py +205 -0
  95. steppy/skills/steppy-app-tours/SKILL.md +448 -0
  96. steppy/skills/steppy-ci-report/SKILL.md +567 -0
  97. steppy/skills/steppy-scenario-author/SKILL.md +2176 -0
  98. steppy/skills/steppy-template/SKILL.md +676 -0
  99. steppy/templates/_steppy_events.py +291 -0
  100. steppy/templates/_steppy_tours.py +404 -0
  101. steppy/templates/angular/component-interactive.ts.j2 +109 -0
  102. steppy/templates/angular/component-video.ts.j2 +112 -0
  103. steppy/templates/angular/component.ts.j2 +229 -0
  104. steppy/templates/angular/content.ts.j2 +31 -0
  105. steppy/templates/angular/gitignore.j2 +3 -0
  106. steppy/templates/angular/group.component.ts.j2 +39 -0
  107. steppy/templates/angular/onboarding.component.ts.j2 +43 -0
  108. steppy/templates/angular/routes.ts.j2 +34 -0
  109. steppy/templates/ci/github-actions.yml +284 -0
  110. steppy/templates/ci/gitlab-ci.yml +178 -0
  111. steppy/templates/conftest.py.j2 +2405 -0
  112. steppy/templates/guide.book.html.j2 +145 -0
  113. steppy/templates/guide.fragment.html.j2 +257 -0
  114. steppy/templates/guide.html.j2 +301 -0
  115. steppy/templates/guide.md.j2 +18 -0
  116. steppy/templates/init/config.yaml +124 -0
  117. steppy/templates/init/credentials.yaml.example +30 -0
  118. steppy/templates/init/scenarios/example--first-flow.md +53 -0
  119. steppy/templates/report/_head.html.j2 +14 -0
  120. steppy/templates/report/_mark.html.j2 +3 -0
  121. steppy/templates/report/coverage.html.j2 +124 -0
  122. steppy/templates/report/index.html.j2 +203 -0
  123. steppy/templates/report/report.css +401 -0
  124. steppy/templates/report/scenario.html.j2 +137 -0
  125. steppy/templates/static/_headers.j2 +30 -0
  126. steppy/templates/static/_mode_links.html.j2 +22 -0
  127. steppy/templates/static/_seo_head.html.j2 +37 -0
  128. steppy/templates/static/cursor.js.j2 +10 -0
  129. steppy/templates/static/group.html.j2 +78 -0
  130. steppy/templates/static/guides.css +678 -0
  131. steppy/templates/static/index.html.j2 +79 -0
  132. steppy/templates/static/interactive.html.j2 +100 -0
  133. steppy/templates/static/onboarding.html.j2 +64 -0
  134. steppy/templates/static/page.html.j2 +145 -0
  135. steppy/templates/static/robots.txt.j2 +7 -0
  136. steppy/templates/static/video.html.j2 +135 -0
  137. steppy/templates/test_scenario.py.j2 +767 -0
  138. steppy/testids.py +166 -0
  139. steppy/totp.py +26 -0
  140. steppy/tour_spec.py +163 -0
  141. steppy/tours/__init__.py +22 -0
  142. steppy/tours/actions.py +402 -0
  143. steppy/tours/compiler.py +144 -0
  144. steppy/tours/diagnostics.py +137 -0
  145. steppy/tours/emit.py +239 -0
  146. steppy/tours/ids.py +40 -0
  147. steppy/tours/inference.py +399 -0
  148. steppy/tours/model.py +120 -0
  149. steppy/tours/paths.py +126 -0
  150. steppy/tours/pipeline.py +277 -0
  151. steppy/tours/publish.py +124 -0
  152. steppy/tours/replay.py +234 -0
  153. steppy/tours/routes.py +175 -0
  154. steppy/tours/settings.py +164 -0
  155. steppy/tours/text.py +237 -0
  156. steppy/tours_cli.py +219 -0
  157. steppy/tts/__init__.py +280 -0
  158. steppy/tts/cache.py +185 -0
  159. steppy/tts/presets.py +125 -0
  160. steppy/tts/providers/__init__.py +6 -0
  161. steppy/tts/providers/http.py +184 -0
  162. steppy/tts/providers/local.py +207 -0
  163. steppy/tts/providers/null.py +28 -0
  164. steppy/tts/registry.py +201 -0
  165. steppy/tts/track.py +158 -0
  166. steppy/tts/types.py +94 -0
  167. steppy/update_cli.py +460 -0
  168. steppy/video.py +730 -0
  169. steppy/video_cache.py +239 -0
  170. steppy/video_cli.py +271 -0
  171. steppy/worker.py +301 -0
  172. steppy_dev-0.3.0.dist-info/METADATA +1335 -0
  173. steppy_dev-0.3.0.dist-info/RECORD +177 -0
  174. steppy_dev-0.3.0.dist-info/WHEEL +4 -0
  175. steppy_dev-0.3.0.dist-info/entry_points.txt +2 -0
  176. steppy_dev-0.3.0.dist-info/licenses/LICENSE +202 -0
  177. steppy_dev-0.3.0.dist-info/licenses/NOTICE +13 -0
@@ -0,0 +1,316 @@
1
+ """Scenario-level retry pass for parallel runs.
2
+
3
+ After the main queue drains, re-run the scenarios that failed — at reduced
4
+ concurrency, in the **already-running** stacks. A scenario that fails under N
5
+ workers and then passes alone is FLAKY (contention), not broken; one that fails
6
+ both times is a real failure. That single distinction is what turns an
7
+ 18-failure summary into "3 real failures, 15 flaky".
8
+
9
+ Why this is different from ``report.retries`` (pytest-rerunfailures): that
10
+ reruns the test in-process, immediately — same worker, same moment, same
11
+ contention. It catches per-action timing flakes and nothing else. This pass
12
+ waits for the contention to be *gone* before retrying, which is the only way to
13
+ attribute a failure to it.
14
+
15
+ Mechanism — the cheap part is that there barely is one:
16
+
17
+ * ``worker.py`` resolves its whole world from ``STEPPY_COORD_DIR``, so pointing
18
+ attempt *n* at ``<tmp>/retry-<n>/`` gives it a private queue/claims/results
19
+ tree. **Neither ``worker.py`` nor ``queue.py`` needs any change**, and
20
+ attempt 1's evidence stays intact on disk beside attempt 2's (which is what
21
+ lets ``failures.py`` copy the right ``pytest.log`` afterwards).
22
+ * The app services are left running. We recreate only ``steppy-worker``, with
23
+ ``--no-deps``, so the database, the network and any completed ``steppy-preauth``
24
+ survive untouched and the retry starts in seconds rather than minutes.
25
+
26
+ Two things that look optional but are not:
27
+
28
+ * A **fresh EventBus** per pass. ``EventBus._tests`` is keyed
29
+ ``(worker_id, nodeid)`` and ``test-start`` overwrites the record in place,
30
+ while ``_counts`` only ever increments — feeding a second attempt into the
31
+ same bus would both lose attempt 1's outcome and double-count the totals.
32
+ * **Fresh log tailers.** ``docker compose logs -f`` is bound to the container
33
+ that existed when it started; recreating ``steppy-worker`` orphans it.
34
+
35
+ Lazy-imported from :mod:`steppy.coordinator`, like every other ``_parallel``
36
+ module, so the single-worker path stays clean
37
+ (``tests/test_no_parallel_import_on_default_path.py``).
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import subprocess
43
+ from concurrent.futures import ThreadPoolExecutor, as_completed
44
+ from dataclasses import dataclass, field
45
+ from pathlib import Path
46
+ from typing import Callable, Optional
47
+
48
+ from .compose import render_override
49
+ from .event_bus import EventBus, RawLogChannel
50
+ from .progress import WorkerLogTailer
51
+ from .queue import Queue, ScenarioResult
52
+
53
+
54
+ @dataclass
55
+ class PassResult:
56
+ """One attempt's outcome. ``attempt`` is 1 for the main pass."""
57
+
58
+ attempt: int
59
+ coord_root: Path
60
+ results: list[ScenarioResult]
61
+ orphans: dict[str, list[str]] = field(default_factory=dict)
62
+ bus: Optional[EventBus] = None
63
+ # Scenarios that were queued and never claimed by anybody — every worker died
64
+ # with work left. Deliberately NOT part of ``failed_slugs``: see below.
65
+ never_started: set[str] = field(default_factory=set)
66
+
67
+ @property
68
+ def failed_slugs(self) -> set[str]:
69
+ """Scenarios this attempt did not get a clean result for — non-zero
70
+ exit codes plus orphans (claimed, never reported)."""
71
+ out = {r.slug for r in self.results if r.exit_code != 0}
72
+ for slugs in self.orphans.values():
73
+ out.update(slugs)
74
+ return out
75
+
76
+ @property
77
+ def pending_slugs(self) -> set[str]:
78
+ """What the next attempt should run: the failures **plus** anything that
79
+ never started.
80
+
81
+ The split matters because :func:`classify` builds ``ever_failed`` from
82
+ ``failed_slugs``. A scenario that never ran and then passed on retry is not
83
+ flaky, and marking it so would poison the append-only
84
+ ``.steppy/history.jsonl`` that ``steppy flaky`` ranks from — permanently,
85
+ and for a scenario that is not even faulty. An exclusion group makes that
86
+ acute: one worker death strands every unstarted member of its batch at
87
+ once, not just the one in flight.
88
+ """
89
+ return self.failed_slugs | self.never_started
90
+
91
+ @property
92
+ def passed_slugs(self) -> set[str]:
93
+ return {r.slug for r in self.results if r.exit_code == 0}
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class Verdict:
98
+ """Final per-scenario classification across every attempt."""
99
+
100
+ passed: list[str]
101
+ flaky: list[str] # failed at least once, passed in the end
102
+ failed: list[str] # never passed
103
+ attempts: dict[str, int] # slug -> how many attempts it took/consumed
104
+
105
+ @property
106
+ def any_real_failure(self) -> bool:
107
+ return bool(self.failed)
108
+
109
+
110
+ def classify(passes: list[PassResult]) -> Verdict:
111
+ """Fold the per-attempt results into passed / flaky / failed.
112
+
113
+ Later attempts win: a scenario is FLAKY when some attempt failed it and a
114
+ later one passed it, FAILED when no attempt ever passed it. Scenarios that
115
+ only ever passed are plain passes.
116
+
117
+ Orphans count as failures for the attempt they happened in, so a worker that
118
+ died mid-scenario is retried like any other failure instead of being a hard
119
+ loss (the v0.1 ``queue.orphaned_running_tasks`` behaviour).
120
+ """
121
+ final_outcome: dict[str, bool] = {}
122
+ ever_failed: set[str] = set()
123
+ attempts: dict[str, int] = {}
124
+
125
+ for p in passes:
126
+ failed = p.failed_slugs
127
+ for slug in failed:
128
+ ever_failed.add(slug)
129
+ final_outcome[slug] = False
130
+ attempts[slug] = p.attempt
131
+ for slug in p.passed_slugs:
132
+ final_outcome[slug] = True
133
+ attempts[slug] = p.attempt
134
+
135
+ passed, flaky, failed_out = [], [], []
136
+ for slug in sorted(final_outcome):
137
+ if not final_outcome[slug]:
138
+ failed_out.append(slug)
139
+ elif slug in ever_failed:
140
+ flaky.append(slug)
141
+ else:
142
+ passed.append(slug)
143
+ return Verdict(passed=passed, flaky=flaky, failed=failed_out, attempts=attempts)
144
+
145
+
146
+ def log_roots_by_slug(passes: list[PassResult]) -> dict[str, Path]:
147
+ """Map each scenario to the coord root of its FINAL attempt.
148
+
149
+ ``failures.persist_failure_artifacts`` reads ``<root>/logs/<worker>/<slug>.log``;
150
+ without this it would look in the main pass's dir and silently copy a stale
151
+ (or missing) log for anything the retry pass touched.
152
+ """
153
+ roots: dict[str, Path] = {}
154
+ for p in passes: # later passes overwrite earlier ones
155
+ for slug in p.failed_slugs | p.passed_slugs:
156
+ roots[slug] = p.coord_root
157
+ return roots
158
+
159
+
160
+ def _recreate_worker(
161
+ *,
162
+ project: str,
163
+ compose_file: Path,
164
+ override: Path,
165
+ ) -> tuple[int, Optional[str]]:
166
+ """Replace one project's ``steppy-worker`` container with a fresh one bound
167
+ to the retry override. ``--no-deps`` is what keeps the app services (and the
168
+ already-completed preauth) alive, so only the worker restarts."""
169
+ cmd = [
170
+ "docker",
171
+ "compose",
172
+ "-p",
173
+ project,
174
+ "-f",
175
+ str(compose_file),
176
+ "-f",
177
+ str(override),
178
+ "up",
179
+ "-d",
180
+ "--no-deps",
181
+ "--force-recreate",
182
+ "steppy-worker",
183
+ ]
184
+ result = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
185
+ if result.returncode != 0:
186
+ return result.returncode, f"{project}: {result.stderr.strip()}"
187
+ return 0, None
188
+
189
+
190
+ def run_retry_pass(
191
+ *,
192
+ attempt: int,
193
+ slugs: list[str],
194
+ projects: list[str],
195
+ batches: Optional[dict[str, list[str]]] = None,
196
+ compose_file: Path,
197
+ tmp_dir: Path,
198
+ override_kwargs: dict,
199
+ console,
200
+ verbose: bool,
201
+ resolve_container_ids: Callable[[list[str]], dict[str, str]],
202
+ wait_for_workers: Callable[..., list[int]],
203
+ register_teardown: Callable[[Callable[[], None]], None],
204
+ ) -> Optional[PassResult]:
205
+ """Run one retry attempt over ``slugs``. Returns None if it couldn't start.
206
+
207
+ ``projects`` is the subset of stacks to use (the caller slices it down to
208
+ ``parallel.retry.workers``). The unused stacks simply idle — they are torn
209
+ down with the rest at the end of the run.
210
+
211
+ ``override_kwargs`` is the exact kwargs dict the main pass handed to
212
+ :func:`render_override`; we re-render with ``coord_subdir`` pointed at this
213
+ attempt's private queue so a drifted main-pass setting can never silently
214
+ fail to apply to the retry.
215
+
216
+ ``slugs`` is a list of TASK IDS and ``batches`` maps any of them that is an
217
+ exclusion group to its members — re-derived by the caller for this attempt's
218
+ pending set, never inherited, since a retry usually re-runs only part of a
219
+ group. Without it two failed members of one group would run concurrently
220
+ whenever ``parallel.retry.workers`` is above its default of 1.
221
+
222
+ The coordinator's ``_resolve_worker_container_ids`` / ``_wait_for_workers``
223
+ / lifecycle hooks are injected rather than imported to keep this module
224
+ free of a circular dependency on ``coordinator``.
225
+ """
226
+ coord_subdir = f"retry-{attempt}"
227
+ coord_root = tmp_dir / coord_subdir
228
+ queue = Queue(root=coord_root)
229
+ if batches:
230
+ queue.seed(slugs, batches=batches)
231
+ else:
232
+ queue.seed(slugs)
233
+
234
+ console.print()
235
+ console.rule(f"[bold yellow]Retry pass {attempt}[/bold yellow]")
236
+ console.print(
237
+ f"[dim]re-running {len(slugs)} failed scenario(s) across {len(projects)} worker(s) "
238
+ f"— reduced concurrency isolates contention flakes[/dim]"
239
+ )
240
+
241
+ # Re-render one override per participating stack, pointed at this attempt's
242
+ # queue. Worker index must match the project's original index so worker ids
243
+ # stay stable in the summary.
244
+ overrides: list[Path] = []
245
+ for idx, _project in enumerate(projects, start=1):
246
+ kwargs = dict(override_kwargs)
247
+ kwargs["worker_index"] = idx
248
+ kwargs["coord_subdir"] = coord_subdir
249
+ if kwargs.get("auth_mount_by_worker"):
250
+ kwargs["auth_mount"] = kwargs["auth_mount_by_worker"][idx - 1]
251
+ kwargs.pop("auth_mount_by_worker", None)
252
+ path = tmp_dir / f"override-retry{attempt}-w{idx}.yml"
253
+ path.write_text(render_override(**kwargs), encoding="utf-8")
254
+ overrides.append(path)
255
+
256
+ failures: list[str] = []
257
+ with ThreadPoolExecutor(max_workers=max(1, len(projects))) as pool:
258
+ futures = [
259
+ pool.submit(_recreate_worker, project=p, compose_file=compose_file, override=o)
260
+ for p, o in zip(projects, overrides)
261
+ ]
262
+ for fut in as_completed(futures):
263
+ code, err = fut.result()
264
+ if code != 0:
265
+ failures.append(err or f"unknown error (exit {code})")
266
+ if failures:
267
+ console.print(f"[yellow]WARNING[/yellow] retry pass could not start: {'; '.join(failures)}")
268
+ return None
269
+
270
+ # Fresh bus per pass — see the module docstring; reusing the main pass's bus
271
+ # would overwrite attempt 1's records and double-count the totals.
272
+ raw_channel = RawLogChannel(console=console, verbose=verbose, log_dir=coord_root)
273
+ bus = EventBus(raw_line_sink=raw_channel)
274
+ register_teardown(raw_channel.close)
275
+ for idx in range(1, len(projects) + 1):
276
+ bus.register_worker(f"w{idx}")
277
+
278
+ # Fresh tailers too — the main pass's `compose logs -f` died with the
279
+ # container it was following.
280
+ for idx, project in enumerate(projects, start=1):
281
+ tailer = WorkerLogTailer(
282
+ project=project,
283
+ worker_id=f"w{idx}",
284
+ bus=bus,
285
+ app_log_file=tmp_dir / f"w{idx}-retry{attempt}-stack.log",
286
+ )
287
+ tailer.start()
288
+ register_teardown(tailer.stop)
289
+
290
+ try:
291
+ container_ids = resolve_container_ids(projects)
292
+ except Exception as e: # noqa: BLE001 — a retry that can't start is not fatal
293
+ console.print(f"[yellow]WARNING[/yellow] retry pass could not resolve workers: {e}")
294
+ return None
295
+
296
+ worker_exits = wait_for_workers(container_ids, console)
297
+ for idx, code in enumerate(worker_exits, start=1):
298
+ bus.mark_worker_finished(f"w{idx}", died=code != 0)
299
+
300
+ return PassResult(
301
+ attempt=attempt,
302
+ coord_root=coord_root,
303
+ results=queue.collect_results(),
304
+ orphans=queue.orphaned_running_tasks(),
305
+ bus=bus,
306
+ never_started=set(queue.unclaimed_slugs()),
307
+ )
308
+
309
+
310
+ __all__ = [
311
+ "PassResult",
312
+ "Verdict",
313
+ "classify",
314
+ "log_roots_by_slug",
315
+ "run_retry_pass",
316
+ ]
@@ -0,0 +1,158 @@
1
+ Third-party software bundled in the Steppy player bundles
2
+ (steppy-player.js, steppy-video.js). steppy-cursor-legacy.js bundles none.
3
+
4
+ ================================================================================
5
+ lit 3.3.3
6
+ ================================================================================
7
+
8
+ BSD 3-Clause License
9
+
10
+ Copyright (c) 2017 Google LLC. All rights reserved.
11
+
12
+ Redistribution and use in source and binary forms, with or without
13
+ modification, are permitted provided that the following conditions are met:
14
+
15
+ 1. Redistributions of source code must retain the above copyright notice, this
16
+ list of conditions and the following disclaimer.
17
+
18
+ 2. Redistributions in binary form must reproduce the above copyright notice,
19
+ this list of conditions and the following disclaimer in the documentation
20
+ and/or other materials provided with the distribution.
21
+
22
+ 3. Neither the name of the copyright holder nor the names of its
23
+ contributors may be used to endorse or promote products derived from
24
+ this software without specific prior written permission.
25
+
26
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
27
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
30
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
31
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
32
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
33
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
34
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
35
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ ================================================================================
37
+ lit-html 3.3.3
38
+ ================================================================================
39
+
40
+ BSD 3-Clause License
41
+
42
+ Copyright (c) 2017 Google LLC. All rights reserved.
43
+
44
+ Redistribution and use in source and binary forms, with or without
45
+ modification, are permitted provided that the following conditions are met:
46
+
47
+ 1. Redistributions of source code must retain the above copyright notice, this
48
+ list of conditions and the following disclaimer.
49
+
50
+ 2. Redistributions in binary form must reproduce the above copyright notice,
51
+ this list of conditions and the following disclaimer in the documentation
52
+ and/or other materials provided with the distribution.
53
+
54
+ 3. Neither the name of the copyright holder nor the names of its
55
+ contributors may be used to endorse or promote products derived from
56
+ this software without specific prior written permission.
57
+
58
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
59
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
60
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
61
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
62
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
63
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
64
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
65
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
66
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
67
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
68
+
69
+ ================================================================================
70
+ lit-element 4.2.2
71
+ ================================================================================
72
+
73
+ BSD 3-Clause License
74
+
75
+ Copyright (c) 2017 Google LLC. All rights reserved.
76
+
77
+ Redistribution and use in source and binary forms, with or without
78
+ modification, are permitted provided that the following conditions are met:
79
+
80
+ 1. Redistributions of source code must retain the above copyright notice, this
81
+ list of conditions and the following disclaimer.
82
+
83
+ 2. Redistributions in binary form must reproduce the above copyright notice,
84
+ this list of conditions and the following disclaimer in the documentation
85
+ and/or other materials provided with the distribution.
86
+
87
+ 3. Neither the name of the copyright holder nor the names of its
88
+ contributors may be used to endorse or promote products derived from
89
+ this software without specific prior written permission.
90
+
91
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
92
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
93
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
94
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
95
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
96
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
97
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
98
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
99
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
100
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
101
+ ================================================================================
102
+ @lit/reactive-element 2.1.2
103
+ ================================================================================
104
+
105
+ BSD 3-Clause License
106
+
107
+ Copyright (c) 2017 Google LLC. All rights reserved.
108
+
109
+ Redistribution and use in source and binary forms, with or without
110
+ modification, are permitted provided that the following conditions are met:
111
+
112
+ 1. Redistributions of source code must retain the above copyright notice, this
113
+ list of conditions and the following disclaimer.
114
+
115
+ 2. Redistributions in binary form must reproduce the above copyright notice,
116
+ this list of conditions and the following disclaimer in the documentation
117
+ and/or other materials provided with the distribution.
118
+
119
+ 3. Neither the name of the copyright holder nor the names of its
120
+ contributors may be used to endorse or promote products derived from
121
+ this software without specific prior written permission.
122
+
123
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
124
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
125
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
126
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
127
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
128
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
129
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
130
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
131
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
132
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
133
+ ================================================================================
134
+ zod 3.25.76
135
+ ================================================================================
136
+
137
+ MIT License
138
+
139
+ Copyright (c) 2025 Colin McDonnell
140
+
141
+ Permission is hereby granted, free of charge, to any person obtaining a copy
142
+ of this software and associated documentation files (the "Software"), to deal
143
+ in the Software without restriction, including without limitation the rights
144
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
145
+ copies of the Software, and to permit persons to whom the Software is
146
+ furnished to do so, subject to the following conditions:
147
+
148
+ The above copyright notice and this permission notice shall be included in all
149
+ copies or substantial portions of the Software.
150
+
151
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
152
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
153
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
154
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
155
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
156
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
157
+ SOFTWARE.
158
+
@@ -0,0 +1,100 @@
1
+ """Vendored ``@steppy/player`` build artifacts.
2
+
3
+ The cursor IIFE bundle (legacy stepbystep cursor animation), the
4
+ ``<steppy-guide>`` ESM bundle (interactive player) and the video
5
+ renderer IIFE live here as pre-built artifacts, **committed to git** so
6
+ ``pip install git+…`` needs no Node toolchain. After changing
7
+ ``packages/player/src/``, run ``npm --prefix packages/player run build``
8
+ and copy those three bundles (with their maps) back here; CI compares them
9
+ byte for byte, and the release pipeline rebuilds them for the wheel.
10
+
11
+ The fourth bundle the player build produces, the in-app tour overlay
12
+ (``steppy-tour.js``), is deliberately not vendored: it ships to apps
13
+ through npm, and no Python code serves it.
14
+ """
15
+
16
+ import importlib.resources
17
+ from functools import lru_cache
18
+ from pathlib import Path
19
+
20
+
21
+ class CursorBundleMissingError(FileNotFoundError):
22
+ """Raised when the legacy cursor IIFE bundle hasn't been built yet."""
23
+
24
+
25
+ _BUILD_HINT = "Run `npm --prefix packages/player install && npm --prefix packages/player run build` to populate it."
26
+
27
+
28
+ @lru_cache(maxsize=1)
29
+ def get_legacy_cursor_iife() -> str:
30
+ """Return the bundled cursor IIFE JS as a string.
31
+
32
+ Single source of truth for the cursor animation. Both
33
+ ``templates/guide.html.j2`` (standalone HTML guide) and
34
+ ``templates/static/cursor.js.j2`` (build static target) read this
35
+ via the ``cursor_iife`` Jinja context variable.
36
+ """
37
+ pkg = importlib.resources.files("steppy") / "_player_bundle" / "steppy-cursor-legacy.js"
38
+ try:
39
+ return pkg.read_text(encoding="utf-8")
40
+ except (FileNotFoundError, OSError) as exc:
41
+ raise CursorBundleMissingError(
42
+ "Vendored cursor bundle missing at src/steppy/_player_bundle/steppy-cursor-legacy.js. " + _BUILD_HINT
43
+ ) from exc
44
+
45
+
46
+ def get_player_bundle_path() -> Path:
47
+ """Filesystem path of the ESM player bundle (steppy-player.js).
48
+
49
+ Used by ``steppy build`` to copy the bundle next to the emitted
50
+ ``guide.json`` so ``<steppy-guide>`` works on both Angular and static
51
+ targets without a CDN.
52
+ """
53
+ path = bundle_dir() / "steppy-player.js"
54
+ if not path.is_file():
55
+ raise CursorBundleMissingError(f"Vendored player bundle missing at {path}. " + _BUILD_HINT)
56
+ return path
57
+
58
+
59
+ def get_cursor_bundle_path() -> Path:
60
+ """Filesystem path of the legacy cursor IIFE bundle (steppy-cursor-legacy.js).
61
+
62
+ Used by ``steppy build`` (Angular target) to copy the bundle into the
63
+ assets dir as ``steppy-cursor.js``. The generated per-guide component loads
64
+ it once and calls ``window.__STEPPY_CURSOR__.mount(...)`` to animate the
65
+ stepbystep cursor — the static target inlines the same source via
66
+ ``get_legacy_cursor_iife()`` instead.
67
+ """
68
+ path = bundle_dir() / "steppy-cursor-legacy.js"
69
+ if not path.is_file():
70
+ raise CursorBundleMissingError(f"Vendored cursor bundle missing at {path}. " + _BUILD_HINT)
71
+ return path
72
+
73
+
74
+ @lru_cache(maxsize=1)
75
+ def get_video_iife() -> str:
76
+ """Return the bundled video-renderer IIFE as a string.
77
+
78
+ Exposes ``window.__STEPPY_VIDEO__``. Unlike the other two bundles this one
79
+ is **never copied into a build's output**: ``steppy/video.py`` inlines it
80
+ into a throwaway harness page, drives it frame by frame over CDP, and
81
+ deletes the page. Published guide sites get an ``.mp4``, not a renderer.
82
+ """
83
+ path = bundle_dir() / "steppy-video.js"
84
+ try:
85
+ return path.read_text(encoding="utf-8")
86
+ except (FileNotFoundError, OSError) as exc:
87
+ raise CursorBundleMissingError(f"Vendored video bundle missing at {path}. " + _BUILD_HINT) from exc
88
+
89
+
90
+ def get_video_bundle_path() -> Path:
91
+ """Filesystem path of the video-renderer IIFE bundle (steppy-video.js)."""
92
+ path = bundle_dir() / "steppy-video.js"
93
+ if not path.is_file():
94
+ raise CursorBundleMissingError(f"Vendored video bundle missing at {path}. " + _BUILD_HINT)
95
+ return path
96
+
97
+
98
+ def bundle_dir() -> Path:
99
+ """Filesystem path of the vendored bundle directory."""
100
+ return Path(__file__).resolve().parent
@@ -0,0 +1,2 @@
1
+ (function(){"use strict";const w={pre_action_ms:200,action_ms:300,post_action_ms:150,travel_speed_px_per_ms:1.5},h=n=>new Promise(t=>setTimeout(t,Math.max(0,n)));function E(n){return n*n*(3-2*n)}function y(n,t){if(!t||!t.width||!t.height)return null;const e=n.getBoundingClientRect();return!e.width||!e.height?null:{sx:e.width/t.width,sy:e.height/t.height}}function f(n,t){return{x:n.x*t.sx,y:n.y*t.sy,width:n.width*t.sx,height:n.height*t.sy}}function T(n){return{x:n.x+n.width/2,y:n.y+n.height/2}}function m(n,t,e){if(!n)return null;const s=y(t,e);return s?T(f(n,s)):null}function v(n,t,e){const s=t.x-n.x,i=t.y-n.y,a=Math.hypot(s,i);return Math.max(150,a/(e||1.5))}function P(n,t){const e=t.x-n.x,s=t.y-n.y,i=Math.hypot(e,s);if(!i)return{x:n.x,y:n.y};const a=(n.x+t.x)/2,o=(n.y+t.y)/2,r=-s/i,c=e/i;return{x:a+r*i*.15,y:o+c*i*.15}}class d{constructor(t){var e;this.cursorEl=null,this.rippleEl=null,this.kbdEl=null,this.imgEl=null,this.currentPos={x:8,y:8},this.aborted=!1,this.stage=t.stage,this.timings={...w,...(e=t.timings)!=null?e:{}},this.onPhaseChange=t.onPhaseChange,this.refreshDom()}setTimings(t){this.timings={...this.timings,...t}}setStage(t){this.stage=t,this.refreshDom()}abort(){this.aborted=!0}hide(){var t;this.cursorEl&&this.cursorEl.classList.remove("visible"),this.kbdEl&&this.kbdEl.classList.remove("visible"),(t=this.onPhaseChange)==null||t.call(this,"idle")}refreshDom(){this.cursorEl=this.stage.querySelector(".steppy-cursor"),this.rippleEl=this.stage.querySelector(".steppy-ripple"),this.kbdEl=this.stage.querySelector(".steppy-kbd"),this.imgEl=this.stage.querySelector("img")}static normalizeTargets(t){if(!t)return[];if(t.kind==="type-multi")return t.targets.map(i=>({bbox:i.bbox,value:i.value,selector:i.selector,kind:"type"}));const e=t.target,s=t.kind==="type"?t.value:void 0;return[{bbox:e.bbox,value:s,selector:e.selector,kind:t.kind==="click"?"click":t.kind==="hover"?"hover":"type"}]}async waitImageReady(){const t=this.imgEl;t&&(t.complete&&t.naturalWidth||await new Promise(e=>{const s=()=>e();t.addEventListener("load",s,{once:!0}),t.addEventListener("error",s,{once:!0})}))}async animateFrame(t){if(this.aborted=!1,this.refreshDom(),!this.cursorEl)return;if(!t.cursor){this.hide();return}await this.waitImageReady();const e=d.normalizeTargets(t.cursor);this.currentPos={x:8,y:8},this.cursorEl.classList.add("visible");for(let s=0;s<e.length;s++){if(this.aborted||(await this.travelToTargetIndex(t,e,s),this.aborted)||(await this.playTargetActionIndex(t,e,s),this.aborted))return;await h(this.timings.post_action_ms)}}async travelToTarget(t,e){if(this.aborted=!1,this.refreshDom(),!this.cursorEl)return;if(!t.cursor){this.hide();return}await this.waitImageReady();const s=d.normalizeTargets(t.cursor);this.cursorEl.classList.add("visible"),await this.travelToTargetIndex(t,s,e)}async playTargetAction(t,e){if(this.aborted=!1,this.refreshDom(),!t.cursor)return;const s=d.normalizeTargets(t.cursor);await this.playTargetActionIndex(t,s,e),!this.aborted&&await h(this.timings.post_action_ms)}computeDisplayRect(t,e){const s=this.imgEl;if(!s||!t)return null;const i=y(s,e);return i?f(t,i):null}async travelToTargetIndex(t,e,s){var r,c;const i=e[s],a=this.cursorEl;if(!i||!a)return;const o=m(i.bbox,this.imgEl,t.viewport);o&&((r=this.onPhaseChange)==null||r.call(this,"travel",i),await this.travelTo(a,this.currentPos,o),this.currentPos=o,(c=this.onPhaseChange)==null||c.call(this,"pre-action",i),a.classList.remove("pulse"),a.offsetWidth,a.classList.add("pulse"),await h(this.timings.pre_action_ms),a.classList.remove("pulse"))}async playTargetActionIndex(t,e,s){var o,r;const i=e[s];if(!i)return;const a=m(i.bbox,this.imgEl,t.viewport)||this.currentPos;(o=this.onPhaseChange)==null||o.call(this,"action",i),i.kind==="click"?(this.rippleEl&&this.fireClick(this.rippleEl,a),await h(this.timings.action_ms)):i.kind==="type"?this.kbdEl&&await this.typeValue(this.kbdEl,a,i.value):await h(this.timings.action_ms),(r=this.onPhaseChange)==null||r.call(this,"post-action",i)}async travelTo(t,e,s){const i=v(e,s,this.timings.travel_speed_px_per_ms),a=P(e,s);return new Promise(o=>{const r=performance.now(),c=u=>{if(this.aborted){o();return}const g=Math.min(1,(u-r)/i),l=E(g),b=(1-l)*(1-l)*e.x+2*(1-l)*l*a.x+l*l*s.x,I=(1-l)*(1-l)*e.y+2*(1-l)*l*a.y+l*l*s.y;t.style.left=`${b}px`,t.style.top=`${I}px`,g<1?requestAnimationFrame(c):o()};requestAnimationFrame(c)})}fireClick(t,e){t.style.left=`${e.x}px`,t.style.top=`${e.y}px`,t.classList.remove("fire"),t.offsetWidth,t.classList.add("fire")}async typeValue(t,e,s){var o;t.style.left=`${e.x+16}px`,t.style.top=`${e.y+16}px`,t.textContent="",t.classList.add("visible");const i=(s!=null?s:"").split(""),a=i.length?this.timings.action_ms/i.length:this.timings.action_ms;for(const r of i){if(this.aborted)break;t.textContent=((o=t.textContent)!=null?o:"")+r,await h(a)}await h(80),t.classList.remove("visible")}}function S(n,t,e,s){var o;if(!t)return Promise.resolve();const i=Array.from(n.querySelectorAll(".steppy-stage")),a=(o=t.frames)!=null?o:[];return(async()=>{for(let r=0;r<i.length;r++){const c=i[r],u=a[r];if(!u)continue;const g=new d({stage:c,timings:e});s==null||s.push(g),await g.animateFrame(u)}})()}function p(n,t,e){const s=Array.from(n.querySelectorAll(".steppy-step")),i=[],a=r=>{const c=r.dataset.stepIndex,u=c!==void 0?parseInt(c,10)-1:-1;return S(r,u>=0?t[u]:void 0,e,i)};let o=null;return"IntersectionObserver"in window?(o=new IntersectionObserver(r=>{r.forEach(c=>{c.isIntersecting&&(o.unobserve(c.target),a(c.target))})},{threshold:.5}),s.forEach(r=>o.observe(r))):(async()=>{for(const r of s)await a(r)})(),function(){o&&o.disconnect(),i.forEach(c=>c.abort())}}function x(){var n;p(document,(n=window.__STEPPY_STEPS__)!=null?n:[],window.__STEPPY_TIMINGS__)}window.__STEPPY_CURSOR__={mount:p,init:x};function _(){!window.__STEPPY_STEPS__&&!window.__STEPPY_STEPS_BY_LAYOUT__||x()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",_):_()})();
2
+ //# sourceMappingURL=steppy-cursor-legacy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steppy-cursor-legacy.js","sources":["../src/cursor/types.ts","../src/cursor/animator.ts","../src/cursor/legacy-iife.ts"],"sourcesContent":["/**\n * Cursor data shapes — shared by the Lit player, the legacy IIFE bundle, and\n * the manifest validator. Kept dependency-free (no Zod) so the legacy IIFE\n * stays a tiny standalone bundle.\n *\n * The manifest schema in ``../manifest/schema.ts`` produces types that are\n * structurally identical to these — by design.\n */\n\nexport interface Bbox {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface Viewport {\n width: number;\n height: number;\n}\n\nexport interface SingleTarget {\n selector?: string;\n bbox: Bbox;\n}\n\nexport interface MultiTarget extends SingleTarget {\n value?: string;\n}\n\nexport type CursorBehavior =\n | { kind: 'click'; target: SingleTarget }\n | { kind: 'hover'; target: SingleTarget }\n | { kind: 'type'; target: SingleTarget; value?: string }\n | { kind: 'type-multi'; targets: MultiTarget[] };\n\nexport interface CursorTimings {\n pre_action_ms: number;\n action_ms: number;\n post_action_ms: number;\n travel_speed_px_per_ms: number;\n}\n\nexport const DEFAULT_TIMINGS: CursorTimings = {\n pre_action_ms: 200,\n action_ms: 300,\n post_action_ms: 150,\n travel_speed_px_per_ms: 1.5,\n};\n\nexport interface AnimatorFrame {\n cursor: CursorBehavior | null;\n viewport: Viewport | null;\n}\n\n/** A scaled, displayed-space rect derived from a bbox + image size + viewport. */\nexport interface DisplayRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface DisplayScale {\n sx: number;\n sy: number;\n}\n","/**\n * Cursor animation — canonical TS port of the inline JS that previously lived\n * in ``templates/static/cursor.js.j2`` and ``templates/guide.html.j2``. Both\n * the new ``<steppy-guide>`` Lit element and the legacy stepbystep templates\n * consume this module (the latter via ``legacy-iife.ts``).\n *\n * Two animation modes are supported off the same primitives:\n *\n * - **Full frame** (stepbystep): ``animateFrame(frame)`` walks every target in\n * the frame and runs travel → pre-action → action → post-action without\n * pause. Matches the historical behavior of the inline JS.\n * - **Interactive** (the new player): ``travelToTarget`` /\n * ``playTargetAction`` split the sequence so the player can wait for the\n * user to click a hotspot between phases.\n *\n * The animator owns the cursor / ripple / kbd DOM elements (it does not\n * create them — the host provides them via ``opts.stage`` and the elements\n * inside that stage). Coordinates are scaled at animation time from the\n * frame's capture-space viewport to the displayed image size; this is the\n * single point where capture-space → display-space conversion happens.\n */\n\nimport {\n AnimatorFrame,\n Bbox,\n CursorBehavior,\n CursorTimings,\n DEFAULT_TIMINGS,\n DisplayRect,\n DisplayScale,\n MultiTarget,\n SingleTarget,\n Viewport,\n} from './types.js';\n\nexport type TargetKind = 'click' | 'hover' | 'type';\n\nexport interface NormalizedTarget {\n bbox: Bbox;\n value?: string;\n selector?: string;\n kind: TargetKind;\n}\n\nconst sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));\n\nexport function easeInOut(t: number): number {\n // Smoothstep — symmetric S-curve, matches the legacy inline JS.\n return t * t * (3 - 2 * t);\n}\n\nexport function getDisplayScale(\n img: HTMLElement,\n viewport: Viewport | null | undefined,\n): DisplayScale | null {\n if (!viewport || !viewport.width || !viewport.height) return null;\n const rect = img.getBoundingClientRect();\n if (!rect.width || !rect.height) return null;\n return { sx: rect.width / viewport.width, sy: rect.height / viewport.height };\n}\n\nexport function scaleRect(bbox: Bbox, scale: DisplayScale): DisplayRect {\n return {\n x: bbox.x * scale.sx,\n y: bbox.y * scale.sy,\n width: bbox.width * scale.sx,\n height: bbox.height * scale.sy,\n };\n}\n\nexport function rectCenter(rect: DisplayRect): { x: number; y: number } {\n return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };\n}\n\n/**\n * Legacy entry point preserved verbatim for the IIFE bundle: capture-space\n * bbox + image + viewport → display-space center point.\n */\nexport function scaleBbox(\n bbox: Bbox | null | undefined,\n img: HTMLElement,\n viewport: Viewport | null | undefined,\n): { x: number; y: number } | null {\n if (!bbox) return null;\n const scale = getDisplayScale(img, viewport);\n if (!scale) return null;\n return rectCenter(scaleRect(bbox, scale));\n}\n\n/**\n * Quadratic-Bezier travel duration. Pulled out so it's unit-testable\n * without DOM. ``speed`` is in capture-space px per ms.\n */\nexport function travelDuration(\n from: { x: number; y: number },\n to: { x: number; y: number },\n speed: number,\n): number {\n const dx = to.x - from.x;\n const dy = to.y - from.y;\n const dist = Math.hypot(dx, dy);\n return Math.max(150, dist / (speed || 1.5));\n}\n\n/**\n * Compute a quadratic-Bezier control point offset 15% perpendicular to the\n * straight line between ``from`` and ``to``. Same formula the inline JS uses.\n */\nexport function bezierControlPoint(\n from: { x: number; y: number },\n to: { x: number; y: number },\n): { x: number; y: number } {\n const dx = to.x - from.x;\n const dy = to.y - from.y;\n const dist = Math.hypot(dx, dy);\n if (!dist) return { x: from.x, y: from.y };\n const mx = (from.x + to.x) / 2;\n const my = (from.y + to.y) / 2;\n const nx = -dy / dist;\n const ny = dx / dist;\n return { x: mx + nx * dist * 0.15, y: my + ny * dist * 0.15 };\n}\n\nexport interface AnimatorOptions {\n /** The stage element. Must contain ``.steppy-cursor``, ``.steppy-ripple``,\n * ``.steppy-kbd`` descendants and a single ``img`` (the screenshot). */\n stage: HTMLElement;\n timings?: Partial<CursorTimings>;\n /** Hook called when the cursor moves to or fires on a target. Used by the\n * Lit player to broadcast progress events. */\n onPhaseChange?: (phase: 'travel' | 'pre-action' | 'action' | 'post-action' | 'idle', target?: NormalizedTarget) => void;\n}\n\nexport class CursorAnimator {\n private stage: HTMLElement;\n private timings: CursorTimings;\n private onPhaseChange: AnimatorOptions['onPhaseChange'];\n private cursorEl: HTMLElement | null = null;\n private rippleEl: HTMLElement | null = null;\n private kbdEl: HTMLElement | null = null;\n private imgEl: HTMLElement | null = null;\n private currentPos: { x: number; y: number } = { x: 8, y: 8 };\n private aborted = false;\n\n constructor(opts: AnimatorOptions) {\n this.stage = opts.stage;\n this.timings = { ...DEFAULT_TIMINGS, ...(opts.timings ?? {}) };\n this.onPhaseChange = opts.onPhaseChange;\n this.refreshDom();\n }\n\n setTimings(t: Partial<CursorTimings>): void {\n this.timings = { ...this.timings, ...t };\n }\n\n /** Re-point the animator at a (possibly relocated) stage element and re-read\n * its children. Needed when a host re-mounts the stage subtree — e.g. the\n * Lit player switching presentation templates moves ``.steppy-stage`` to a\n * new DOM position, and the cached ``stage`` ref would otherwise go stale. */\n setStage(stage: HTMLElement): void {\n this.stage = stage;\n this.refreshDom();\n }\n\n abort(): void {\n this.aborted = true;\n }\n\n hide(): void {\n if (this.cursorEl) this.cursorEl.classList.remove('visible');\n if (this.kbdEl) this.kbdEl.classList.remove('visible');\n this.onPhaseChange?.('idle');\n }\n\n /** Re-read stage children. Call after the stage's DOM has been re-rendered. */\n refreshDom(): void {\n this.cursorEl = this.stage.querySelector<HTMLElement>('.steppy-cursor');\n this.rippleEl = this.stage.querySelector<HTMLElement>('.steppy-ripple');\n this.kbdEl = this.stage.querySelector<HTMLElement>('.steppy-kbd');\n this.imgEl = this.stage.querySelector<HTMLImageElement>('img');\n }\n\n /**\n * Normalize a frame's cursor behavior into an ordered list of targets.\n * ``type-multi`` expands into N targets of kind ``type``; single-target\n * variants produce a single entry. Returns ``[]`` when there's nothing to\n * animate (cursor is null or no targets).\n */\n static normalizeTargets(behavior: CursorBehavior | null): NormalizedTarget[] {\n if (!behavior) return [];\n if (behavior.kind === 'type-multi') {\n return behavior.targets.map(\n (t: MultiTarget): NormalizedTarget => ({\n bbox: t.bbox,\n value: t.value,\n selector: t.selector,\n kind: 'type',\n }),\n );\n }\n const tgt = behavior.target as SingleTarget;\n const value = behavior.kind === 'type' ? behavior.value : undefined;\n return [\n {\n bbox: tgt.bbox,\n value,\n selector: tgt.selector,\n kind: behavior.kind === 'click' ? 'click' : behavior.kind === 'hover' ? 'hover' : 'type',\n },\n ];\n }\n\n /** Wait for the screenshot ``<img>`` to be ready (load or error). */\n async waitImageReady(): Promise<void> {\n const img = this.imgEl as HTMLImageElement | null;\n if (!img) return;\n if (img.complete && img.naturalWidth) return;\n await new Promise<void>((resolve) => {\n const done = () => resolve();\n img.addEventListener('load', done, { once: true });\n img.addEventListener('error', done, { once: true });\n });\n }\n\n /**\n * Full sequence over every target in ``frame.cursor`` — historical\n * stepbystep behavior. No user interaction.\n */\n async animateFrame(frame: AnimatorFrame): Promise<void> {\n this.aborted = false;\n this.refreshDom();\n if (!this.cursorEl) return;\n if (!frame.cursor) {\n this.hide();\n return;\n }\n await this.waitImageReady();\n const targets = CursorAnimator.normalizeTargets(frame.cursor);\n this.currentPos = { x: 8, y: 8 };\n this.cursorEl.classList.add('visible');\n for (let i = 0; i < targets.length; i++) {\n if (this.aborted) return;\n await this.travelToTargetIndex(frame, targets, i);\n if (this.aborted) return;\n await this.playTargetActionIndex(frame, targets, i);\n if (this.aborted) return;\n await sleep(this.timings.post_action_ms);\n }\n }\n\n /**\n * Interactive mode: move the cursor to ``targetIndex`` and pulse it. Caller\n * waits for user input before invoking ``playTargetAction``.\n */\n async travelToTarget(frame: AnimatorFrame, targetIndex: number): Promise<void> {\n this.aborted = false;\n this.refreshDom();\n if (!this.cursorEl) return;\n if (!frame.cursor) {\n this.hide();\n return;\n }\n await this.waitImageReady();\n const targets = CursorAnimator.normalizeTargets(frame.cursor);\n this.cursorEl.classList.add('visible');\n await this.travelToTargetIndex(frame, targets, targetIndex);\n }\n\n /**\n * Interactive mode: run the action animation for ``targetIndex`` and the\n * post-action settling beat.\n */\n async playTargetAction(frame: AnimatorFrame, targetIndex: number): Promise<void> {\n this.aborted = false;\n this.refreshDom();\n if (!frame.cursor) return;\n const targets = CursorAnimator.normalizeTargets(frame.cursor);\n await this.playTargetActionIndex(frame, targets, targetIndex);\n if (this.aborted) return;\n await sleep(this.timings.post_action_ms);\n }\n\n /**\n * Pure helper: project a capture-space bbox into the displayed coordinate\n * space. Returns ``null`` when the image hasn't laid out yet. Exposed so\n * the Lit player can place hotspots without duplicating the math.\n */\n computeDisplayRect(bbox: Bbox | null | undefined, viewport: Viewport | null | undefined): DisplayRect | null {\n const img = this.imgEl;\n if (!img || !bbox) return null;\n const scale = getDisplayScale(img, viewport);\n if (!scale) return null;\n return scaleRect(bbox, scale);\n }\n\n // ── Internal building blocks ───────────────────────────────────────────\n\n private async travelToTargetIndex(\n frame: AnimatorFrame,\n targets: NormalizedTarget[],\n index: number,\n ): Promise<void> {\n const tgt = targets[index];\n const cursor = this.cursorEl;\n if (!tgt || !cursor) return;\n const center = scaleBbox(tgt.bbox, this.imgEl!, frame.viewport);\n if (!center) return;\n\n this.onPhaseChange?.('travel', tgt);\n await this.travelTo(cursor, this.currentPos, center);\n this.currentPos = center;\n\n this.onPhaseChange?.('pre-action', tgt);\n // Pulse: drop the class, force reflow to restart the animation, re-add.\n cursor.classList.remove('pulse');\n void cursor.offsetWidth;\n cursor.classList.add('pulse');\n await sleep(this.timings.pre_action_ms);\n cursor.classList.remove('pulse');\n }\n\n private async playTargetActionIndex(\n frame: AnimatorFrame,\n targets: NormalizedTarget[],\n index: number,\n ): Promise<void> {\n const tgt = targets[index];\n if (!tgt) return;\n const center = scaleBbox(tgt.bbox, this.imgEl!, frame.viewport) || this.currentPos;\n this.onPhaseChange?.('action', tgt);\n if (tgt.kind === 'click') {\n if (this.rippleEl) this.fireClick(this.rippleEl, center);\n await sleep(this.timings.action_ms);\n } else if (tgt.kind === 'type') {\n if (this.kbdEl) await this.typeValue(this.kbdEl, center, tgt.value);\n } else {\n // hover: linger\n await sleep(this.timings.action_ms);\n }\n this.onPhaseChange?.('post-action', tgt);\n }\n\n private async travelTo(\n cursor: HTMLElement,\n from: { x: number; y: number },\n to: { x: number; y: number },\n ): Promise<void> {\n const duration = travelDuration(from, to, this.timings.travel_speed_px_per_ms);\n const cp = bezierControlPoint(from, to);\n return new Promise<void>((resolve) => {\n const start = performance.now();\n const frame = (now: number) => {\n if (this.aborted) {\n resolve();\n return;\n }\n const raw = Math.min(1, (now - start) / duration);\n const t = easeInOut(raw);\n const x = (1 - t) * (1 - t) * from.x + 2 * (1 - t) * t * cp.x + t * t * to.x;\n const y = (1 - t) * (1 - t) * from.y + 2 * (1 - t) * t * cp.y + t * t * to.y;\n cursor.style.left = `${x}px`;\n cursor.style.top = `${y}px`;\n if (raw < 1) requestAnimationFrame(frame);\n else resolve();\n };\n requestAnimationFrame(frame);\n });\n }\n\n private fireClick(ripple: HTMLElement, to: { x: number; y: number }): void {\n ripple.style.left = `${to.x}px`;\n ripple.style.top = `${to.y}px`;\n ripple.classList.remove('fire');\n void ripple.offsetWidth;\n ripple.classList.add('fire');\n }\n\n private async typeValue(\n kbd: HTMLElement,\n to: { x: number; y: number },\n value: string | undefined,\n ): Promise<void> {\n kbd.style.left = `${to.x + 16}px`;\n kbd.style.top = `${to.y + 16}px`;\n kbd.textContent = '';\n kbd.classList.add('visible');\n const chars = (value ?? '').split('');\n const per = chars.length ? this.timings.action_ms / chars.length : this.timings.action_ms;\n for (const ch of chars) {\n if (this.aborted) break;\n kbd.textContent = (kbd.textContent ?? '') + ch;\n await sleep(per);\n }\n await sleep(80);\n kbd.classList.remove('visible');\n }\n}\n","/**\n * Legacy IIFE — drop-in replacement for the inline JS that previously lived\n * in [templates/static/cursor.js.j2](../../../../templates/static/cursor.js.j2)\n * and the cursor block of [templates/guide.html.j2](../../../../templates/guide.html.j2).\n *\n * Reads ``window.__STEPPY_STEPS__`` (per-step frame data) and\n * ``window.__STEPPY_TIMINGS__`` (cursor timing overrides), then animates each\n * step's cursor when its ``.steppy-step`` section scrolls into view. This is\n * the exact contract those Jinja templates already produce; making them\n * include this bundle replaces ~175 lines of duplicated JS with a single\n * built artifact.\n *\n * Two consumers:\n *\n * - **Static / standalone HTML** (``cursor.js.j2`` / ``guide.html.j2``): the\n * page sets the ``__STEPPY_STEPS__`` globals before the bundle loads, so the\n * bundle auto-inits on DOM-ready over the whole document.\n * - **Angular host** (``angular/component.ts.j2``): there are no globals and\n * the guide HTML is injected via ``[innerHTML]`` *after* DOM-ready (and\n * re-injected on locale/layout/mode change). The bundle never auto-inits in\n * that case; instead it exposes ``window.__STEPPY_CURSOR__.mount(root, steps,\n * timings)`` so the component can (re)drive the cursor over its own subtree\n * each time the DOM is swapped. ``mount`` returns a disposer that disconnects\n * the observer and aborts any in-flight animation.\n *\n * Output format: IIFE bundle. Vite builds this via ``vite.iife.config.ts``.\n */\n\nimport type { CursorTimings } from './types.js';\nimport { CursorAnimator } from './animator.js';\n\ninterface LegacyFrame {\n screenshot: string;\n viewport: { width: number; height: number } | null;\n cursor: Parameters<CursorAnimator['animateFrame']>[0]['cursor'];\n}\n\ninterface LegacyStep {\n index: number;\n frames: LegacyFrame[];\n}\n\ntype LegacyTimings = Partial<CursorTimings> | undefined;\n\ninterface SteppyCursorApi {\n /** Drive the cursor over every ``.steppy-step`` under ``root``, animating\n * each step as it scrolls into view. Returns a disposer. */\n mount(root: ParentNode, steps: LegacyStep[], timings?: LegacyTimings): () => void;\n /** Auto-init entry point used by the static/standalone templates (reads the\n * ``window.__STEPPY_STEPS__`` globals and mounts over ``document``). */\n init(): void;\n}\n\ndeclare global {\n interface Window {\n __STEPPY_STEPS__?: LegacyStep[];\n __STEPPY_STEPS_BY_LAYOUT__?: Record<string, LegacyStep[]>;\n __STEPPY_TIMINGS__?: Partial<CursorTimings>;\n __STEPPY_CURSOR__?: SteppyCursorApi;\n }\n}\n\nfunction animateStep(\n section: Element,\n step: LegacyStep | undefined,\n timings: LegacyTimings,\n animators?: CursorAnimator[],\n): Promise<void> {\n if (!step) return Promise.resolve();\n const stages = Array.from(section.querySelectorAll<HTMLElement>('.steppy-stage'));\n const frames = step.frames ?? [];\n return (async () => {\n for (let i = 0; i < stages.length; i++) {\n const stage = stages[i]!;\n const frame = frames[i];\n if (!frame) continue;\n const animator = new CursorAnimator({ stage, timings });\n animators?.push(animator);\n await animator.animateFrame(frame);\n }\n })();\n}\n\n/**\n * Drive the cursor over a subtree. Used directly by the Angular host (which\n * passes its component root and the per-(locale, layout) step data) and\n * indirectly by ``init`` for the static templates (which pass ``document`` and\n * the ``window.__STEPPY_STEPS__`` globals).\n */\nfunction mount(root: ParentNode, steps: LegacyStep[], timings?: LegacyTimings): () => void {\n const sections = Array.from(root.querySelectorAll<HTMLElement>('.steppy-step'));\n const animators: CursorAnimator[] = [];\n\n const runSection = (section: HTMLElement): Promise<void> => {\n const idxAttr = section.dataset.stepIndex;\n const idx = idxAttr !== undefined ? parseInt(idxAttr, 10) - 1 : -1;\n return animateStep(section, idx >= 0 ? steps[idx] : undefined, timings, animators);\n };\n\n let io: IntersectionObserver | null = null;\n if (!('IntersectionObserver' in window)) {\n // Fallback: animate every section sequentially.\n void (async () => {\n for (const section of sections) await runSection(section);\n })();\n } else {\n io = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (!entry.isIntersecting) return;\n io!.unobserve(entry.target);\n void runSection(entry.target as HTMLElement);\n });\n },\n { threshold: 0.5 },\n );\n sections.forEach((section) => io!.observe(section));\n }\n\n return function dispose(): void {\n if (io) io.disconnect();\n animators.forEach((a) => a.abort());\n };\n}\n\nfunction init(): void {\n mount(document, window.__STEPPY_STEPS__ ?? [], window.__STEPPY_TIMINGS__);\n}\n\n// Expose the programmatic API so SPA hosts (Angular) can (re)drive the cursor\n// over content injected after page load. Harmless for the static templates.\nwindow.__STEPPY_CURSOR__ = { mount, init };\n\n// Auto-init only when the page pre-declared the step globals (static /\n// standalone HTML). In an SPA the globals are absent and the host calls\n// ``mount`` explicitly after each render, so auto-init would be a no-op anyway\n// — guarding here avoids spinning up a stray document-wide observer.\nfunction autoInit(): void {\n if (!window.__STEPPY_STEPS__ && !window.__STEPPY_STEPS_BY_LAYOUT__) return;\n init();\n}\n\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', autoInit);\n} else {\n autoInit();\n}\n"],"names":["DEFAULT_TIMINGS","sleep","ms","resolve","easeInOut","t","getDisplayScale","img","viewport","rect","scaleRect","bbox","scale","rectCenter","scaleBbox","travelDuration","from","to","speed","dx","dy","dist","bezierControlPoint","mx","my","nx","ny","CursorAnimator","opts","_a","stage","behavior","tgt","value","done","frame","targets","i","targetIndex","index","cursor","center","_b","duration","cp","start","now","raw","x","y","ripple","kbd","chars","per","ch","animateStep","section","step","timings","animators","stages","frames","animator","mount","root","steps","sections","runSection","idxAttr","idx","io","entries","entry","a","init","autoInit"],"mappings":"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"}