Play a saved tour on any page — no extension required.
저장한 시연을 어떤 페이지에서나 재생 — 확장 없이.
The standalone player is a single, dependency-free
JavaScript bundle. Drop it onto a static HTML page, hand it a
scenario JSON, and the spotlights, notes, and narration replay
themselves — step by step, across pages, with zero browser
extension installed on the visitor's machine. This guide takes you
from an empty page to a working multi-page tour, one step at a time.
스탠드얼론 플레이어는 의존성이 없는 단일
JavaScript 번들입니다. 정적 HTML 페이지에 끼워 넣고 시나리오
JSON 하나만 건네면, 강조 표시·메모·음성 안내가 스스로 재생됩니다 —
단계별로, 여러 페이지를 넘나들며, 방문자 기기에 어떤 브라우저 확장도
깔지 않은 채로요. 이 문서는 빈 페이지에서 시작해 동작하는 멀티 페이지
투어까지, 한 걸음씩 따라갈 수 있도록 안내합니다.
1. What the standalone player is
1. 스탠드얼론 플레이어란
Manuscript has two ways to replay a tour outside
the authoring extension. Pick the one that matches your audience:
Manuscript 에는 작성용 확장 밖에서 시연을 재생하는 방법이
두 가지 있습니다. 대상에 맞는 쪽을 고르세요:
Runtime bridge (see the bridge guide) —
a tiny remote control. It asks an installed Manuscript
extension to do the replay. Great when your visitors are likely
to have the extension, or when you want them to install it.
Standalone player (this page) — the replay
engine itself, packaged as a chrome-free bundle. It plays
without any extension. Nothing is installed, nothing is
uploaded, and it works for every visitor. This is what you want
for a public demo, embedded docs, or an onboarding flow.
런타임 브리지 (브리지 가이드 보기) —
작은 리모컨입니다. 설치된 Manuscript 확장에 재생을
부탁합니다. 방문자에게 확장이 있을 가능성이 높거나, 설치를
유도하고 싶을 때 좋습니다.
스탠드얼론 플레이어 (이 페이지) — 재생 엔진
자체를 chrome-free 번들로 묶은 것입니다. 어떤 확장도 없이
재생됩니다. 아무것도 설치되지 않고, 아무것도 업로드되지 않으며,
모든 방문자에게 동작합니다. 공개 데모, 임베드 문서, 온보딩
흐름에 바로 이것을 쓰면 됩니다.
The bundle is chrome-free. It calls only standard
browser APIs — sessionStorage,
localStorage, fetch, the DOM, and
requestAnimationFrame. There is no
chrome.* dependency, so it runs in a plain page, an
embedded <iframe>, or anywhere a
<script> tag can.
번들은 chrome-free 입니다. 표준 브라우저 API —
sessionStorage, localStorage,
fetch, DOM, requestAnimationFrame 만
사용합니다. chrome.* 의존성이 전혀 없어, 평범한
페이지든 임베드된 <iframe> 이든
<script> 태그를 넣을 수 있는 어디서나 동작합니다.
2. What you need first
2. 먼저 필요한 것
Two things, both of which you may already have:
두 가지인데, 이미 갖고 있을 수도 있습니다:
A scenario JSON. This is the tour itself — the
steps, spotlights, and notes. Author one with the
Manuscript extension and click
Export, or have an
AI agent generate one from
SKILL.md. You own the file; it is just JSON.
A static server. The bundle and the scenario
are fetched over HTTP, so opening the file with
file:// will not work (browsers block
fetch there). Any tiny server does — for example
npx serve or
python -m http.server.
시나리오 JSON. 투어 그 자체입니다 — 단계,
강조 표시, 메모. Manuscript 확장으로
만들고 내보내기(Export)를 누르거나,
AI 에이전트에게 SKILL.md 로
생성하게 하세요. 파일은 사용자 소유이며, 그냥 JSON 입니다.
정적 서버. 번들과 시나리오는 HTTP 로
불러오므로, file:// 로 파일을 직접 열면 동작하지
않습니다(브라우저가 그 환경에서 fetch 를 막습니다).
아주 작은 서버면 충분해요 — 예: npx serve 또는
python -m http.server.
3. Quick start — your first replay
3. 빠른 시작 — 첫 재생
Put the bundle and a scenario next to an HTML file, then add this
one script. Open the page through your static server and the tour
plays itself. That is the whole thing.
번들과 시나리오를 HTML 파일 옆에 두고, 이 스크립트 하나만
추가하세요. 정적 서버로 페이지를 열면 투어가 스스로 재생됩니다.
이게 전부입니다.
<!-- index.html -->
<!doctype html>
<h1>My product</h1>
<button id="feature">Cool feature</button>
<script type="module">
// 1. import the chrome-free bundle (default export is the player)import player from'./player/manuscript-player.0.1.0.esm.js';
// 2. fetch the tour you exported from the extensionconst scenario = await fetch('./tour.json').then(r => r.json());
// 3. load it, then playawait player.load(scenario);
await player.play();
</script>
<!-- index.html -->
<!doctype html>
<h1>내 제품</h1>
<button id="feature">멋진 기능</button>
<script type="module">
// 1. chrome-free 번들 import (default export 가 player)import player from'./player/manuscript-player.0.1.0.esm.js';
// 2. 확장에서 내보낸 투어를 fetchconst scenario = await fetch('./tour.json').then(r => r.json());
// 3. load 한 뒤 playawait player.load(scenario);
await player.play();
</script>
A small controls bar appears at the bottom (a step counter,
prev/next, pause). The page does the rest. If you want to start
paused, or from a specific step, pass options:
player.play({ startIndex: 2, paused: true }).
하단에 작은 컨트롤 바(단계 카운터, 이전/다음, 일시정지)가
나타납니다. 나머지는 페이지가 알아서 합니다. 일시정지 상태로,
혹은 특정 단계에서 시작하려면 옵션을 넘기세요:
player.play({ startIndex: 2, paused: true }).
4. Where the bundle comes from
4. 번들은 어디서 오나
This site serves a copy under ./player/ — the same
bundle the live demo in §11 below imports. Download the files
straight from here:
You get two flavors. Use whichever fits your page:
두 가지 형태가 나옵니다. 페이지에 맞는 쪽을 쓰세요:
ESM — manuscript-player.<ver>.esm.js.
Import it with <script type="module">. It does
not touch any global, so it never collides with other
scripts. Recommended. (It loads a couple of helper chunks
(player-entry.js, spotlight-editor.js)
from the same folder, so keep all three files together.)
UMD — manuscript-player.<ver>.umd.js.
A single self-contained file you load with a classic
<script src="…">. It exposes the player as the
global window.manuscript. Handy for pages that
can't use ES modules, but watch for the name clash below.
ESM — manuscript-player.<ver>.esm.js.
<script type="module"> 로 import 합니다. 전역을
건드리지 않아 다른 스크립트와 충돌하지 않습니다. 권장.
(같은 폴더의 헬퍼 청크 player-entry.js ·
spotlight-editor.js 를 함께 불러오므로,
세 파일을 같은 폴더에 함께 두세요.)
UMD — manuscript-player.<ver>.umd.js.
고전적인 <script src="…"> 로 불러오는 단일
자체 완결 파일입니다. 플레이어를 전역
window.manuscript 로 노출합니다. ES 모듈을 못 쓰는
페이지에 편리하지만, 아래 이름 충돌을 주의하세요.
Heads-up — name clash. The runtime bridge also
defines window.manuscript. If a page loads
both the bridge and the UMD player, they fight over that
global. Prefer the ESM build (no global), or load the UMD bundle
and immediately stash it under your own name:
const player = window.manuscript; before the bridge
script runs.
주의 — 이름 충돌. 런타임 브리지도
window.manuscript 를 정의합니다. 한 페이지가 브리지와
UMD 플레이어를 둘 다 불러오면 이 전역을 두고 충돌합니다.
ESM 빌드(전역 없음)를 쓰거나, UMD 번들을 불러온 직후 브리지
스크립트가 실행되기 전에 본인 이름으로 챙겨두세요:
const player = window.manuscript;.
5. Optional configuration
5. 선택 설정
The bundle reads an optional global, window.__MANUSCRIPT_PLAYER__,
before it loads. Set it in a plain script that runs first,
then import the module:
번들은 로드 전에 선택적 전역
window.__MANUSCRIPT_PLAYER__ 를 읽습니다. 먼저
실행되는 평범한 스크립트에서 설정한 뒤 모듈을 import 하세요:
window.__MANUSCRIPT_PLAYER__ = {
scenarioUrlBase: './tours/', // where to fetch <id>.json on a cross-origin resume
assetBaseUrl: './player/', // optional — reserved for bundled UI assets (currently none)
tts: true, // read step descriptions aloud (Web Speech API)
ttsVoice: 'Google US English',
allowedParentOrigins: ['https://app.example.com'], // cross-origin only
};
Key
What it does
키
역할
scenarioUrlBase
Folder the player fetches <scenarioId>.json from when resuming a tour that crossed an origin boundary. Same-origin tours don't need it.
출처(origin) 경계를 넘은 투어를 이어 재생할 때 <scenarioId>.json 을 불러올 폴더. 같은 출처 투어에는 필요 없습니다.
assetBaseUrl
Reserved base URL for resolving bundled UI assets. The current bundle inlines its controls-bar icons and uses system / Pretendard fonts, so it fetches none — you can leave this unset.
번들 UI 에셋을 해석할 기준 URL(예약). 현재 번들은 컨트롤 바 아이콘을 인라인 SVG 로 넣고 시스템·Pretendard 글꼴을 쓰므로 따로 받는 에셋이 없습니다 — 비워두면 됩니다.
tts / ttsVoice
Turn on read-aloud narration and pick a voice. Uses the browser's built-in speechSynthesis; nothing leaves the device.
소리 내어 읽기를 켜고 음성을 고릅니다. 브라우저 내장 speechSynthesis 를 쓰며, 어떤 것도 기기를 벗어나지 않습니다.
allowedParentOrigins
Cross-origin tours only — the parent origins the presence beacon is allowed to answer. See §7.
크로스 오리진 투어 전용 — presence 비콘이 응답해도 되는 부모 출처. §7 참고.
6. Multi-page tours on one site
6. 한 사이트의 여러 페이지를 잇는 투어
A tour can walk a visitor across several pages of the same site —
/ → /player.html →
/privacy.html — and the player handles the page
changes for you. Each step in the scenario carries a
pickedAtUrl (the page its element lives on). When the
tour advances to a step on a different page, the player saves its
place and navigates there; the next page picks the tour right back
up where it left off.
하나의 투어로 같은 사이트의 여러 페이지를 가로지를 수 있습니다 —
/ → /player.html →
/privacy.html — 페이지 전환은 플레이어가 알아서
처리합니다. 시나리오의 각 단계에는 pickedAtUrl(그
요소가 사는 페이지)이 들어 있습니다. 투어가 다른 페이지의 단계로
넘어가면 플레이어가 위치를 저장하고 그쪽으로 이동하며, 다음
페이지에서 멈췄던 자리부터 투어를 이어갑니다.
The one rule: include the bundle on every page the tour
visits. On load it quietly checks for an in-progress tour
and resumes if there is one — so no button is needed on the middle
pages. A typical wiring, shared by all pages:
규칙은 하나: 투어가 들르는 모든 페이지에 번들을
넣으세요. 로드될 때 진행 중인 투어가 있는지 조용히
확인하고 있으면 이어서 재생합니다 — 그래서 중간 페이지에는 버튼이
필요 없어요. 모든 페이지가 공유하는 전형적인 배선:
<!-- on every page the tour can visit -->
<script type="module">
import player from'./player/manuscript-player.0.1.0.esm.js';
// On load the bundle auto-resumes an in-progress tour (sessionStorage).// You only wire a start button on the page the tour begins on:
document.getElementById('start')?.addEventListener('click', async () => {
const scenario = await fetch('./tours/site-tour.json').then(r => r.json());
await player.load(scenario);
await player.play();
});
</script>
<!-- 투어가 들를 수 있는 모든 페이지에 -->
<script type="module">
import player from'./player/manuscript-player.0.1.0.esm.js';
// 로드 시 번들이 진행 중인 투어를 자동으로 이어 재생합니다(sessionStorage).// 시작 버튼은 투어가 시작되는 페이지에만 답니다:
document.getElementById('start')?.addEventListener('click', async () => {
const scenario = await fetch('./tours/site-tour.json').then(r => r.json());
await player.load(scenario);
await player.play();
});
</script>
How it survives the page change. Progress lives in
sessionStorage and the scenario is cached in
localStorage — both are per-origin, so they carry
across same-origin navigations for free. Pressing F5 or
hitting Back ends the tour on purpose (it isn't a forward step), so
a reload never traps the visitor mid-tour.
페이지 전환을 견디는 원리. 진행 상황은
sessionStorage 에, 시나리오는
localStorage 에 저장됩니다 — 둘 다 출처별이라 같은
출처 이동에서는 그대로 따라옵니다. F5 나 뒤로가기는 투어를
의도적으로 끝냅니다(앞으로 가는 단계가 아니므로). 그래서 새로고침이
방문자를 투어 중간에 가둬버리는 일이 없습니다.
7. Crossing to another origin (advanced)
7. 다른 출처로 건너가기 (심화)
If a tour needs to jump from marketing.example.com to
app.example.com, sessionStorage can't
follow — it's per-origin. The player handles this with a
presence handshake: the bundle on the destination origin answers a
hidden probe, and the tour is handed over through a
?mn-player=… URL query parameter (it rides the query,
not the hash, so a host hash router's #/route is left
intact). To allow it, both sides
load the bundle, and the destination lists the source in
allowedParentOrigins. If the destination doesn't
answer, the player shows a small "continue on the other site"
notice instead of navigating blindly. Most users never need this —
one site, one origin, is the common case from §6.
투어가 marketing.example.com 에서
app.example.com 으로 건너뛰어야 한다면,
sessionStorage 는 따라갈 수 없습니다 — 출처별이니까요.
플레이어는 presence 핸드셰이크로 이를 처리합니다: 목적지 출처의
번들이 숨겨진 probe 에 응답하고, 투어는 ?mn-player=…
URL 쿼리 파라미터로 넘겨집니다 (해시가 아니라 쿼리에 실어, 호스트
해시 라우터의 #/route 를 건드리지 않습니다). 허용하려면
양쪽 모두 번들을 불러오고,
목적지는 출처를 allowedParentOrigins 에 등록합니다.
목적지가 응답하지 않으면 무작정 이동하는 대신 "다른 사이트에서
계속" 안내를 띄웁니다. 대부분은 필요 없어요 — §6 의 한 사이트, 한
출처가 일반적입니다.
Limitations
제약
The player carries resume state between pages two ways:
same-origin via sessionStorage, and
cross-origin via the ?mn-player= query
parameter — but only when the player drives the navigation.
플레이어는 페이지 사이 재생 상태를 두 가지로 넘깁니다:
같은 출처는 sessionStorage,
다른 출처는 ?mn-player= 쿼리 파라미터 —
단, 플레이어가 이동을 주도할 때만요.
same-origin
cross-origin
Player-driven
플레이어 주도
sessionStorage
?mn-player= ✅
Site-driven (waitForNavigation)
사이트 주도 (waitForNavigation)
sessionStorage ✅
❌
⚠️ When a waitForNavigation step sends the user to a
different origin via a site button or link, the destination
URL belongs to the site — the player can't attach ?mn-player=, so
the tour won't resume there. A warning is printed to the browser
console at load when this combination is detected.
⚠️ waitForNavigation 스텝에서 사용자가 사이트 버튼이나
링크로 다른 출처로 이동하면, 목적지 URL 은 사이트의 것이라
플레이어가 ?mn-player= 를 붙일 수 없어 그 페이지에서 투어가
이어지지 않습니다. 이 조합이 감지되면 로드 시 브라우저 콘솔에
warning 이 출력됩니다.
✅ Model cross-origin hops as a player-driven step (set the
next step's pickedAtUrl to the other origin) so the player
navigates with the parameter. Keep waitForNavigation for
same-origin interactions.
✅ 다른 출처로 가는 hop 은 플레이어 주도 스텝(다음 스텝의
pickedAtUrl 을 다른 출처로)으로 작성해 플레이어가 파라미터를
붙여 이동하게 하세요. waitForNavigation 은 같은 출처
상호작용용입니다.
8. API reference
8. API 레퍼런스
The default export is the player object. Every method
is a no-op when there's nothing to act on (e.g. calling
next() with no tour running), so you can wire them to
buttons without guarding.
default export 가 player 객체입니다. 모든 메서드는
할 일이 없을 때 no-op 입니다(예: 투어가 없을 때
next() 호출). 그래서 가드 없이 버튼에 바로 연결할 수
있어요.
Method
What it does
메서드
역할
load(scenario)
Validate & cache a scenario (object or JSON string). Call before play().
시나리오(객체 또는 JSON 문자열)를 검증·캐시. play() 전에 호출.
play({ startIndex?, paused? })
Mount the controls and start replay. Defaults: step 0, playing.
컨트롤을 띄우고 재생 시작. 기본값: 0번 단계, 재생 중.
pause()
Toggle pause/resume of the running tour.
진행 중인 투어의 일시정지/재개 토글.
next() / prev()
Step forward / back (navigates pages if the next step lives elsewhere).
앞으로 / 뒤로 한 단계(다음 단계가 다른 페이지면 이동).
jump(index)
Go straight to a step by its index.
인덱스로 특정 단계로 바로 이동.
exit()
End the tour, tear down the controls, clear progress.
투어 종료, 컨트롤 정리, 진행 상황 삭제.
onExit(fn)
Subscribe to "tour ended". Returns an unsubscribe function.
"투어 종료" 구독. 구독 해제 함수를 반환.
resumeIfActive()
Resume an in-progress same-origin tour from sessionStorage. Skipped on F5/Back.
진행 중인 같은 출처 투어를 sessionStorage 에서 이어 재생. F5/뒤로가기에서는 건너뜀.
bootResume()
What the bundle calls on load: a handoff-hash resume first, then resumeIfActive(). You rarely call it yourself.
번들이 로드 시 호출하는 것: 핸드오프 해시 재개를 먼저 시도하고, 그다음 resumeIfActive(). 직접 호출할 일은 드뭅니다.
9. Keyboard during playback
9. 재생 중 키보드
The controls bar is fully keyboard-driven, so a presenter never
has to reach for the mouse:
컨트롤 바는 키보드만으로 조작할 수 있어, 발표자가 마우스를 잡을
필요가 없습니다:
←→ — previous / next step
Space — pause / resume
Esc — exit the tour
←→ — 이전 / 다음 단계
Space — 일시정지 / 재개
Esc — 투어 종료
10. When something doesn't play
10. 잘 재생되지 않을 때
Nothing happens / the console says fetch failed.
You opened the file with file://. Serve it over
http:// instead (§2).
The module fails to import. The ESM bundle pulls
a couple of helper files from its own folder — keep all three
player files together, not just the one
.esm.js.
The spotlight lands in the wrong place, or a "couldn't
find it" note appears. The page changed since the tour
was authored. Manuscript remembers each element three ways and
keeps going, but re-pick that step in the extension and re-export
for a perfect hit.
A multi-page tour stops after the first navigation.
The bundle isn't on the second page. Include it on every page the
tour visits (§6).
No narration. Set tts: true in the
config (§5), and note that browsers only allow speech after a
user gesture — start the tour from a click.
Narration goes quiet on pages the tour auto-advances to.
Browsers block audio on a page opened without a click, so a page
the tour moves to on its own can't start narration. The speaker
turns orange with a bouncing "click anywhere for sound" hint —
click the page to play its narration. Pages the viewer advances
by clicking keep sound. (Same limit applies to the extension's
replay; for a hands-free multi-page tour, expect a click per
auto-opened page.)
아무 일도 없고 콘솔에 fetch 실패가 뜬다.file:// 로 파일을 열었습니다. 대신
http:// 로 띄우세요(§2).
모듈 import 가 실패한다. ESM 번들은 자기 폴더에서
헬퍼 파일 몇 개를 불러옵니다 — .esm.js
하나만이 아니라 세 파일을 같은 폴더에 함께 두세요.
강조 표시가 엉뚱한 곳에 뜨거나 "찾지 못했어요" 메모가
보인다. 투어를 만든 뒤 페이지가 바뀐 것입니다.
Manuscript 는 각 요소를 세 가지 방법으로 기억해 계속 진행하지만,
확장에서 그 단계를 다시 선택하고 다시 내보내면 정확히 맞습니다.
멀티 페이지 투어가 첫 이동 후 멈춘다. 두 번째
페이지에 번들이 없습니다. 투어가 들르는 모든 페이지에 넣으세요(§6).
음성 안내가 없다. 설정에서
tts: true 를 켜세요(§5). 그리고 브라우저는 사용자
동작 이후에만 음성을 허용하므로, 투어를 클릭으로 시작하세요.
투어가 자동으로 넘어가서 연 페이지에서 음성이 끊긴다.
브라우저는 클릭 없이 열린 페이지의 소리를 막습니다. 그래서 투어가
스스로 넘어간 페이지에서는 음성을 시작하지 못해요. 이때 스피커가
주황색으로 바뀌고 "아무 곳이나 클릭하세요" 안내가 통통 튑니다 —
페이지를 클릭하면 그 페이지 음성이 재생됩니다. 보는 사람이 직접
클릭해서 넘긴 페이지는 소리가 이어집니다. (확장 재생에도 동일하게
적용되며, 핸드프리 멀티 페이지 투어라면 자동으로 열린 페이지마다
클릭이 필요합니다.)
11. Try it — right here on this page
11. 바로 이 페이지에서 해보기
This guide embeds the standalone player and tours itself.
Press play and a spotlight walks across the key sections of this
very page — what it is, quick start, the bundle, config, and the
API — in the page's current language, with no extension and
no page navigation. The source that powers it is printed
right below, so you can copy it onto your own page and just swap
the selectors. (Arrow keys move, Space
pauses, Esc exits.)
이 가이드는 스탠드얼론 플레이어를 임베드해 자기 자신을
둘러봅니다. 재생을 누르면 강조 표시가 이 페이지의 핵심 영역 —
소개·빠른 시작·번들·설정·API — 을 차례로 비춥니다.
페이지의 현재 언어로, 확장도 페이지 이동도 없이요.
이를 구동하는 소스가 바로 아래 적혀 있으니, 내 페이지에 복붙하고
셀렉터만 바꾸면 됩니다. (방향키 이동, Space
일시정지, Esc 종료.)
The source applied to this page — a complete, self-contained demo:
이 페이지에 적용된 소스 — 그대로 완결되는 데모입니다:
<!-- the button this guide shows above -->
<button id="mn-page-demo">▶ Play this page's tour</button>
<script type="module">
import player from'./player/manuscript-player.0.1.0.esm.js';
// Narration follows the page language (KO / EN). url is location.href,// so it's right on localhost now and on your live domain after deploy.const ko = () => document.body.getAttribute('lang') === 'ko';
const STEPS = [
['#player-what', 'The standalone player is one small script, and it needs no extension. It replays a saved tour right here on the page.', '스탠드얼론 플레이어는 작은 스크립트 하나예요. 확장 없이도 저장한 시연을 이 페이지에서 그대로 재생합니다.'],
['#player-quickstart', 'To start, you import the bundle, fetch your tour file, then call load and play. That is all it takes.', '시작은 간단합니다. 번들을 가져오고 시연 파일을 불러온 뒤 load 와 play 만 호출하면 됩니다.'],
['#player-bundle', 'You download the bundle right from this site — a single UMD file, or the ESM files together.', '번들은 이 사이트에서 바로 내려받을 수 있어요. 단일 UMD 파일이나 ESM 파일 묶음 중에서 고르면 됩니다.'],
['#player-config', 'Optional settings let you turn on read-aloud narration and choose the voice you like.', '선택 설정에서 소리 내어 읽기를 켜고 원하는 음성을 고를 수 있습니다.'],
['#player-api', 'The API stays small — load, play, pause, and stepping back and forth — and every method is safe to wire to a button.', 'API 는 단출합니다. 재생과 일시정지, 앞뒤 이동 정도이고, 모든 메서드를 버튼에 바로 연결해도 안전합니다.'],
];
function buildTour() {
const k = ko();
return {
schemaVersion: '0.1.1', id: 'player-page-tour',
name: k ? '스탠드얼론 플레이어 둘러보기' : 'A tour of the standalone player',
url: location.href,
steps: STEPS.map(([sel, en, koText], i) => {
const note = k ? koText : en;
return {
id: 'step-' + i, name: note, description: note, thumbnailDataUrl: null,
selectors: { layer1: { kind: 'stable-attr', cssSelector: sel } },
annotations: [{ kind: 'text', id: 'note-' + i, text: note,
position: { x: 52, y: 16 },
style: { fontSize: 15, bold: true, color: '#1a2438',
backgroundColor: '#fff8df', backgroundOpacity: 1 } }],
autoAdvanceMs: 6000, waitForNavigation: false,
};
}),
};
}
// wire the button: load + play (re-enable when the tour ends)const btn = document.getElementById('mn-page-demo');
btn?.addEventListener('click', async () => {
btn.disabled = true;
await player.load(buildTour());
await player.play();
});
player.onExit(() => { btn.disabled = false; });
</script>