hono-decks 1.2.4 → 1.4.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
@@ -294,7 +294,7 @@ export default defineConfig({
294
294
  });
295
295
  ```
296
296
 
297
- 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を安全に識別できる場合は表示ページを再読み込みせずに内容を更新します。対応するprops・Markdownコンテキストの変更、別々のislandの並べ替え・追加・削除でも状態を維持し、現在のスライド・ステップを保ちます。クライアントコードの変更や同一islandの区別がつかない場合など、安全に更新できないときは表示ページを再読み込みしてスライドとステップを復元します。このときislandの状態はリセットされます。どちらの構成でも通常の`bun run dev`だけでauthoringできます。
298
298
 
299
299
  ## Runtime configuration
300
300
 
package/README.md CHANGED
@@ -295,7 +295,7 @@ export default defineConfig({
295
295
  });
296
296
  ~~~
297
297
 
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, 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, presentation pages update without reloading when existing islands can be identified safely. Their state survives supported prop and Markdown context changes, reordering, and the addition or removal of distinct islands. The current slide and step are retained. When an update cannot be applied safely, such as a changed client bundle or ambiguous duplicate islands, the presentation page reloads and restores the slide and step; island state is then reset. In both setups, the application's normal <code>dev</code> command is enough for authoring.
299
299
 
300
300
  ## Runtime configuration
301
301
 
@@ -31,8 +31,37 @@ function createMarkdownAdapter({ cwd, options }) {
31
31
  function emitMarkdownClient(islands, runtime) {
32
32
  if (!islands.length) return "";
33
33
  const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
34
+ const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
35
+ const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
36
+ const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
37
+ const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
34
38
  return `import { hydrateMarkdownIslands } from ${literal(runtime)};
39
+ import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
40
+ import { MarkdownContext } from ${literal(contextModule)};
41
+ import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
42
+ import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
43
+ function hydrateDeckMarkdownIslands() {
35
44
  hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
45
+ schedule(element, loader, props, strategy, mount) {
46
+ return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
47
+ const live = window.__honoDecksLive;
48
+ if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
49
+ const Component = module.default;
50
+ function LiveMarkdownIsland(componentProps) {
51
+ const [current, setCurrent] = useLiveState({
52
+ props: componentProps,
53
+ context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
54
+ });
55
+ const [active, setActive] = useLiveState(true);
56
+ live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
57
+ return active ? liveJsx(MarkdownContext.Provider, {
58
+ value: current.context,
59
+ children: liveJsx(Component, current.props),
60
+ }) : null;
61
+ }
62
+ return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
63
+ });
64
+ },
36
65
  ready(element, start) {
37
66
  const slide = element.closest('section[data-slide-state]');
38
67
  if (!slide || slide.dataset.slideState === 'active') { start(); return; }
@@ -41,7 +70,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
41
70
  });
42
71
  observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
43
72
  }
44
- });`;
73
+ });
74
+ }
75
+ hydrateDeckMarkdownIslands();
76
+ if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
45
77
  }
46
78
  //#endregion
47
79
  export { createHonoMarkdownAdapter, createMarkdownAdapter };
package/dist/advanced.js CHANGED
@@ -804,19 +804,209 @@ 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
+ const scriptNonce = document.currentScript?.nonce;
809
+ let running = false;
810
+ let pending = false;
811
+ const selector = "[data-hono-decks-island],[data-hono-md-island]";
812
+ const islandUpdaters = new WeakMap();
813
+ const hydrators = new Set();
814
+ window.__honoDecksLive = {
815
+ register: (element, update, dispose) => islandUpdaters.set(element, { update, dispose }),
816
+ registerHydrator: (hydrate) => hydrators.add(hydrate),
817
+ };
818
+ const clientSelector = "script[data-hono-decks-client-entry]";
819
+ const clientText = async (url) => {
820
+ if (!url) return "";
821
+ const response = await fetch(url, { cache: "no-store" });
822
+ if (!response.ok) throw new Error("Client entry fetch failed");
823
+ return response.text();
824
+ };
825
+ const domReady = document.readyState === "loading"
826
+ ? new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }))
827
+ : Promise.resolve();
828
+ let clientSnapshot = domReady.then(() => clientText(document.querySelector(clientSelector)?.src)).catch(() => null);
829
+
830
+ function signature(element, includeContext = true, includeProps = true) {
831
+ const attributes = element.hasAttribute("data-hono-md-island")
832
+ ? ["data-hono-md-island", "data-hono-md-hydrate", "data-hono-md-props"]
833
+ : ["data-hono-decks-island", "data-hono-decks-props"];
834
+ return JSON.stringify([attributes.map((name) => !includeProps && name.endsWith("-props") ? null : element.getAttribute(name)),
835
+ includeContext ? element.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") ?? null : null]);
836
+ }
837
+
838
+ async function update() {
839
+ const response = await fetch(location.href, { cache: "no-store" });
840
+ if (!response.ok || !response.headers.get("content-type")?.includes("text/html")) throw new Error("Deck fetch failed");
841
+ const next = new DOMParser().parseFromString(await response.text(), "text/html");
842
+ const currentDeck = document.querySelector("[data-hono-decks-deck]");
843
+ const nextDeck = next.querySelector("[data-hono-decks-deck]");
844
+ const style = document.querySelector("[data-hono-decks-presentation-style]");
845
+ const nextStyle = next.querySelector("[data-hono-decks-presentation-style]");
846
+ const runtime = window.__honoDecksPresentationRuntime;
847
+ if (!currentDeck || !nextDeck || !style || !nextStyle || !runtime?.refresh) throw new Error("Invalid deck document");
848
+ const oldSlides = Array.from(currentDeck.querySelectorAll(".slide"));
849
+ const newSlides = Array.from(nextDeck.querySelectorAll(".slide"));
850
+ if (!newSlides.length) throw new Error("Empty deck");
851
+ // Scripts parsed from fetched HTML are inert; reload so their behavior is not lost.
852
+ if (currentDeck.querySelector("script") || nextDeck.querySelector("script")) throw new Error("Deck contains scripts");
853
+ const collect = (slides) => slides.flatMap((slide, slideIndex) =>
854
+ Array.from(slide.querySelectorAll(selector)).map((element, position) => ({ element, slideIndex, position })));
855
+ const oldItems = collect(oldSlides);
856
+ const newItems = collect(newSlides);
857
+ const nextScript = next.querySelector(clientSelector);
858
+ const fetchedClient = await clientText(nextScript?.getAttribute("src"));
859
+ const previousClient = await clientSnapshot;
860
+ if (previousClient === null) throw new Error("Initial client entry fetch failed");
861
+ // Keep the loaded registry when the last island disappears; it can dispose
862
+ // that island and initialize it again if it is reintroduced later.
863
+ const nextClient = !nextScript && !newItems.length ? previousClient : fetchedClient;
864
+ // A changed client bundle could contain new implementations for running islands.
865
+ if (oldItems.length && previousClient !== nextClient) throw new Error("Client code changed");
866
+ const needsEntry = !document.querySelector(clientSelector) && Boolean(nextScript);
867
+ if (newItems.length && !nextScript) throw new Error("New islands require a client entry");
868
+ if (!needsEntry && previousClient !== nextClient) throw new Error("Client code changed");
869
+
870
+ const duplicateKeys = new Set([...oldItems, ...newItems].map((item) => signature(item.element, false)));
871
+ for (const key of duplicateKeys) {
872
+ const before = oldItems.filter((item) => signature(item.element, false) === key);
873
+ const after = newItems.filter((item) => signature(item.element, false) === key);
874
+ if (before.length <= 1 && after.length <= 1) continue;
875
+ if (before.length !== after.length || before.some((item, index) =>
876
+ item.slideIndex !== after[index].slideIndex || item.position !== after[index].position))
877
+ throw new Error("Ambiguous duplicate islands");
878
+ }
879
+ const pairs = [];
880
+ const remainingOld = new Set(oldItems);
881
+ const remainingNew = new Set(newItems);
882
+ function pair(oldItem, newItem) {
883
+ remainingOld.delete(oldItem);
884
+ remainingNew.delete(newItem);
885
+ pairs.push([oldItem, newItem]);
886
+ }
887
+ // Exact matches can move between slides. Match only unique instances so
888
+ // duplicate islands cannot accidentally inherit each other's state.
889
+ for (const oldItem of oldItems) {
890
+ const key = signature(oldItem.element, false);
891
+ const oldMatches = Array.from(remainingOld).filter((item) => signature(item.element, false) === key);
892
+ const newMatches = Array.from(remainingNew).filter((item) => signature(item.element, false) === key);
893
+ if (oldMatches.length === 1 && newMatches.length === 1) pair(oldItem, newMatches[0]);
894
+ }
895
+ // Changed props can be reconciled if there is only one island of that type.
896
+ for (const oldItem of Array.from(remainingOld)) {
897
+ const key = signature(oldItem.element, false, false);
898
+ const oldMatches = Array.from(remainingOld).filter((item) => signature(item.element, false, false) === key);
899
+ const newMatches = Array.from(remainingNew).filter((item) => signature(item.element, false, false) === key);
900
+ if (oldMatches.length === 1 && newMatches.length === 1 &&
901
+ oldItem.slideIndex === newMatches[0].slideIndex && oldItem.position === newMatches[0].position)
902
+ pair(oldItem, newMatches[0]);
903
+ }
904
+ // Existing duplicate instances remain safe only when their positions and
905
+ // identity attributes are unchanged; ambiguous structural edits reload.
906
+ for (const oldItem of Array.from(remainingOld)) {
907
+ const newItem = Array.from(remainingNew).find((item) => item.slideIndex === oldItem.slideIndex && item.position === oldItem.position);
908
+ if (newItem && signature(oldItem.element, false) === signature(newItem.element, false)) pair(oldItem, newItem);
909
+ }
910
+ for (const oldItem of remainingOld) {
911
+ const key = signature(oldItem.element, false, false);
912
+ if (Array.from(remainingNew).some((item) => signature(item.element, false, false) === key))
913
+ throw new Error("Ambiguous island identity");
914
+ }
915
+ const removals = [];
916
+ for (const oldItem of remainingOld) {
917
+ const entry = islandUpdaters.get(oldItem.element);
918
+ if (!entry?.dispose) throw new Error("Cannot safely dispose island");
919
+ removals.push(entry.dispose);
920
+ }
921
+ const updates = [];
922
+ let preserveSlides = false;
923
+ for (const [oldItem, newItem] of pairs) {
924
+ const before = oldItem.element;
925
+ const after = newItem.element;
926
+ const pendingMarkdown = before.hasAttribute("data-hono-md-island") && !before.hasAttribute("data-hono-md-mounted");
927
+ if (pendingMarkdown) {
928
+ if (oldSlides.length !== newSlides.length || oldItem.slideIndex !== newItem.slideIndex)
929
+ throw new Error("Pending island moved between slides");
930
+ preserveSlides = true;
931
+ }
932
+ if (signature(before, !pendingMarkdown) !== signature(after, !pendingMarkdown)) {
933
+ const entry = islandUpdaters.get(before);
934
+ if (!entry?.update || signature(before, false, false) !== signature(after, false, false))
935
+ throw new Error("Island properties changed");
936
+ const markdown = before.hasAttribute("data-hono-md-island");
937
+ const propsAttribute = markdown ? "data-hono-md-props" : "data-hono-decks-props";
938
+ const rawProps = after.getAttribute(propsAttribute) || "{}";
939
+ const props = JSON.parse(rawProps);
940
+ const context = markdown ? JSON.parse(after.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") || "{}") : undefined;
941
+ updates.push([before, entry.update, propsAttribute, rawProps, markdown ? { props, context } : props]);
942
+ }
943
+ }
944
+ for (const dispose of removals) dispose();
945
+ for (const [oldItem, newItem] of pairs) newItem.element.replaceWith(oldItem.element);
946
+ if (preserveSlides) {
947
+ for (let i = 0; i < oldSlides.length; i++) {
948
+ const oldSlide = oldSlides[i];
949
+ const newSlide = newSlides[i];
950
+ for (const attribute of Array.from(oldSlide.attributes)) oldSlide.removeAttribute(attribute.name);
951
+ for (const attribute of Array.from(newSlide.attributes)) oldSlide.setAttribute(attribute.name, attribute.value);
952
+ oldSlide.replaceChildren(...newSlide.childNodes);
953
+ newSlide.replaceWith(oldSlide);
954
+ }
955
+ }
956
+ currentDeck.replaceChildren(...nextDeck.childNodes);
957
+ for (const [island, update, propsAttribute, rawProps, value] of updates) {
958
+ island.setAttribute(propsAttribute, rawProps);
959
+ update(value);
960
+ }
961
+ style.textContent = nextStyle.textContent;
962
+ document.title = next.title;
963
+ clientSnapshot = Promise.resolve(nextClient);
964
+ runtime.refresh();
965
+ if (remainingNew.size) {
966
+ if (needsEntry) {
967
+ const url = new URL(nextScript.getAttribute("src"), location.href);
968
+ if (url.origin !== location.origin) throw new Error("Cross-origin client entry");
969
+ url.searchParams.set("hono-decks-update", String(Date.now()));
970
+ const script = document.createElement("script");
971
+ script.type = "module";
972
+ script.setAttribute("data-hono-decks-client-entry", "");
973
+ if (scriptNonce) script.nonce = scriptNonce;
974
+ script.src = url.href;
975
+ await new Promise((resolve, reject) => {
976
+ script.onload = resolve;
977
+ script.onerror = () => reject(new Error("Client entry failed"));
978
+ document.body.appendChild(script);
979
+ });
980
+ } else {
981
+ for (const hydrate of hydrators) hydrate();
982
+ }
983
+ for (const item of remainingNew) {
984
+ const element = item.element;
985
+ if (!element.hasAttribute("data-hono-decks-live-mounted") && !element.hasAttribute("data-hono-md-scheduled"))
986
+ throw new Error("New island did not initialize");
987
+ }
988
+ }
989
+ }
990
+
991
+ async function onUpdate() {
992
+ if (running) { pending = true; return; }
993
+ running = true;
994
+ try {
995
+ do { pending = false; await update(); } while (pending);
996
+ } catch (error) { console.warn("[hono-decks] Live update failed; reloading", error); location.reload(); }
997
+ finally { running = false; }
998
+ }
999
+ events.addEventListener("deck:updated", onUpdate);
810
1000
  })();
811
1001
  <\/script>`;
812
1002
  }
813
1003
  function renderClientEntryScript(clientEntry, nonce) {
814
- return `<script type="module" src="${escapeHtml$6(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
1004
+ return `<script data-hono-decks-client-entry type="module" src="${escapeHtml$6(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
815
1005
  }
816
1006
  function renderPresentationScript(nonce) {
817
1007
  return `<script${documentNonceAttribute(nonce)}>
818
1008
  (() => {
819
- const slides = Array.from(document.querySelectorAll(".slide"));
1009
+ let slides = Array.from(document.querySelectorAll(".slide"));
820
1010
  const stage = document.querySelector("[data-hono-decks-stage]");
821
1011
  const deck = document.querySelector("[data-hono-decks-deck]");
822
1012
  const DESIGN_WIDTH = ${PRESENTATION_DESIGN_WIDTH};
@@ -1090,6 +1280,7 @@ function renderPresentationScript(nonce) {
1090
1280
  updateFires(nextStepIndex);
1091
1281
  publishState();
1092
1282
  requestAnimationFrame(() => {
1283
+ if (token !== transitionToken) return;
1093
1284
  setSlideState(outgoing, "leaving", direction, transition, timing);
1094
1285
  setSlideState(incoming, "active", direction, transition, timing);
1095
1286
  waitForSlideTransition(outgoing, incoming, direction, token);
@@ -1196,13 +1387,41 @@ function renderPresentationScript(nonce) {
1196
1387
  await document.documentElement.requestFullscreen?.();
1197
1388
  }
1198
1389
 
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);
1390
+ const boundSlides = new WeakSet();
1391
+ function bindSlideClicks() {
1392
+ slides.forEach((slide) => {
1393
+ if (boundSlides.has(slide)) return;
1394
+ boundSlides.add(slide);
1395
+ slide.addEventListener("click", () => {
1396
+ if (!document.body.hasAttribute("data-overview-mode")) return;
1397
+ document.body.removeAttribute("data-overview-mode");
1398
+ show(slides.indexOf(slide));
1399
+ });
1204
1400
  });
1205
- });
1401
+ }
1402
+
1403
+ function refresh() {
1404
+ transitionToken += 1;
1405
+ isTransitioning = false;
1406
+ pendingNavigation = null;
1407
+ slides = Array.from(deck?.querySelectorAll(".slide") ?? []);
1408
+ bindSlideClicks();
1409
+ if (document.body.hasAttribute("data-overview-mode")) {
1410
+ slides.forEach((slide) => { slide.hidden = false; slide.setAttribute("data-slide-state", "active"); });
1411
+ setFiresVisible(Array.from(deck?.querySelectorAll("[data-hono-decks-fire]") ?? []), true);
1412
+ index = Math.max(0, Math.min(slides.length - 1, index));
1413
+ stepCount = fireCountForSlide(index);
1414
+ stepIndex = Math.max(0, Math.min(stepCount, stepIndex));
1415
+ publishState();
1416
+ } else {
1417
+ applyInstantSlideChange(index, stepIndex);
1418
+ }
1419
+ document.querySelectorAll(".speaker-notes").forEach((note) => {
1420
+ note.hidden = !document.body.hasAttribute("data-presenter-mode");
1421
+ });
1422
+ fitDeck();
1423
+ }
1424
+ bindSlideClicks();
1206
1425
 
1207
1426
  document.addEventListener("keydown", (event) => {
1208
1427
  if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
@@ -1225,7 +1444,7 @@ function renderPresentationScript(nonce) {
1225
1444
  handleCommand(message.action, message.index, message.stepIndex);
1226
1445
  });
1227
1446
 
1228
- window.__honoDecksPresentationRuntime = { command: handleCommand };
1447
+ window.__honoDecksPresentationRuntime = { command: handleCommand, refresh };
1229
1448
 
1230
1449
  window.addEventListener("resize", fitDeck);
1231
1450
  if (typeof ResizeObserver !== "undefined" && stage instanceof HTMLElement) {
@@ -1261,7 +1480,7 @@ function renderCompiledDeckPage(input) {
1261
1480
  <meta charset="utf-8" />
1262
1481
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1263
1482
  <title>${escapeHtml$5(presentationPageTitle(deck))}</title>
1264
- <style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
1483
+ <style data-hono-decks-presentation-style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
1265
1484
  ${document.head}
1266
1485
  </head>
1267
1486
  <body${bodyAttrs.length ? ` ${bodyAttrs.join(" ")}` : ""}>
@@ -1292,7 +1511,7 @@ async function renderCompiledDeckPageAsync(input) {
1292
1511
  <meta charset="utf-8" />
1293
1512
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1294
1513
  <title>${escapeHtml$5(presentationPageTitle(deck))}</title>
1295
- <style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
1514
+ <style data-hono-decks-presentation-style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
1296
1515
  ${document.head}
1297
1516
  </head>
1298
1517
  <body${bodyAttrs.length ? ` ${bodyAttrs.join(" ")}` : ""}>
package/dist/bin.js CHANGED
@@ -1660,8 +1660,37 @@ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
1660
1660
  function emitMarkdownClient(islands, runtime) {
1661
1661
  if (!islands.length) return "";
1662
1662
  const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
1663
+ const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
1664
+ const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
1665
+ const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
1666
+ const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
1663
1667
  return `import { hydrateMarkdownIslands } from ${literal(runtime)};
1668
+ import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
1669
+ import { MarkdownContext } from ${literal(contextModule)};
1670
+ import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
1671
+ import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
1672
+ function hydrateDeckMarkdownIslands() {
1664
1673
  hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
1674
+ schedule(element, loader, props, strategy, mount) {
1675
+ return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
1676
+ const live = window.__honoDecksLive;
1677
+ if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
1678
+ const Component = module.default;
1679
+ function LiveMarkdownIsland(componentProps) {
1680
+ const [current, setCurrent] = useLiveState({
1681
+ props: componentProps,
1682
+ context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
1683
+ });
1684
+ const [active, setActive] = useLiveState(true);
1685
+ live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
1686
+ return active ? liveJsx(MarkdownContext.Provider, {
1687
+ value: current.context,
1688
+ children: liveJsx(Component, current.props),
1689
+ }) : null;
1690
+ }
1691
+ return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
1692
+ });
1693
+ },
1665
1694
  ready(element, start) {
1666
1695
  const slide = element.closest('section[data-slide-state]');
1667
1696
  if (!slide || slide.dataset.slideState === 'active') { start(); return; }
@@ -1670,7 +1699,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
1670
1699
  });
1671
1700
  observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
1672
1701
  }
1673
- });`;
1702
+ });
1703
+ }
1704
+ hydrateDeckMarkdownIslands();
1705
+ if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
1674
1706
  }
1675
1707
  //#endregion
1676
1708
  //#region src/node/markdown-adapter.ts
@@ -1719,11 +1751,15 @@ async function emitClientEntryModule(input) {
1719
1751
  import { hydrateSlideIslands } from "hono-decks/client";
1720
1752
  ${imports.join("\n")}
1721
1753
 
1722
- hydrateSlideIslands({
1723
- components: {
1724
- ${registrations.join(",\n ")}
1725
- }
1726
- });
1754
+ function hydrateDeckIslands() {
1755
+ hydrateSlideIslands({
1756
+ components: {
1757
+ ${registrations.join(",\n ")}
1758
+ }
1759
+ });
1760
+ }
1761
+ hydrateDeckIslands();
1762
+ if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckIslands);
1727
1763
  `,
1728
1764
  resolveDir: input.cwd,
1729
1765
  sourcefile: "hono-decks-client-entry.tsx",
package/dist/cli.js CHANGED
@@ -1659,8 +1659,37 @@ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
1659
1659
  function emitMarkdownClient(islands, runtime) {
1660
1660
  if (!islands.length) return "";
1661
1661
  const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
1662
+ const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
1663
+ const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
1664
+ const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
1665
+ const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
1662
1666
  return `import { hydrateMarkdownIslands } from ${literal(runtime)};
1667
+ import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
1668
+ import { MarkdownContext } from ${literal(contextModule)};
1669
+ import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
1670
+ import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
1671
+ function hydrateDeckMarkdownIslands() {
1663
1672
  hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
1673
+ schedule(element, loader, props, strategy, mount) {
1674
+ return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
1675
+ const live = window.__honoDecksLive;
1676
+ if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
1677
+ const Component = module.default;
1678
+ function LiveMarkdownIsland(componentProps) {
1679
+ const [current, setCurrent] = useLiveState({
1680
+ props: componentProps,
1681
+ context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
1682
+ });
1683
+ const [active, setActive] = useLiveState(true);
1684
+ live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
1685
+ return active ? liveJsx(MarkdownContext.Provider, {
1686
+ value: current.context,
1687
+ children: liveJsx(Component, current.props),
1688
+ }) : null;
1689
+ }
1690
+ return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
1691
+ });
1692
+ },
1664
1693
  ready(element, start) {
1665
1694
  const slide = element.closest('section[data-slide-state]');
1666
1695
  if (!slide || slide.dataset.slideState === 'active') { start(); return; }
@@ -1669,7 +1698,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
1669
1698
  });
1670
1699
  observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
1671
1700
  }
1672
- });`;
1701
+ });
1702
+ }
1703
+ hydrateDeckMarkdownIslands();
1704
+ if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
1673
1705
  }
1674
1706
  //#endregion
1675
1707
  //#region src/node/markdown-adapter.ts
@@ -1718,11 +1750,15 @@ async function emitClientEntryModule(input) {
1718
1750
  import { hydrateSlideIslands } from "hono-decks/client";
1719
1751
  ${imports.join("\n")}
1720
1752
 
1721
- hydrateSlideIslands({
1722
- components: {
1723
- ${registrations.join(",\n ")}
1724
- }
1725
- });
1753
+ function hydrateDeckIslands() {
1754
+ hydrateSlideIslands({
1755
+ components: {
1756
+ ${registrations.join(",\n ")}
1757
+ }
1758
+ });
1759
+ }
1760
+ hydrateDeckIslands();
1761
+ if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckIslands);
1726
1762
  `,
1727
1763
  resolveDir: input.cwd,
1728
1764
  sourcefile: "hono-decks-client-entry.tsx",
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { render } from "hono/jsx/dom";
1
+ import { flushSync, render, useState } from "hono/jsx/dom";
2
2
  import { jsx } from "hono/jsx/dom/jsx-runtime";
3
3
  //#region src/client/islands.ts
4
4
  function hydrateSlideIslands(input) {
@@ -8,7 +8,19 @@ function hydrateSlideIslands(input) {
8
8
  if (!name) continue;
9
9
  const component = input.components[name];
10
10
  if (!component) continue;
11
- render(jsx(component, parseIslandProps(island.dataset.honoDecksProps)), island);
11
+ const props = parseIslandProps(island.dataset.honoDecksProps);
12
+ const live = window.__honoDecksLive;
13
+ if (live) {
14
+ if (island.dataset.honoDecksLiveMounted) continue;
15
+ function LiveIsland() {
16
+ const [currentProps, setCurrentProps] = useState(props);
17
+ const [active, setActive] = useState(true);
18
+ live.register(island, setCurrentProps, () => flushSync(() => setActive(false)));
19
+ return active ? jsx(component, currentProps) : null;
20
+ }
21
+ render(jsx(LiveIsland, {}), island);
22
+ island.dataset.honoDecksLiveMounted = "true";
23
+ } else render(jsx(component, props), island);
12
24
  }
13
25
  }
14
26
  function parseIslandProps(value) {
package/dist/node.js CHANGED
@@ -1852,8 +1852,37 @@ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
1852
1852
  function emitMarkdownClient(islands, runtime) {
1853
1853
  if (!islands.length) return "";
1854
1854
  const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
1855
+ const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
1856
+ const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
1857
+ const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
1858
+ const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
1855
1859
  return `import { hydrateMarkdownIslands } from ${literal(runtime)};
1860
+ import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
1861
+ import { MarkdownContext } from ${literal(contextModule)};
1862
+ import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
1863
+ import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
1864
+ function hydrateDeckMarkdownIslands() {
1856
1865
  hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
1866
+ schedule(element, loader, props, strategy, mount) {
1867
+ return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
1868
+ const live = window.__honoDecksLive;
1869
+ if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
1870
+ const Component = module.default;
1871
+ function LiveMarkdownIsland(componentProps) {
1872
+ const [current, setCurrent] = useLiveState({
1873
+ props: componentProps,
1874
+ context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
1875
+ });
1876
+ const [active, setActive] = useLiveState(true);
1877
+ live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
1878
+ return active ? liveJsx(MarkdownContext.Provider, {
1879
+ value: current.context,
1880
+ children: liveJsx(Component, current.props),
1881
+ }) : null;
1882
+ }
1883
+ return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
1884
+ });
1885
+ },
1857
1886
  ready(element, start) {
1858
1887
  const slide = element.closest('section[data-slide-state]');
1859
1888
  if (!slide || slide.dataset.slideState === 'active') { start(); return; }
@@ -1862,7 +1891,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
1862
1891
  });
1863
1892
  observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
1864
1893
  }
1865
- });`;
1894
+ });
1895
+ }
1896
+ hydrateDeckMarkdownIslands();
1897
+ if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
1866
1898
  }
1867
1899
  //#endregion
1868
1900
  //#region src/node/markdown-adapter.ts
@@ -2001,11 +2033,15 @@ async function emitClientEntryModule(input) {
2001
2033
  import { hydrateSlideIslands } from "hono-decks/client";
2002
2034
  ${imports.join("\n")}
2003
2035
 
2004
- hydrateSlideIslands({
2005
- components: {
2006
- ${registrations.join(",\n ")}
2007
- }
2008
- });
2036
+ function hydrateDeckIslands() {
2037
+ hydrateSlideIslands({
2038
+ components: {
2039
+ ${registrations.join(",\n ")}
2040
+ }
2041
+ });
2042
+ }
2043
+ hydrateDeckIslands();
2044
+ if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckIslands);
2009
2045
  `,
2010
2046
  resolveDir: input.cwd,
2011
2047
  sourcefile: "hono-decks-client-entry.tsx",
@@ -3523,19 +3559,209 @@ function escapeHtml$8(value) {
3523
3559
  function renderLiveReloadScript(eventsPath, nonce) {
3524
3560
  return `<script${documentNonceAttribute(nonce)}>
3525
3561
  (() => {
3526
- const eventsUrl = ${JSON.stringify(eventsPath)};
3527
- const events = new EventSource(eventsUrl);
3528
- events.addEventListener("deck:updated", () => location.reload());
3562
+ const events = new EventSource(${JSON.stringify(eventsPath)});
3563
+ const scriptNonce = document.currentScript?.nonce;
3564
+ let running = false;
3565
+ let pending = false;
3566
+ const selector = "[data-hono-decks-island],[data-hono-md-island]";
3567
+ const islandUpdaters = new WeakMap();
3568
+ const hydrators = new Set();
3569
+ window.__honoDecksLive = {
3570
+ register: (element, update, dispose) => islandUpdaters.set(element, { update, dispose }),
3571
+ registerHydrator: (hydrate) => hydrators.add(hydrate),
3572
+ };
3573
+ const clientSelector = "script[data-hono-decks-client-entry]";
3574
+ const clientText = async (url) => {
3575
+ if (!url) return "";
3576
+ const response = await fetch(url, { cache: "no-store" });
3577
+ if (!response.ok) throw new Error("Client entry fetch failed");
3578
+ return response.text();
3579
+ };
3580
+ const domReady = document.readyState === "loading"
3581
+ ? new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }))
3582
+ : Promise.resolve();
3583
+ let clientSnapshot = domReady.then(() => clientText(document.querySelector(clientSelector)?.src)).catch(() => null);
3584
+
3585
+ function signature(element, includeContext = true, includeProps = true) {
3586
+ const attributes = element.hasAttribute("data-hono-md-island")
3587
+ ? ["data-hono-md-island", "data-hono-md-hydrate", "data-hono-md-props"]
3588
+ : ["data-hono-decks-island", "data-hono-decks-props"];
3589
+ return JSON.stringify([attributes.map((name) => !includeProps && name.endsWith("-props") ? null : element.getAttribute(name)),
3590
+ includeContext ? element.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") ?? null : null]);
3591
+ }
3592
+
3593
+ async function update() {
3594
+ const response = await fetch(location.href, { cache: "no-store" });
3595
+ if (!response.ok || !response.headers.get("content-type")?.includes("text/html")) throw new Error("Deck fetch failed");
3596
+ const next = new DOMParser().parseFromString(await response.text(), "text/html");
3597
+ const currentDeck = document.querySelector("[data-hono-decks-deck]");
3598
+ const nextDeck = next.querySelector("[data-hono-decks-deck]");
3599
+ const style = document.querySelector("[data-hono-decks-presentation-style]");
3600
+ const nextStyle = next.querySelector("[data-hono-decks-presentation-style]");
3601
+ const runtime = window.__honoDecksPresentationRuntime;
3602
+ if (!currentDeck || !nextDeck || !style || !nextStyle || !runtime?.refresh) throw new Error("Invalid deck document");
3603
+ const oldSlides = Array.from(currentDeck.querySelectorAll(".slide"));
3604
+ const newSlides = Array.from(nextDeck.querySelectorAll(".slide"));
3605
+ if (!newSlides.length) throw new Error("Empty deck");
3606
+ // Scripts parsed from fetched HTML are inert; reload so their behavior is not lost.
3607
+ if (currentDeck.querySelector("script") || nextDeck.querySelector("script")) throw new Error("Deck contains scripts");
3608
+ const collect = (slides) => slides.flatMap((slide, slideIndex) =>
3609
+ Array.from(slide.querySelectorAll(selector)).map((element, position) => ({ element, slideIndex, position })));
3610
+ const oldItems = collect(oldSlides);
3611
+ const newItems = collect(newSlides);
3612
+ const nextScript = next.querySelector(clientSelector);
3613
+ const fetchedClient = await clientText(nextScript?.getAttribute("src"));
3614
+ const previousClient = await clientSnapshot;
3615
+ if (previousClient === null) throw new Error("Initial client entry fetch failed");
3616
+ // Keep the loaded registry when the last island disappears; it can dispose
3617
+ // that island and initialize it again if it is reintroduced later.
3618
+ const nextClient = !nextScript && !newItems.length ? previousClient : fetchedClient;
3619
+ // A changed client bundle could contain new implementations for running islands.
3620
+ if (oldItems.length && previousClient !== nextClient) throw new Error("Client code changed");
3621
+ const needsEntry = !document.querySelector(clientSelector) && Boolean(nextScript);
3622
+ if (newItems.length && !nextScript) throw new Error("New islands require a client entry");
3623
+ if (!needsEntry && previousClient !== nextClient) throw new Error("Client code changed");
3624
+
3625
+ const duplicateKeys = new Set([...oldItems, ...newItems].map((item) => signature(item.element, false)));
3626
+ for (const key of duplicateKeys) {
3627
+ const before = oldItems.filter((item) => signature(item.element, false) === key);
3628
+ const after = newItems.filter((item) => signature(item.element, false) === key);
3629
+ if (before.length <= 1 && after.length <= 1) continue;
3630
+ if (before.length !== after.length || before.some((item, index) =>
3631
+ item.slideIndex !== after[index].slideIndex || item.position !== after[index].position))
3632
+ throw new Error("Ambiguous duplicate islands");
3633
+ }
3634
+ const pairs = [];
3635
+ const remainingOld = new Set(oldItems);
3636
+ const remainingNew = new Set(newItems);
3637
+ function pair(oldItem, newItem) {
3638
+ remainingOld.delete(oldItem);
3639
+ remainingNew.delete(newItem);
3640
+ pairs.push([oldItem, newItem]);
3641
+ }
3642
+ // Exact matches can move between slides. Match only unique instances so
3643
+ // duplicate islands cannot accidentally inherit each other's state.
3644
+ for (const oldItem of oldItems) {
3645
+ const key = signature(oldItem.element, false);
3646
+ const oldMatches = Array.from(remainingOld).filter((item) => signature(item.element, false) === key);
3647
+ const newMatches = Array.from(remainingNew).filter((item) => signature(item.element, false) === key);
3648
+ if (oldMatches.length === 1 && newMatches.length === 1) pair(oldItem, newMatches[0]);
3649
+ }
3650
+ // Changed props can be reconciled if there is only one island of that type.
3651
+ for (const oldItem of Array.from(remainingOld)) {
3652
+ const key = signature(oldItem.element, false, false);
3653
+ const oldMatches = Array.from(remainingOld).filter((item) => signature(item.element, false, false) === key);
3654
+ const newMatches = Array.from(remainingNew).filter((item) => signature(item.element, false, false) === key);
3655
+ if (oldMatches.length === 1 && newMatches.length === 1 &&
3656
+ oldItem.slideIndex === newMatches[0].slideIndex && oldItem.position === newMatches[0].position)
3657
+ pair(oldItem, newMatches[0]);
3658
+ }
3659
+ // Existing duplicate instances remain safe only when their positions and
3660
+ // identity attributes are unchanged; ambiguous structural edits reload.
3661
+ for (const oldItem of Array.from(remainingOld)) {
3662
+ const newItem = Array.from(remainingNew).find((item) => item.slideIndex === oldItem.slideIndex && item.position === oldItem.position);
3663
+ if (newItem && signature(oldItem.element, false) === signature(newItem.element, false)) pair(oldItem, newItem);
3664
+ }
3665
+ for (const oldItem of remainingOld) {
3666
+ const key = signature(oldItem.element, false, false);
3667
+ if (Array.from(remainingNew).some((item) => signature(item.element, false, false) === key))
3668
+ throw new Error("Ambiguous island identity");
3669
+ }
3670
+ const removals = [];
3671
+ for (const oldItem of remainingOld) {
3672
+ const entry = islandUpdaters.get(oldItem.element);
3673
+ if (!entry?.dispose) throw new Error("Cannot safely dispose island");
3674
+ removals.push(entry.dispose);
3675
+ }
3676
+ const updates = [];
3677
+ let preserveSlides = false;
3678
+ for (const [oldItem, newItem] of pairs) {
3679
+ const before = oldItem.element;
3680
+ const after = newItem.element;
3681
+ const pendingMarkdown = before.hasAttribute("data-hono-md-island") && !before.hasAttribute("data-hono-md-mounted");
3682
+ if (pendingMarkdown) {
3683
+ if (oldSlides.length !== newSlides.length || oldItem.slideIndex !== newItem.slideIndex)
3684
+ throw new Error("Pending island moved between slides");
3685
+ preserveSlides = true;
3686
+ }
3687
+ if (signature(before, !pendingMarkdown) !== signature(after, !pendingMarkdown)) {
3688
+ const entry = islandUpdaters.get(before);
3689
+ if (!entry?.update || signature(before, false, false) !== signature(after, false, false))
3690
+ throw new Error("Island properties changed");
3691
+ const markdown = before.hasAttribute("data-hono-md-island");
3692
+ const propsAttribute = markdown ? "data-hono-md-props" : "data-hono-decks-props";
3693
+ const rawProps = after.getAttribute(propsAttribute) || "{}";
3694
+ const props = JSON.parse(rawProps);
3695
+ const context = markdown ? JSON.parse(after.closest("[data-hono-md-context]")?.getAttribute("data-hono-md-context") || "{}") : undefined;
3696
+ updates.push([before, entry.update, propsAttribute, rawProps, markdown ? { props, context } : props]);
3697
+ }
3698
+ }
3699
+ for (const dispose of removals) dispose();
3700
+ for (const [oldItem, newItem] of pairs) newItem.element.replaceWith(oldItem.element);
3701
+ if (preserveSlides) {
3702
+ for (let i = 0; i < oldSlides.length; i++) {
3703
+ const oldSlide = oldSlides[i];
3704
+ const newSlide = newSlides[i];
3705
+ for (const attribute of Array.from(oldSlide.attributes)) oldSlide.removeAttribute(attribute.name);
3706
+ for (const attribute of Array.from(newSlide.attributes)) oldSlide.setAttribute(attribute.name, attribute.value);
3707
+ oldSlide.replaceChildren(...newSlide.childNodes);
3708
+ newSlide.replaceWith(oldSlide);
3709
+ }
3710
+ }
3711
+ currentDeck.replaceChildren(...nextDeck.childNodes);
3712
+ for (const [island, update, propsAttribute, rawProps, value] of updates) {
3713
+ island.setAttribute(propsAttribute, rawProps);
3714
+ update(value);
3715
+ }
3716
+ style.textContent = nextStyle.textContent;
3717
+ document.title = next.title;
3718
+ clientSnapshot = Promise.resolve(nextClient);
3719
+ runtime.refresh();
3720
+ if (remainingNew.size) {
3721
+ if (needsEntry) {
3722
+ const url = new URL(nextScript.getAttribute("src"), location.href);
3723
+ if (url.origin !== location.origin) throw new Error("Cross-origin client entry");
3724
+ url.searchParams.set("hono-decks-update", String(Date.now()));
3725
+ const script = document.createElement("script");
3726
+ script.type = "module";
3727
+ script.setAttribute("data-hono-decks-client-entry", "");
3728
+ if (scriptNonce) script.nonce = scriptNonce;
3729
+ script.src = url.href;
3730
+ await new Promise((resolve, reject) => {
3731
+ script.onload = resolve;
3732
+ script.onerror = () => reject(new Error("Client entry failed"));
3733
+ document.body.appendChild(script);
3734
+ });
3735
+ } else {
3736
+ for (const hydrate of hydrators) hydrate();
3737
+ }
3738
+ for (const item of remainingNew) {
3739
+ const element = item.element;
3740
+ if (!element.hasAttribute("data-hono-decks-live-mounted") && !element.hasAttribute("data-hono-md-scheduled"))
3741
+ throw new Error("New island did not initialize");
3742
+ }
3743
+ }
3744
+ }
3745
+
3746
+ async function onUpdate() {
3747
+ if (running) { pending = true; return; }
3748
+ running = true;
3749
+ try {
3750
+ do { pending = false; await update(); } while (pending);
3751
+ } catch (error) { console.warn("[hono-decks] Live update failed; reloading", error); location.reload(); }
3752
+ finally { running = false; }
3753
+ }
3754
+ events.addEventListener("deck:updated", onUpdate);
3529
3755
  })();
3530
3756
  <\/script>`;
3531
3757
  }
3532
3758
  function renderClientEntryScript(clientEntry, nonce) {
3533
- return `<script type="module" src="${escapeHtml$7(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
3759
+ return `<script data-hono-decks-client-entry type="module" src="${escapeHtml$7(clientEntry)}"${documentNonceAttribute(nonce)}><\/script>`;
3534
3760
  }
3535
3761
  function renderPresentationScript(nonce) {
3536
3762
  return `<script${documentNonceAttribute(nonce)}>
3537
3763
  (() => {
3538
- const slides = Array.from(document.querySelectorAll(".slide"));
3764
+ let slides = Array.from(document.querySelectorAll(".slide"));
3539
3765
  const stage = document.querySelector("[data-hono-decks-stage]");
3540
3766
  const deck = document.querySelector("[data-hono-decks-deck]");
3541
3767
  const DESIGN_WIDTH = ${PRESENTATION_DESIGN_WIDTH};
@@ -3809,6 +4035,7 @@ function renderPresentationScript(nonce) {
3809
4035
  updateFires(nextStepIndex);
3810
4036
  publishState();
3811
4037
  requestAnimationFrame(() => {
4038
+ if (token !== transitionToken) return;
3812
4039
  setSlideState(outgoing, "leaving", direction, transition, timing);
3813
4040
  setSlideState(incoming, "active", direction, transition, timing);
3814
4041
  waitForSlideTransition(outgoing, incoming, direction, token);
@@ -3915,13 +4142,41 @@ function renderPresentationScript(nonce) {
3915
4142
  await document.documentElement.requestFullscreen?.();
3916
4143
  }
3917
4144
 
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);
4145
+ const boundSlides = new WeakSet();
4146
+ function bindSlideClicks() {
4147
+ slides.forEach((slide) => {
4148
+ if (boundSlides.has(slide)) return;
4149
+ boundSlides.add(slide);
4150
+ slide.addEventListener("click", () => {
4151
+ if (!document.body.hasAttribute("data-overview-mode")) return;
4152
+ document.body.removeAttribute("data-overview-mode");
4153
+ show(slides.indexOf(slide));
4154
+ });
3923
4155
  });
3924
- });
4156
+ }
4157
+
4158
+ function refresh() {
4159
+ transitionToken += 1;
4160
+ isTransitioning = false;
4161
+ pendingNavigation = null;
4162
+ slides = Array.from(deck?.querySelectorAll(".slide") ?? []);
4163
+ bindSlideClicks();
4164
+ if (document.body.hasAttribute("data-overview-mode")) {
4165
+ slides.forEach((slide) => { slide.hidden = false; slide.setAttribute("data-slide-state", "active"); });
4166
+ setFiresVisible(Array.from(deck?.querySelectorAll("[data-hono-decks-fire]") ?? []), true);
4167
+ index = Math.max(0, Math.min(slides.length - 1, index));
4168
+ stepCount = fireCountForSlide(index);
4169
+ stepIndex = Math.max(0, Math.min(stepCount, stepIndex));
4170
+ publishState();
4171
+ } else {
4172
+ applyInstantSlideChange(index, stepIndex);
4173
+ }
4174
+ document.querySelectorAll(".speaker-notes").forEach((note) => {
4175
+ note.hidden = !document.body.hasAttribute("data-presenter-mode");
4176
+ });
4177
+ fitDeck();
4178
+ }
4179
+ bindSlideClicks();
3925
4180
 
3926
4181
  document.addEventListener("keydown", (event) => {
3927
4182
  if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
@@ -3944,7 +4199,7 @@ function renderPresentationScript(nonce) {
3944
4199
  handleCommand(message.action, message.index, message.stepIndex);
3945
4200
  });
3946
4201
 
3947
- window.__honoDecksPresentationRuntime = { command: handleCommand };
4202
+ window.__honoDecksPresentationRuntime = { command: handleCommand, refresh };
3948
4203
 
3949
4204
  window.addEventListener("resize", fitDeck);
3950
4205
  if (typeof ResizeObserver !== "undefined" && stage instanceof HTMLElement) {
@@ -3980,7 +4235,7 @@ async function renderCompiledDeckPageAsync(input) {
3980
4235
  <meta charset="utf-8" />
3981
4236
  <meta name="viewport" content="width=device-width, initial-scale=1" />
3982
4237
  <title>${escapeHtml$6(presentationPageTitle(deck))}</title>
3983
- <style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
4238
+ <style data-hono-decks-presentation-style${nonceAttribute}>${basePresentationStyle()}${deck.themeStyle ?? ""}${input.style ?? ""}</style>
3984
4239
  ${document.head}
3985
4240
  </head>
3986
4241
  <body${bodyAttrs.length ? ` ${bodyAttrs.join(" ")}` : ""}>
package/dist/vite.js CHANGED
@@ -1659,8 +1659,37 @@ async function createHonoMarkdownAdapter(cwd, legacy = false, options) {
1659
1659
  function emitMarkdownClient(islands, runtime) {
1660
1660
  if (!islands.length) return "";
1661
1661
  const literal = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
1662
+ const isVoid = runtime.startsWith("void-plugin-hono-jsx/");
1663
+ const contextModule = isVoid ? "void-plugin-hono-jsx/markdown" : "@ts-76/hono-markdown";
1664
+ const scheduler = isVoid ? "void/pages-client" : "@ts-76/hono-markdown/client";
1665
+ const schedulerName = isVoid ? "scheduleHydration" : "scheduleMarkdownIsland";
1662
1666
  return `import { hydrateMarkdownIslands } from ${literal(runtime)};
1667
+ import { ${schedulerName} as scheduleIsland } from ${literal(scheduler)};
1668
+ import { MarkdownContext } from ${literal(contextModule)};
1669
+ import { jsx as liveJsx } from "hono/jsx/dom/jsx-runtime";
1670
+ import { flushSync as flushLiveSync, useState as useLiveState } from "hono/jsx/dom";
1671
+ function hydrateDeckMarkdownIslands() {
1663
1672
  hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () => import(${literal(island.modulePath)})`).join(",")} }, {
1673
+ schedule(element, loader, props, strategy, mount) {
1674
+ return scheduleIsland(element, loader, props, strategy, (target, module, initialProps) => {
1675
+ const live = window.__honoDecksLive;
1676
+ if (!live || !module || typeof module.default !== 'function') return mount(target, module, initialProps);
1677
+ const Component = module.default;
1678
+ function LiveMarkdownIsland(componentProps) {
1679
+ const [current, setCurrent] = useLiveState({
1680
+ props: componentProps,
1681
+ context: JSON.parse(element.closest('[data-hono-md-context]')?.getAttribute('data-hono-md-context') || '{}'),
1682
+ });
1683
+ const [active, setActive] = useLiveState(true);
1684
+ live.register(element, setCurrent, () => flushLiveSync(() => setActive(false)));
1685
+ return active ? liveJsx(MarkdownContext.Provider, {
1686
+ value: current.context,
1687
+ children: liveJsx(Component, current.props),
1688
+ }) : null;
1689
+ }
1690
+ return mount(target, { ...module, default: LiveMarkdownIsland }, initialProps);
1691
+ });
1692
+ },
1664
1693
  ready(element, start) {
1665
1694
  const slide = element.closest('section[data-slide-state]');
1666
1695
  if (!slide || slide.dataset.slideState === 'active') { start(); return; }
@@ -1669,7 +1698,10 @@ hydrateMarkdownIslands({ ${islands.map((island) => `${literal(island.id)}: () =>
1669
1698
  });
1670
1699
  observer.observe(slide, { attributes: true, attributeFilter: ['data-slide-state'] });
1671
1700
  }
1672
- });`;
1701
+ });
1702
+ }
1703
+ hydrateDeckMarkdownIslands();
1704
+ if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckMarkdownIslands);`;
1673
1705
  }
1674
1706
  //#endregion
1675
1707
  //#region src/node/markdown-adapter.ts
@@ -1718,11 +1750,15 @@ async function emitClientEntryModule(input) {
1718
1750
  import { hydrateSlideIslands } from "hono-decks/client";
1719
1751
  ${imports.join("\n")}
1720
1752
 
1721
- hydrateSlideIslands({
1722
- components: {
1723
- ${registrations.join(",\n ")}
1724
- }
1725
- });
1753
+ function hydrateDeckIslands() {
1754
+ hydrateSlideIslands({
1755
+ components: {
1756
+ ${registrations.join(",\n ")}
1757
+ }
1758
+ });
1759
+ }
1760
+ hydrateDeckIslands();
1761
+ if (window.__honoDecksLive) window.__honoDecksLive.registerHydrator(hydrateDeckIslands);
1726
1762
  `,
1727
1763
  resolveDir: input.cwd,
1728
1764
  sourcefile: "hono-decks-client-entry.tsx",
@@ -2307,6 +2343,8 @@ function honoDecks(options = {}) {
2307
2343
  let mountChanged = false;
2308
2344
  let modules = /* @__PURE__ */ new Map();
2309
2345
  let routerModulePath = "";
2346
+ const eventClients = /* @__PURE__ */ new Set();
2347
+ let eventsPath = "/@hono-decks/events";
2310
2348
  const syncWatchedPaths = async () => {
2311
2349
  if (!server) return;
2312
2350
  const next = /* @__PURE__ */ new Set([
@@ -2374,7 +2412,7 @@ function honoDecks(options = {}) {
2374
2412
  environment.moduleGraph.invalidateAll();
2375
2413
  if (environment.name !== "client") environment.hot.send({ type: "full-reload" });
2376
2414
  }
2377
- server.ws.send({ type: "full-reload" });
2415
+ for (const client of eventClients) client.write("event: deck:updated\ndata: {}\n\n");
2378
2416
  }
2379
2417
  } finally {
2380
2418
  compiling = false;
@@ -2422,8 +2460,9 @@ export { createDecks };
2422
2460
  export const decks = createDecks(config);`;
2423
2461
  if (!id.startsWith(MODULE_PREFIX)) return null;
2424
2462
  const path = id.slice(MODULE_PREFIX.length);
2425
- const code = modules.get(path);
2463
+ let code = modules.get(path);
2426
2464
  if (code === void 0) return null;
2465
+ if (path === routerModulePath && resolvedConfig?.command === "serve") code = code.replace("const generatedDecks = defineDecks({", `const generatedDecks = defineDecks({\n liveReloadPath: () => ${JSON.stringify(eventsPath)},`);
2427
2466
  return (await transform(code, {
2428
2467
  loader: "ts",
2429
2468
  format: "esm",
@@ -2436,16 +2475,30 @@ export const decks = createDecks(config);`;
2436
2475
  },
2437
2476
  async configResolved(config) {
2438
2477
  resolvedConfig = config;
2478
+ eventsPath = `${config.base?.startsWith("/") ? config.base.replace(/\/$/, "") : ""}/@hono-decks/events`;
2439
2479
  projectRoot = config.resolve?.preserveSymlinks ? config.root : await realpath(config.root);
2440
2480
  configPath = resolve(projectRoot, options.configFile ?? "hono-decks.config.ts");
2441
2481
  await compile(true);
2442
2482
  },
2443
2483
  configureServer(devServer) {
2444
2484
  server = devServer;
2485
+ devServer.middlewares.use((req, res, next) => {
2486
+ if (req.url?.split("?")[0] !== eventsPath || req.method !== "GET") return next();
2487
+ res.writeHead(200, {
2488
+ "Content-Type": "text/event-stream",
2489
+ "Cache-Control": "no-cache, no-transform",
2490
+ Connection: "keep-alive"
2491
+ });
2492
+ res.write(": connected\n\n");
2493
+ eventClients.add(res);
2494
+ res.on("close", () => eventClients.delete(res));
2495
+ });
2445
2496
  syncWatchedPaths();
2446
2497
  devServer.watcher.on("all", onWatcherEvent);
2447
2498
  const cleanup = () => {
2448
2499
  if (timer) clearTimeout(timer);
2500
+ for (const client of eventClients) client.end();
2501
+ eventClients.clear();
2449
2502
  devServer.watcher.off("all", onWatcherEvent);
2450
2503
  server = void 0;
2451
2504
  watchedPaths.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-decks",
3
- "version": "1.2.4",
3
+ "version": "1.4.0",
4
4
  "description": "Slide decks for Hono and Cloudflare Workers",
5
5
  "keywords": [
6
6
  "cloudflare-workers",