hono-decks 1.2.3 → 1.3.0

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.
package/README.ja.md CHANGED
@@ -72,11 +72,13 @@ npm run build
72
72
  ```
73
73
 
74
74
  この連携には standalone compiler/client export を持つアダプターが必要です。
75
- 新規インストールの既定値は `void-plugin-hono-jsx@^0.7.0` です。
75
+ 新規インストールの既定値は `void-plugin-hono-jsx@^0.9.1` です。
76
76
  `--install` は、インストール済みの互換アダプターを維持します。
77
77
  `--install` を省略する場合は、Markdown 用の任意 peer も必要です:
78
- `markdown-exit@^1.3.0 yaml@^2.9.1 markdown-it-container@^4.0.0 shiki@^4.5.0 @shikijs/transformers@^4.5.0`。
79
- 動作確認した組み合わせは Node >=24.22.1 <25、Void 1.2.1、Vite 8.3.4、Hono 4.13.13 です。既存のVoid 1.1.11アプリでは互換性のある0.6アダプターを維持し、未導入の場合も0.6を選択します。0.7では[Void/Cloudflare依存の既知リスク](https://github.com/ts-76/void-plugin-hono-jsx/blob/v0.7.0/docs/dependency-security.md)が残ります。
78
+ `yaml@^2.9.1 shiki@^4.5.0 @shikijs/transformers@^4.5.0`。
79
+
80
+ 新しい導入では `@ts-76/hono-markdown` 0.2.1以降がパーサー2パッケージを自動導入します。利用者側で `markdown-exit`・`markdown-it-container` を追加する必要はありません。既存Void 1.1用の0.6アダプターでは従来のpeer依存をinitが補完します。
81
+ 動作確認した組み合わせは Node >=24.22.1 <25、Void 1.2.1、Vite 8.3.4、Hono 4.13.13 です。既存のVoid 1.1.11アプリでは互換性のある0.6アダプターを維持し、未導入の場合も0.6を選択します。0.9.1では[Void/Cloudflare依存の既知リスク](https://github.com/ts-76/void-plugin-hono-jsx/blob/v0.9.1/docs/dependency-security.md)が残ります。
80
82
 
81
83
  `--install` は npm/pnpm/Yarn/Bun を packageManager または lockfile から判定し、
82
84
  不足する依存だけを追加します。互換性のない既存バージョンは自動更新せずエラーにします。
@@ -292,7 +294,7 @@ export default defineConfig({
292
294
  });
293
295
  ```
294
296
 
295
- pluginはVite起動前にcompileし、deck rootと`hono-decks.config.ts`をVite watcherへ登録します。再compileに成功するとfull reloadを通知するため、手動更新は不要です。どちらの構成でも通常の`bun run dev`だけでauthoringできます。
297
+ pluginはVite起動前にcompileし、deck rootと`hono-decks.config.ts`をVite watcherへ登録します。再compileに成功すると、安全にislandを維持できる場合はスライドの内容だけを更新し、現在のスライド・ステップとislandの状態を保ちます。islandのコード、props、Markdownのコンテキスト、構造などが変わり、安全に引き継げない場合は表示ページを再読み込みし、スライドとステップを復元します。どちらの構成でも通常の`bun run dev`だけでauthoringできます。
296
298
 
297
299
  ## Runtime configuration
298
300
 
package/README.md CHANGED
@@ -71,10 +71,12 @@ npm run build
71
71
  ~~~
72
72
 
73
73
  This integration requires an adapter with the `/markdown/compiler` and
74
- `/markdown/client` exports. New installations default to `void-plugin-hono-jsx@^0.7.0`;
74
+ `/markdown/client` exports. New installations default to `void-plugin-hono-jsx@^0.9.1`;
75
75
  `--install` preserves an already installed compatible adapter. Without `--install`, also install the optional Markdown peers:
76
- `markdown-exit@^1.3.0 yaml@^2.9.1 markdown-it-container@^4.0.0 shiki@^4.5.0 @shikijs/transformers@^4.5.0`.
77
- The verified host versions are Void 1.2.1, Vite 8.3.4, and Hono 4.13.13 on Node >=24.22.1 <25. Existing Void 1.1.11 apps keep their compatible 0.6 adapter; if it is missing, init selects 0.6 rather than forcing a host upgrade. Adapter 0.7 retains the documented [Void/Cloudflare dependency risks](https://github.com/ts-76/void-plugin-hono-jsx/blob/v0.7.0/docs/dependency-security.md).
76
+ `yaml@^2.9.1 shiki@^4.5.0 @shikijs/transformers@^4.5.0`.
77
+
78
+ With `@ts-76/hono-markdown` 0.2.1 or newer, parser dependencies are installed automatically. Do not add `markdown-exit` or `markdown-it-container` to new apps. Existing Void 1.1 apps using adapter 0.6 retain automatic installation of its legacy parser peers.
79
+ The verified host versions are Void 1.2.1, Vite 8.3.4, and Hono 4.13.13 on Node >=24.22.1 <25. Existing Void 1.1.11 apps keep their compatible 0.6 adapter; if it is missing, init selects 0.6 rather than forcing a host upgrade. Adapter 0.9.1 retains the documented [Void/Cloudflare dependency risks](https://github.com/ts-76/void-plugin-hono-jsx/blob/v0.9.1/docs/dependency-security.md).
78
80
 
79
81
  `--install` selects npm/pnpm/Yarn/Bun from packageManager or lockfiles and installs
80
82
  missing dependencies. Incompatible installed versions require an explicit update;
@@ -293,7 +295,7 @@ export default defineConfig({
293
295
  });
294
296
  ~~~
295
297
 
296
- The plugin compiles before Vite starts and adds the deck root and <code>hono-decks.config.ts</code> to the Vite watcher. After a successful recompile, it requests a full browser reload. In both setups, the application's normal <code>dev</code> command is enough for authoring.
298
+ The plugin compiles before Vite starts and adds the deck root and <code>hono-decks.config.ts</code> to the Vite watcher. After a successful recompile, open presentation pages update their deck content without reloading when it is safe to retain the existing islands. The current slide, step, and island state are preserved. If the update cannot safely retain islands (for example, their code, props, Markdown context, or structure changed), the presentation page reloads instead and restores the slide and step. In both setups, the application's normal <code>dev</code> command is enough for authoring.
297
299
 
298
300
  ## Runtime configuration
299
301
 
@@ -467,7 +469,7 @@ At this level, the application owns the mount path, source policy, and option me
467
469
  ## Void Hono Markdown
468
470
 
469
471
  Set `build.markdown: "void"` in `hono-decks.config.ts` to opt into `.md` decks
470
- alongside MDX. This requires `void-plugin-hono-jsx@^0.7.0` with its standalone compiler and client exports.
472
+ alongside MDX. This requires `void-plugin-hono-jsx@^0.9.1` with its standalone compiler and client exports.
471
473
  See the [Void consumer fixture](https://github.com/ts-76/hono-decks/tree/main/fixtures/void-markdown)
472
474
  for package verification, Void middleware hosting, supported syntax and limitations.
473
475
 
package/dist/advanced.js CHANGED
@@ -804,19 +804,94 @@ function escapeHtml$7(value) {
804
804
  function renderLiveReloadScript(eventsPath, nonce) {
805
805
  return `<script${documentNonceAttribute(nonce)}>
806
806
  (() => {
807
- const eventsUrl = ${JSON.stringify(eventsPath)};
808
- const events = new EventSource(eventsUrl);
809
- events.addEventListener("deck:updated", () => location.reload());
807
+ const events = new EventSource(${JSON.stringify(eventsPath)});
808
+ let running = false;
809
+ let pending = false;
810
+ const selector = "[data-hono-decks-island],[data-hono-md-island]";
811
+ const clientSelector = "script[data-hono-decks-client-entry]";
812
+ const clientText = async (url) => {
813
+ if (!url) return "";
814
+ const response = await fetch(url, { cache: "no-store" });
815
+ if (!response.ok) throw new Error("Client entry fetch failed");
816
+ return response.text();
817
+ };
818
+ const domReady = document.readyState === "loading"
819
+ ? new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }))
820
+ : Promise.resolve();
821
+ let clientSnapshot = domReady.then(() => clientText(document.querySelector(clientSelector)?.src)).catch(() => null);
822
+
823
+ function signature(element) {
824
+ const attributes = element.hasAttribute("data-hono-md-island")
825
+ ? ["data-hono-md-island", "data-hono-md-hydrate", "data-hono-md-props"]
826
+ : ["data-hono-decks-island", "data-hono-decks-props"];
827
+ return JSON.stringify([attributes.map((name) => element.getAttribute(name)),
828
+ element.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") ?? null]);
829
+ }
830
+
831
+ async function update() {
832
+ const response = await fetch(location.href, { cache: "no-store" });
833
+ if (!response.ok || !response.headers.get("content-type")?.includes("text/html")) throw new Error("Deck fetch failed");
834
+ const next = new DOMParser().parseFromString(await response.text(), "text/html");
835
+ const currentDeck = document.querySelector("[data-hono-decks-deck]");
836
+ const nextDeck = next.querySelector("[data-hono-decks-deck]");
837
+ const style = document.querySelector("[data-hono-decks-presentation-style]");
838
+ const nextStyle = next.querySelector("[data-hono-decks-presentation-style]");
839
+ const runtime = window.__honoDecksPresentationRuntime;
840
+ if (!currentDeck || !nextDeck || !style || !nextStyle || !runtime?.refresh) throw new Error("Invalid deck document");
841
+ const oldSlides = Array.from(currentDeck.querySelectorAll(".slide"));
842
+ const newSlides = Array.from(nextDeck.querySelectorAll(".slide"));
843
+ if (!newSlides.length) throw new Error("Empty deck");
844
+ // Scripts parsed from fetched HTML are inert; reload so their behavior is not lost.
845
+ if (currentDeck.querySelector("script") || nextDeck.querySelector("script")) throw new Error("Deck contains scripts");
846
+ const oldIslands = oldSlides.flatMap((slide) => Array.from(slide.querySelectorAll(selector)));
847
+ const nextScript = next.querySelector(clientSelector);
848
+ const nextClient = await clientText(nextScript?.getAttribute("src"));
849
+ const previousClient = await clientSnapshot;
850
+ if (previousClient === null) throw new Error("Initial client entry fetch failed");
851
+ // Changed client code cannot safely replace a running component while retaining its state.
852
+ if (oldIslands.length && previousClient !== nextClient) throw new Error("Client code changed");
853
+ const pairs = [];
854
+ if (oldIslands.length && oldSlides.length !== newSlides.length) throw new Error("Slide structure changed");
855
+ for (let i = 0; i < oldSlides.length && i < newSlides.length; i++) {
856
+ const before = Array.from(oldSlides[i].querySelectorAll(selector));
857
+ const after = Array.from(newSlides[i].querySelectorAll(selector));
858
+ if (before.length !== after.length) throw new Error("Island structure changed");
859
+ for (let j = 0; j < before.length; j++) {
860
+ if (signature(before[j]) !== signature(after[j])) throw new Error("Island properties changed");
861
+ // Deferred Markdown islands observe their original slide. Reload rather than strand the observer.
862
+ if (before[j].hasAttribute("data-hono-md-island") && !before[j].hasAttribute("data-hono-md-mounted"))
863
+ throw new Error("Island is pending hydration");
864
+ pairs.push([before[j], after[j]]);
865
+ }
866
+ }
867
+ if (!oldIslands.length && nextDeck.querySelector(selector)) throw new Error("New islands require initialization");
868
+ for (const [oldIsland, newIsland] of pairs) newIsland.replaceWith(oldIsland);
869
+ currentDeck.replaceChildren(...nextDeck.childNodes);
870
+ style.textContent = nextStyle.textContent;
871
+ document.title = next.title;
872
+ clientSnapshot = Promise.resolve(nextClient);
873
+ runtime.refresh();
874
+ }
875
+
876
+ async function onUpdate() {
877
+ if (running) { pending = true; return; }
878
+ running = true;
879
+ try {
880
+ do { pending = false; await update(); } while (pending);
881
+ } catch (error) { console.warn("[hono-decks] Live update failed; reloading", error); location.reload(); }
882
+ finally { running = false; }
883
+ }
884
+ events.addEventListener("deck:updated", onUpdate);
810
885
  })();
811
886
  <\/script>`;
812
887
  }
813
888
  function renderClientEntryScript(clientEntry, nonce) {
814
- return `<script type="module" src="${escapeHtml$6(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
889
+ return `<script data-hono-decks-client-entry type="module" src="${escapeHtml$6(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
815
890
  }
816
891
  function renderPresentationScript(nonce) {
817
892
  return `<script${documentNonceAttribute(nonce)}>
818
893
  (() => {
819
- const slides = Array.from(document.querySelectorAll(".slide"));
894
+ let slides = Array.from(document.querySelectorAll(".slide"));
820
895
  const stage = document.querySelector("[data-hono-decks-stage]");
821
896
  const deck = document.querySelector("[data-hono-decks-deck]");
822
897
  const DESIGN_WIDTH = ${PRESENTATION_DESIGN_WIDTH};
@@ -1090,6 +1165,7 @@ function renderPresentationScript(nonce) {
1090
1165
  updateFires(nextStepIndex);
1091
1166
  publishState();
1092
1167
  requestAnimationFrame(() => {
1168
+ if (token !== transitionToken) return;
1093
1169
  setSlideState(outgoing, "leaving", direction, transition, timing);
1094
1170
  setSlideState(incoming, "active", direction, transition, timing);
1095
1171
  waitForSlideTransition(outgoing, incoming, direction, token);
@@ -1196,13 +1272,38 @@ function renderPresentationScript(nonce) {
1196
1272
  await document.documentElement.requestFullscreen?.();
1197
1273
  }
1198
1274
 
1199
- slides.forEach((slide, slideIndex) => {
1200
- slide.addEventListener("click", () => {
1201
- if (!document.body.hasAttribute("data-overview-mode")) return;
1202
- document.body.removeAttribute("data-overview-mode");
1203
- show(slideIndex);
1275
+ function bindSlideClicks() {
1276
+ slides.forEach((slide, slideIndex) => {
1277
+ slide.addEventListener("click", () => {
1278
+ if (!document.body.hasAttribute("data-overview-mode")) return;
1279
+ document.body.removeAttribute("data-overview-mode");
1280
+ show(slideIndex);
1281
+ });
1204
1282
  });
1205
- });
1283
+ }
1284
+
1285
+ function refresh() {
1286
+ transitionToken += 1;
1287
+ isTransitioning = false;
1288
+ pendingNavigation = null;
1289
+ slides = Array.from(deck?.querySelectorAll(".slide") ?? []);
1290
+ bindSlideClicks();
1291
+ if (document.body.hasAttribute("data-overview-mode")) {
1292
+ slides.forEach((slide) => { slide.hidden = false; slide.setAttribute("data-slide-state", "active"); });
1293
+ setFiresVisible(Array.from(deck?.querySelectorAll("[data-hono-decks-fire]") ?? []), true);
1294
+ index = Math.max(0, Math.min(slides.length - 1, index));
1295
+ stepCount = fireCountForSlide(index);
1296
+ stepIndex = Math.max(0, Math.min(stepCount, stepIndex));
1297
+ publishState();
1298
+ } else {
1299
+ applyInstantSlideChange(index, stepIndex);
1300
+ }
1301
+ document.querySelectorAll(".speaker-notes").forEach((note) => {
1302
+ note.hidden = !document.body.hasAttribute("data-presenter-mode");
1303
+ });
1304
+ fitDeck();
1305
+ }
1306
+ bindSlideClicks();
1206
1307
 
1207
1308
  document.addEventListener("keydown", (event) => {
1208
1309
  if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
@@ -1225,7 +1326,7 @@ function renderPresentationScript(nonce) {
1225
1326
  handleCommand(message.action, message.index, message.stepIndex);
1226
1327
  });
1227
1328
 
1228
- window.__honoDecksPresentationRuntime = { command: handleCommand };
1329
+ window.__honoDecksPresentationRuntime = { command: handleCommand, refresh };
1229
1330
 
1230
1331
  window.addEventListener("resize", fitDeck);
1231
1332
  if (typeof ResizeObserver !== "undefined" && stage instanceof HTMLElement) {
@@ -1261,7 +1362,7 @@ function renderCompiledDeckPage(input) {
1261
1362
  <meta charset="utf-8" />
1262
1363
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1263
1364
  <title>${escapeHtml$5(presentationPageTitle(deck))}</title>
1264
- <style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
1365
+ <style data-hono-decks-presentation-style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
1265
1366
  ${document.head}
1266
1367
  </head>
1267
1368
  <body${bodyAttrs.length ? ` ${bodyAttrs.join(" ")}` : ""}>
@@ -1292,7 +1393,7 @@ async function renderCompiledDeckPageAsync(input) {
1292
1393
  <meta charset="utf-8" />
1293
1394
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1294
1395
  <title>${escapeHtml$5(presentationPageTitle(deck))}</title>
1295
- <style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
1396
+ <style data-hono-decks-presentation-style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
1296
1397
  ${document.head}
1297
1398
  </head>
1298
1399
  <body${bodyAttrs.length ? ` ${bodyAttrs.join(" ")}` : ""}>
package/dist/bin.js CHANGED
@@ -2350,7 +2350,7 @@ async function runInitCommand(input, args, stdout, stderr) {
2350
2350
  return { exitCode: 1 };
2351
2351
  }
2352
2352
  if (parsed.options.framework) try {
2353
- const { initializeVite } = await import("./init-vite-abCCwOcl.js");
2353
+ const { initializeVite } = await import("./init-vite-BPRPonCD.js");
2354
2354
  const sampleSlug = await initializeVite(input.cwd, parsed.options.framework, parsed.options.configFile, parsed.options.entry, parsed.options.install ? { adapter: parsed.options.adapter } : void 0);
2355
2355
  stdout(`Initialized ${parsed.options.framework} decks at /slides/${sampleSlug}. Run your usual dev or build command; no manual compile is needed.`);
2356
2356
  return { exitCode: 0 };
package/dist/cli.js CHANGED
@@ -2349,7 +2349,7 @@ async function runInitCommand(input, args, stdout, stderr) {
2349
2349
  return { exitCode: 1 };
2350
2350
  }
2351
2351
  if (parsed.options.framework) try {
2352
- const { initializeVite } = await import("./init-vite-abCCwOcl.js");
2352
+ const { initializeVite } = await import("./init-vite-BPRPonCD.js");
2353
2353
  const sampleSlug = await initializeVite(input.cwd, parsed.options.framework, parsed.options.configFile, parsed.options.entry, parsed.options.install ? { adapter: parsed.options.adapter } : void 0);
2354
2354
  stdout(`Initialized ${parsed.options.framework} decks at /slides/${sampleSlug}. Run your usual dev or build command; no manual compile is needed.`);
2355
2355
  return { exitCode: 0 };
@@ -12,12 +12,12 @@ const voidDependencies = [
12
12
  ["void", "void@>=1.2.1 <1.3.0"],
13
13
  ["vite", "vite@^8.0.16"],
14
14
  ["hono", "hono@^4.13.13"],
15
- ["markdown-exit", "markdown-exit@^1.3.0"],
16
15
  ["yaml", "yaml@^2.9.1"],
17
- ["markdown-it-container", "markdown-it-container@^4.0.0"],
18
16
  ["shiki", "shiki@^4.5.0"],
19
17
  ["@shikijs/transformers", "@shikijs/transformers@^4.5.0"]
20
18
  ];
19
+ /** Pre-0.9 adapters on existing Void 1.1 hosts still use parser peers. */
20
+ const legacyMarkdownDependencies = [["markdown-exit", "markdown-exit@^1.3.0"], ["markdown-it-container", "markdown-it-container@^4.0.0"]];
21
21
  //#endregion
22
22
  //#region src/node/install-void.ts
23
23
  async function installVoidDependencies(cwd, options = {}) {
@@ -46,8 +46,9 @@ async function installVoidDependencies(cwd, options = {}) {
46
46
  return true;
47
47
  }
48
48
  };
49
- const required = voidDependencies;
50
- let legacyVoid = false;
49
+ const hostVersion = missing("void") ? void 0 : await installedVersion(require.resolve("void"), "void");
50
+ let legacyVoid = /^1\.1\./.test(hostVersion ?? "");
51
+ const required = [...voidDependencies, ...legacyVoid ? legacyMarkdownDependencies : []];
51
52
  for (const [id, spec] of required) {
52
53
  if (missing(id)) continue;
53
54
  const version = await installedVersion(require.resolve(id), id);
@@ -83,7 +84,7 @@ async function installVoidDependencies(cwd, options = {}) {
83
84
  specs.push(`hono-decks@${requested}`);
84
85
  }
85
86
  if (options.adapter || missing("void-plugin-hono-jsx/markdown/compiler")) {
86
- let adapter = options.adapter ?? `void-plugin-hono-jsx@${legacyVoid ? "^0.6.0" : "^0.7.0"}`;
87
+ let adapter = options.adapter ?? `void-plugin-hono-jsx@${legacyVoid ? "^0.6.0" : "^0.9.1"}`;
87
88
  if (/^\d+\./.test(adapter)) adapter = "void-plugin-hono-jsx@" + adapter;
88
89
  else if (modernYarn && (adapter.endsWith(".tgz") || adapter.startsWith("file:"))) adapter = "void-plugin-hono-jsx@file:" + resolve(cwd, adapter.replace(/^file:/, ""));
89
90
  specs.push(adapter);
package/dist/node.js CHANGED
@@ -3523,19 +3523,94 @@ function escapeHtml$8(value) {
3523
3523
  function renderLiveReloadScript(eventsPath, nonce) {
3524
3524
  return `<script${documentNonceAttribute(nonce)}>
3525
3525
  (() => {
3526
- const eventsUrl = ${JSON.stringify(eventsPath)};
3527
- const events = new EventSource(eventsUrl);
3528
- events.addEventListener("deck:updated", () => location.reload());
3526
+ const events = new EventSource(${JSON.stringify(eventsPath)});
3527
+ let running = false;
3528
+ let pending = false;
3529
+ const selector = "[data-hono-decks-island],[data-hono-md-island]";
3530
+ const clientSelector = "script[data-hono-decks-client-entry]";
3531
+ const clientText = async (url) => {
3532
+ if (!url) return "";
3533
+ const response = await fetch(url, { cache: "no-store" });
3534
+ if (!response.ok) throw new Error("Client entry fetch failed");
3535
+ return response.text();
3536
+ };
3537
+ const domReady = document.readyState === "loading"
3538
+ ? new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }))
3539
+ : Promise.resolve();
3540
+ let clientSnapshot = domReady.then(() => clientText(document.querySelector(clientSelector)?.src)).catch(() => null);
3541
+
3542
+ function signature(element) {
3543
+ const attributes = element.hasAttribute("data-hono-md-island")
3544
+ ? ["data-hono-md-island", "data-hono-md-hydrate", "data-hono-md-props"]
3545
+ : ["data-hono-decks-island", "data-hono-decks-props"];
3546
+ return JSON.stringify([attributes.map((name) => element.getAttribute(name)),
3547
+ element.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") ?? null]);
3548
+ }
3549
+
3550
+ async function update() {
3551
+ const response = await fetch(location.href, { cache: "no-store" });
3552
+ if (!response.ok || !response.headers.get("content-type")?.includes("text/html")) throw new Error("Deck fetch failed");
3553
+ const next = new DOMParser().parseFromString(await response.text(), "text/html");
3554
+ const currentDeck = document.querySelector("[data-hono-decks-deck]");
3555
+ const nextDeck = next.querySelector("[data-hono-decks-deck]");
3556
+ const style = document.querySelector("[data-hono-decks-presentation-style]");
3557
+ const nextStyle = next.querySelector("[data-hono-decks-presentation-style]");
3558
+ const runtime = window.__honoDecksPresentationRuntime;
3559
+ if (!currentDeck || !nextDeck || !style || !nextStyle || !runtime?.refresh) throw new Error("Invalid deck document");
3560
+ const oldSlides = Array.from(currentDeck.querySelectorAll(".slide"));
3561
+ const newSlides = Array.from(nextDeck.querySelectorAll(".slide"));
3562
+ if (!newSlides.length) throw new Error("Empty deck");
3563
+ // Scripts parsed from fetched HTML are inert; reload so their behavior is not lost.
3564
+ if (currentDeck.querySelector("script") || nextDeck.querySelector("script")) throw new Error("Deck contains scripts");
3565
+ const oldIslands = oldSlides.flatMap((slide) => Array.from(slide.querySelectorAll(selector)));
3566
+ const nextScript = next.querySelector(clientSelector);
3567
+ const nextClient = await clientText(nextScript?.getAttribute("src"));
3568
+ const previousClient = await clientSnapshot;
3569
+ if (previousClient === null) throw new Error("Initial client entry fetch failed");
3570
+ // Changed client code cannot safely replace a running component while retaining its state.
3571
+ if (oldIslands.length && previousClient !== nextClient) throw new Error("Client code changed");
3572
+ const pairs = [];
3573
+ if (oldIslands.length && oldSlides.length !== newSlides.length) throw new Error("Slide structure changed");
3574
+ for (let i = 0; i < oldSlides.length && i < newSlides.length; i++) {
3575
+ const before = Array.from(oldSlides[i].querySelectorAll(selector));
3576
+ const after = Array.from(newSlides[i].querySelectorAll(selector));
3577
+ if (before.length !== after.length) throw new Error("Island structure changed");
3578
+ for (let j = 0; j < before.length; j++) {
3579
+ if (signature(before[j]) !== signature(after[j])) throw new Error("Island properties changed");
3580
+ // Deferred Markdown islands observe their original slide. Reload rather than strand the observer.
3581
+ if (before[j].hasAttribute("data-hono-md-island") && !before[j].hasAttribute("data-hono-md-mounted"))
3582
+ throw new Error("Island is pending hydration");
3583
+ pairs.push([before[j], after[j]]);
3584
+ }
3585
+ }
3586
+ if (!oldIslands.length && nextDeck.querySelector(selector)) throw new Error("New islands require initialization");
3587
+ for (const [oldIsland, newIsland] of pairs) newIsland.replaceWith(oldIsland);
3588
+ currentDeck.replaceChildren(...nextDeck.childNodes);
3589
+ style.textContent = nextStyle.textContent;
3590
+ document.title = next.title;
3591
+ clientSnapshot = Promise.resolve(nextClient);
3592
+ runtime.refresh();
3593
+ }
3594
+
3595
+ async function onUpdate() {
3596
+ if (running) { pending = true; return; }
3597
+ running = true;
3598
+ try {
3599
+ do { pending = false; await update(); } while (pending);
3600
+ } catch (error) { console.warn("[hono-decks] Live update failed; reloading", error); location.reload(); }
3601
+ finally { running = false; }
3602
+ }
3603
+ events.addEventListener("deck:updated", onUpdate);
3529
3604
  })();
3530
3605
  <\/script>`;
3531
3606
  }
3532
3607
  function renderClientEntryScript(clientEntry, nonce) {
3533
- return `<script type="module" src="${escapeHtml$7(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
3608
+ return `<script data-hono-decks-client-entry type="module" src="${escapeHtml$7(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
3534
3609
  }
3535
3610
  function renderPresentationScript(nonce) {
3536
3611
  return `<script${documentNonceAttribute(nonce)}>
3537
3612
  (() => {
3538
- const slides = Array.from(document.querySelectorAll(".slide"));
3613
+ let slides = Array.from(document.querySelectorAll(".slide"));
3539
3614
  const stage = document.querySelector("[data-hono-decks-stage]");
3540
3615
  const deck = document.querySelector("[data-hono-decks-deck]");
3541
3616
  const DESIGN_WIDTH = ${PRESENTATION_DESIGN_WIDTH};
@@ -3809,6 +3884,7 @@ function renderPresentationScript(nonce) {
3809
3884
  updateFires(nextStepIndex);
3810
3885
  publishState();
3811
3886
  requestAnimationFrame(() => {
3887
+ if (token !== transitionToken) return;
3812
3888
  setSlideState(outgoing, "leaving", direction, transition, timing);
3813
3889
  setSlideState(incoming, "active", direction, transition, timing);
3814
3890
  waitForSlideTransition(outgoing, incoming, direction, token);
@@ -3915,13 +3991,38 @@ function renderPresentationScript(nonce) {
3915
3991
  await document.documentElement.requestFullscreen?.();
3916
3992
  }
3917
3993
 
3918
- slides.forEach((slide, slideIndex) => {
3919
- slide.addEventListener("click", () => {
3920
- if (!document.body.hasAttribute("data-overview-mode")) return;
3921
- document.body.removeAttribute("data-overview-mode");
3922
- show(slideIndex);
3994
+ function bindSlideClicks() {
3995
+ slides.forEach((slide, slideIndex) => {
3996
+ slide.addEventListener("click", () => {
3997
+ if (!document.body.hasAttribute("data-overview-mode")) return;
3998
+ document.body.removeAttribute("data-overview-mode");
3999
+ show(slideIndex);
4000
+ });
3923
4001
  });
3924
- });
4002
+ }
4003
+
4004
+ function refresh() {
4005
+ transitionToken += 1;
4006
+ isTransitioning = false;
4007
+ pendingNavigation = null;
4008
+ slides = Array.from(deck?.querySelectorAll(".slide") ?? []);
4009
+ bindSlideClicks();
4010
+ if (document.body.hasAttribute("data-overview-mode")) {
4011
+ slides.forEach((slide) => { slide.hidden = false; slide.setAttribute("data-slide-state", "active"); });
4012
+ setFiresVisible(Array.from(deck?.querySelectorAll("[data-hono-decks-fire]") ?? []), true);
4013
+ index = Math.max(0, Math.min(slides.length - 1, index));
4014
+ stepCount = fireCountForSlide(index);
4015
+ stepIndex = Math.max(0, Math.min(stepCount, stepIndex));
4016
+ publishState();
4017
+ } else {
4018
+ applyInstantSlideChange(index, stepIndex);
4019
+ }
4020
+ document.querySelectorAll(".speaker-notes").forEach((note) => {
4021
+ note.hidden = !document.body.hasAttribute("data-presenter-mode");
4022
+ });
4023
+ fitDeck();
4024
+ }
4025
+ bindSlideClicks();
3925
4026
 
3926
4027
  document.addEventListener("keydown", (event) => {
3927
4028
  if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
@@ -3944,7 +4045,7 @@ function renderPresentationScript(nonce) {
3944
4045
  handleCommand(message.action, message.index, message.stepIndex);
3945
4046
  });
3946
4047
 
3947
- window.__honoDecksPresentationRuntime = { command: handleCommand };
4048
+ window.__honoDecksPresentationRuntime = { command: handleCommand, refresh };
3948
4049
 
3949
4050
  window.addEventListener("resize", fitDeck);
3950
4051
  if (typeof ResizeObserver !== "undefined" && stage instanceof HTMLElement) {
@@ -3980,7 +4081,7 @@ async function renderCompiledDeckPageAsync(input) {
3980
4081
  <meta charset="utf-8" />
3981
4082
  <meta name="viewport" content="width=device-width, initial-scale=1" />
3982
4083
  <title>${escapeHtml$6(presentationPageTitle(deck))}</title>
3983
- <style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
4084
+ <style data-hono-decks-presentation-style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
3984
4085
  ${document.head}
3985
4086
  </head>
3986
4087
  <body${bodyAttrs.length ? ` ${bodyAttrs.join(" ")}` : ""}>
package/dist/vite.js CHANGED
@@ -2307,6 +2307,8 @@ function honoDecks(options = {}) {
2307
2307
  let mountChanged = false;
2308
2308
  let modules = /* @__PURE__ */ new Map();
2309
2309
  let routerModulePath = "";
2310
+ const eventClients = /* @__PURE__ */ new Set();
2311
+ let eventsPath = "/@hono-decks/events";
2310
2312
  const syncWatchedPaths = async () => {
2311
2313
  if (!server) return;
2312
2314
  const next = /* @__PURE__ */ new Set([
@@ -2374,7 +2376,7 @@ function honoDecks(options = {}) {
2374
2376
  environment.moduleGraph.invalidateAll();
2375
2377
  if (environment.name !== "client") environment.hot.send({ type: "full-reload" });
2376
2378
  }
2377
- server.ws.send({ type: "full-reload" });
2379
+ for (const client of eventClients) client.write("event: deck:updated\ndata: {}\n\n");
2378
2380
  }
2379
2381
  } finally {
2380
2382
  compiling = false;
@@ -2422,8 +2424,9 @@ export { createDecks };
2422
2424
  export const decks = createDecks(config);`;
2423
2425
  if (!id.startsWith(MODULE_PREFIX)) return null;
2424
2426
  const path = id.slice(MODULE_PREFIX.length);
2425
- const code = modules.get(path);
2427
+ let code = modules.get(path);
2426
2428
  if (code === void 0) return null;
2429
+ if (path === routerModulePath && resolvedConfig?.command === "serve") code = code.replace("const generatedDecks = defineDecks({", `const generatedDecks = defineDecks({\n liveReloadPath: () => ${JSON.stringify(eventsPath)},`);
2427
2430
  return (await transform(code, {
2428
2431
  loader: "ts",
2429
2432
  format: "esm",
@@ -2436,16 +2439,30 @@ export const decks = createDecks(config);`;
2436
2439
  },
2437
2440
  async configResolved(config) {
2438
2441
  resolvedConfig = config;
2442
+ eventsPath = `${config.base?.startsWith("/") ? config.base.replace(/\/$/, "") : ""}/@hono-decks/events`;
2439
2443
  projectRoot = config.resolve?.preserveSymlinks ? config.root : await realpath(config.root);
2440
2444
  configPath = resolve(projectRoot, options.configFile ?? "hono-decks.config.ts");
2441
2445
  await compile(true);
2442
2446
  },
2443
2447
  configureServer(devServer) {
2444
2448
  server = devServer;
2449
+ devServer.middlewares.use((req, res, next) => {
2450
+ if (req.url?.split("?")[0] !== eventsPath || req.method !== "GET") return next();
2451
+ res.writeHead(200, {
2452
+ "Content-Type": "text/event-stream",
2453
+ "Cache-Control": "no-cache, no-transform",
2454
+ Connection: "keep-alive"
2455
+ });
2456
+ res.write(": connected\n\n");
2457
+ eventClients.add(res);
2458
+ res.on("close", () => eventClients.delete(res));
2459
+ });
2445
2460
  syncWatchedPaths();
2446
2461
  devServer.watcher.on("all", onWatcherEvent);
2447
2462
  const cleanup = () => {
2448
2463
  if (timer) clearTimeout(timer);
2464
+ for (const client of eventClients) client.end();
2465
+ eventClients.clear();
2449
2466
  devServer.watcher.off("all", onWatcherEvent);
2450
2467
  server = void 0;
2451
2468
  watchedPaths.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-decks",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Slide decks for Hono and Cloudflare Workers",
5
5
  "keywords": [
6
6
  "cloudflare-workers",