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.
- steppy/CHANGELOG.md +862 -0
- steppy/__init__.py +9 -0
- steppy/__main__.py +13 -0
- steppy/_cli_common.py +183 -0
- steppy/_docker/Dockerfile.installed +53 -0
- steppy/_parallel/__init__.py +7 -0
- steppy/_parallel/compose.py +349 -0
- steppy/_parallel/event_bus.py +418 -0
- steppy/_parallel/failures.py +267 -0
- steppy/_parallel/fingerprint.py +141 -0
- steppy/_parallel/image.py +380 -0
- steppy/_parallel/lastfailed.py +125 -0
- steppy/_parallel/lifecycle.py +90 -0
- steppy/_parallel/preauth.py +109 -0
- steppy/_parallel/progress.py +101 -0
- steppy/_parallel/queue.py +385 -0
- steppy/_parallel/renderer.py +489 -0
- steppy/_parallel/retry.py +316 -0
- steppy/_player_bundle/THIRD_PARTY_LICENSES.txt +158 -0
- steppy/_player_bundle/__init__.py +100 -0
- steppy/_player_bundle/steppy-cursor-legacy.js +2 -0
- steppy/_player_bundle/steppy-cursor-legacy.js.map +1 -0
- steppy/_player_bundle/steppy-player.js +5419 -0
- steppy/_player_bundle/steppy-player.js.map +1 -0
- steppy/_player_bundle/steppy-video.js +472 -0
- steppy/_player_bundle/steppy-video.js.map +1 -0
- steppy/_templating.py +117 -0
- steppy/assertions.py +251 -0
- steppy/auth.py +493 -0
- steppy/build.py +2798 -0
- steppy/build_models.py +669 -0
- steppy/buildid.py +134 -0
- steppy/bundle/__init__.py +47 -0
- steppy/bundle/archive.py +239 -0
- steppy/bundle/classify.py +329 -0
- steppy/bundle/create.py +490 -0
- steppy/bundle/hashing.py +28 -0
- steppy/bundle/manifest.py +334 -0
- steppy/bundle/results.py +546 -0
- steppy/bundle_cli.py +455 -0
- steppy/captures.py +207 -0
- steppy/cli.py +2489 -0
- steppy/cloud/__init__.py +58 -0
- steppy/cloud/client.py +267 -0
- steppy/cloud/upload.py +497 -0
- steppy/compat.py +109 -0
- steppy/config.py +231 -0
- steppy/config_schema.py +87 -0
- steppy/contracts/__init__.py +54 -0
- steppy/contracts/bundle.py +215 -0
- steppy/contracts/report.py +346 -0
- steppy/contracts/tours.py +505 -0
- steppy/coordinator.py +1352 -0
- steppy/coverage/__init__.py +9 -0
- steppy/coverage/gaps.py +190 -0
- steppy/coverage/manifest.py +193 -0
- steppy/coverage/merge.py +363 -0
- steppy/coverage/normalize.py +174 -0
- steppy/coverage/report.py +178 -0
- steppy/coverage/sourcemap.py +219 -0
- steppy/coverage/store.py +473 -0
- steppy/coverage/triage.py +151 -0
- steppy/coverage_cli.py +265 -0
- steppy/durations.py +48 -0
- steppy/exclusive.py +178 -0
- steppy/flaky_cli.py +169 -0
- steppy/generator.py +633 -0
- steppy/guide.py +1467 -0
- steppy/history.py +352 -0
- steppy/init_cli.py +330 -0
- steppy/locales.py +282 -0
- steppy/migrate/__init__.py +35 -0
- steppy/migrate/apply.py +194 -0
- steppy/migrate/plan.py +199 -0
- steppy/migrate/rules.py +465 -0
- steppy/migrate/verify.py +163 -0
- steppy/migrate_cli.py +152 -0
- steppy/models.py +2539 -0
- steppy/naming.py +37 -0
- steppy/parser.py +475 -0
- steppy/pdf.py +193 -0
- steppy/presets.py +284 -0
- steppy/record_cli.py +796 -0
- steppy/report/__init__.py +37 -0
- steppy/report/assets.py +197 -0
- steppy/report/build.py +261 -0
- steppy/report/coverage_source.py +277 -0
- steppy/report/model.py +423 -0
- steppy/report/render.py +303 -0
- steppy/report/source.py +238 -0
- steppy/report_cli.py +173 -0
- steppy/runmeta.py +114 -0
- steppy/selection.py +183 -0
- steppy/sitemap.py +205 -0
- steppy/skills/steppy-app-tours/SKILL.md +448 -0
- steppy/skills/steppy-ci-report/SKILL.md +567 -0
- steppy/skills/steppy-scenario-author/SKILL.md +2176 -0
- steppy/skills/steppy-template/SKILL.md +676 -0
- steppy/templates/_steppy_events.py +291 -0
- steppy/templates/_steppy_tours.py +404 -0
- steppy/templates/angular/component-interactive.ts.j2 +109 -0
- steppy/templates/angular/component-video.ts.j2 +112 -0
- steppy/templates/angular/component.ts.j2 +229 -0
- steppy/templates/angular/content.ts.j2 +31 -0
- steppy/templates/angular/gitignore.j2 +3 -0
- steppy/templates/angular/group.component.ts.j2 +39 -0
- steppy/templates/angular/onboarding.component.ts.j2 +43 -0
- steppy/templates/angular/routes.ts.j2 +34 -0
- steppy/templates/ci/github-actions.yml +284 -0
- steppy/templates/ci/gitlab-ci.yml +178 -0
- steppy/templates/conftest.py.j2 +2405 -0
- steppy/templates/guide.book.html.j2 +145 -0
- steppy/templates/guide.fragment.html.j2 +257 -0
- steppy/templates/guide.html.j2 +301 -0
- steppy/templates/guide.md.j2 +18 -0
- steppy/templates/init/config.yaml +124 -0
- steppy/templates/init/credentials.yaml.example +30 -0
- steppy/templates/init/scenarios/example--first-flow.md +53 -0
- steppy/templates/report/_head.html.j2 +14 -0
- steppy/templates/report/_mark.html.j2 +3 -0
- steppy/templates/report/coverage.html.j2 +124 -0
- steppy/templates/report/index.html.j2 +203 -0
- steppy/templates/report/report.css +401 -0
- steppy/templates/report/scenario.html.j2 +137 -0
- steppy/templates/static/_headers.j2 +30 -0
- steppy/templates/static/_mode_links.html.j2 +22 -0
- steppy/templates/static/_seo_head.html.j2 +37 -0
- steppy/templates/static/cursor.js.j2 +10 -0
- steppy/templates/static/group.html.j2 +78 -0
- steppy/templates/static/guides.css +678 -0
- steppy/templates/static/index.html.j2 +79 -0
- steppy/templates/static/interactive.html.j2 +100 -0
- steppy/templates/static/onboarding.html.j2 +64 -0
- steppy/templates/static/page.html.j2 +145 -0
- steppy/templates/static/robots.txt.j2 +7 -0
- steppy/templates/static/video.html.j2 +135 -0
- steppy/templates/test_scenario.py.j2 +767 -0
- steppy/testids.py +166 -0
- steppy/totp.py +26 -0
- steppy/tour_spec.py +163 -0
- steppy/tours/__init__.py +22 -0
- steppy/tours/actions.py +402 -0
- steppy/tours/compiler.py +144 -0
- steppy/tours/diagnostics.py +137 -0
- steppy/tours/emit.py +239 -0
- steppy/tours/ids.py +40 -0
- steppy/tours/inference.py +399 -0
- steppy/tours/model.py +120 -0
- steppy/tours/paths.py +126 -0
- steppy/tours/pipeline.py +277 -0
- steppy/tours/publish.py +124 -0
- steppy/tours/replay.py +234 -0
- steppy/tours/routes.py +175 -0
- steppy/tours/settings.py +164 -0
- steppy/tours/text.py +237 -0
- steppy/tours_cli.py +219 -0
- steppy/tts/__init__.py +280 -0
- steppy/tts/cache.py +185 -0
- steppy/tts/presets.py +125 -0
- steppy/tts/providers/__init__.py +6 -0
- steppy/tts/providers/http.py +184 -0
- steppy/tts/providers/local.py +207 -0
- steppy/tts/providers/null.py +28 -0
- steppy/tts/registry.py +201 -0
- steppy/tts/track.py +158 -0
- steppy/tts/types.py +94 -0
- steppy/update_cli.py +460 -0
- steppy/video.py +730 -0
- steppy/video_cache.py +239 -0
- steppy/video_cli.py +271 -0
- steppy/worker.py +301 -0
- steppy_dev-0.3.0.dist-info/METADATA +1335 -0
- steppy_dev-0.3.0.dist-info/RECORD +177 -0
- steppy_dev-0.3.0.dist-info/WHEEL +4 -0
- steppy_dev-0.3.0.dist-info/entry_points.txt +2 -0
- steppy_dev-0.3.0.dist-info/licenses/LICENSE +202 -0
- 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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"}
|