The lifecycle
A tour is two lifecycles, one inside the other. A story has the simple one: it begins on one call and every way it ends, ends all of it. A step has the busy one: hooks run in a fixed order, and the step then rests on screen until exactly one thing moves it.
A story’s lifecycle
Section titled “A story’s lifecycle”stop(), running out of steps with no next, a target that never came back, a handler that threw — goes through story.onLeave, and all but one arrives back at idle. The exception is a story that ran out of steps and named a next: the story it names goes up inside the same operation, so the tour is never idle between the two.start(story)is the only way in from outside. Called while a tour is running it does nothing and reportstour-running—stop()first, thenstart(). There is no way to begin part-way through, and no way back: a story that should be revisitable is a shorter story, run again — Several short stories.story.onEnterruns first andstory.onLeaveruns last, bracketing every step. Setup a whole story stands on belongs in the story’s own hooks; clear-up written on the first step’sonLeavefires the moment the tour reaches step 2 — A story sets up what every step stands on.- Every ending is the whole ending. The close control,
stop(), running out of steps, a target that never turned up, anonEnterthat threw — each tears the run down where it stands, andonLeaveruns even for a step whoseonEnterthrew halfway. nextis the only way one story leads to another. It is asked when the last step advances — never onstop(), never on a failure — and the handoff is one operation:onLeaveis told where the tour is going, and the tour is never idle in between — next joins one story to the one after it.- A page load ends the story where it stands. A story that declares
next, whose last step waits for a URL, hands its successor on across the load, and the next document’spickUp(stories)starts that story from its first step — never part-way — DESIGN.md, A page load ends the story, and hands it on.
A step’s lifecycle
Section titled “A step’s lifecycle”stop() is refused. The whole chain from onLeave to the nextonStep runs inside the one call that moved the tour — anasync onEnter runs its first line there and the rest after that call has returned.The order is fixed, and the report deliberately comes last: a step whose
onEnter threw is never reported as reached. The report follows the step being
handed to be drawn, so a step that glides into view or waits for its target is
reported before it is on screen, and one whose target never turns up is
reported and then followed by the ending.
What moves a step on is decided by one field:
- No
awaits: the step has a next control, and pressing it is the only mover.validate, if declared, guards that press — a refusal shakes the cutout (or dims it under reduced motion), putserrorunder the instruction, and the tour stays put. awaits: 'action-name': the step has no next control, andleko.reached('action-name')is the only mover.awaits: { url: /^\/checkout(?:[/?#]|$)/ }: the step has no next control, and a same-document change to the page’s own URL that now matches the pattern is the only mover. It advances on a change, never on an arrival, so a URL that already matched when the step began waiting does nothing. Noreached()call moves it — a link has nowhere to put one — and DESIGN.md argues the whole of it under A URL is a signal the page reports.
Nothing else moves a step: not a DOM event, not a timer, and not a signal
reported before the step’s onEnter returned —
Signals and awaits.
One property holds the whole diagram together: no hook hands anything back,
and Leko waits for nothing. The chain from one step’s onLeave to the next
step’s onStep runs inside the single synchronous call that moved the tour —
a press, a reached(), a start(), a URL change. Work that must finish
before the next step makes sense is not a slow onEnter; it is a step of its
own, with no target and an awaits, started by its own onEnter and ended
by your code reporting in —
A wait is a step that declares awaits, not a promise.