Skip to content

Playground

Everything below is this page. The tour points at the sidebar on the left, or at the menu button that opens it when the window is too narrow to show it, then at the code block further down, and at a field you type into while the overlay is up. Nothing is in an iframe and nothing is a screenshot. There are two ways in — the button below, or the tour in Across a page load, which hands this one on when its link loads this page.

No tour running.

Which story starts depends on the width of the window. Each intro is one step and names thisPage as next, so either way the tour goes on to the same three steps. Narrow or widen the window past the breakpoint while an intro is showing and the page stops it and starts the other one. Once the tour is past the intro it is left alone, because everything else it points at shows at both widths.

Three of the four steps you see carry a next control. The name step does not, and there is no way to configure that: it declares awaits, so the only thing that moves it on is the page reporting that the name was saved.

const thisPage = {
id: 'this-page',
steps: [
{ id: 'source', target: '[data-tour-source]', message: '…' },
{
id: 'name',
target: { elements: '[data-tour-name]', interactive: true },
message: '…',
awaits: 'name-saved',
},
{ id: 'done', target: '[data-tour-start]', message: '…' },
],
} satisfies LekoStory
const desktopIntro = {
id: 'desktop-intro',
steps: [
{
id: 'sidebar',
target: { elements: '#starlight__sidebar', interactive: true },
message: '…',
},
],
next: thisPage,
} satisfies LekoStory
const mobileIntro = {
id: 'mobile-intro',
steps: [{ id: 'menu', target: 'starlight-menu-button button', message: '…' }],
next: thisPage,
} satisfies LekoStory
const introFor = () => (matchMedia('(min-width: 50rem)').matches ? desktopIntro : mobileIntro)
button.addEventListener('click', () => leko.start(introFor()))
leko.pickUp([desktopIntro, mobileIntro]) // once, as the page loads — it takes what Across a page load handed on, or nothing

The Save button’s handler is the page’s own. It saves, and then says so.

save.addEventListener('click', async () => {
if (input.value.trim() === '') {
saved.textContent = 'Type a name first.'
return
}
save.disabled = true
saved.textContent = 'Saving…'
await new Promise((resolve) => setTimeout(resolve, 500))
saved.textContent = 'Saved.'
save.disabled = false
leko.reached('name-saved')
})

Nothing listens to the typing, and nothing listens to the click on the tour’s behalf. The half-second wait stands in for a request. Delete the reached() line and the save still happens, but the tour stays at the name step until somebody ends it. It moves because the page said the save finished — Signals and awaits.

A reload of this page starts nothing, because no story on it ends on a step that waits for a URL — What crosses the load.