@libraz/mejiro-react 0.9.2 → 0.10.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +39 -22
  2. package/dist/index.js +1032 -808
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -176,7 +176,29 @@ import {
176
176
  EditableEpub,
177
177
  exportEditableEpub
178
178
  } from "@libraz/mejiro/epub";
179
- import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef12, useState as useState9 } from "react";
179
+ import { useCallback as useCallback8, useEffect as useEffect10, useMemo as useMemo6, useRef as useRef13, useState as useState9 } from "react";
180
+
181
+ // src/bookIdentity.ts
182
+ var identities = /* @__PURE__ */ new WeakMap();
183
+ function markBookIdentity(derived, identity) {
184
+ identities.set(derived, identity);
185
+ return derived;
186
+ }
187
+ function bookIdentity(book) {
188
+ return book && identities.get(book) || book;
189
+ }
190
+ function chapterContentKey(chapter) {
191
+ return JSON.stringify([chapter.title, chapter.paragraphs]);
192
+ }
193
+ function resolveLayoutEpub(prev, epub, book, chapter) {
194
+ const source = epub?.chapters[chapter];
195
+ if (prev && prev.book === book && prev.chapter === chapter && prev.source === source) return prev;
196
+ const key = source ? chapterContentKey(source) : null;
197
+ if (prev?.epub && prev.book === book && prev.chapter === chapter && prev.key === key) {
198
+ return { ...prev, source };
199
+ }
200
+ return { book, chapter, source, key, epub };
201
+ }
180
202
 
181
203
  // src/errors.ts
182
204
  import { assertEpubInputSize } from "@libraz/mejiro/epub";
@@ -205,14 +227,15 @@ async function withErrorReporting(task, report) {
205
227
  import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
206
228
  import { normalizeFontFamily as normalizeFontFamily5 } from "@libraz/mejiro/browser";
207
229
  import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
230
+ import { pageNumberFor as pageNumberFor2 } from "@libraz/mejiro/render";
208
231
  import {
209
232
  forwardRef,
210
- useCallback as useCallback6,
211
- useEffect as useEffect8,
233
+ useCallback as useCallback7,
234
+ useEffect as useEffect9,
212
235
  useImperativeHandle,
213
236
  useLayoutEffect as useLayoutEffect4,
214
237
  useMemo as useMemo5,
215
- useRef as useRef11,
238
+ useRef as useRef12,
216
239
  useState as useState8
217
240
  } from "react";
218
241
 
@@ -228,7 +251,10 @@ function MejiroPageIndicator({ current, total }) {
228
251
 
229
252
  // src/MejiroScrollView.tsx
230
253
  import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
254
+ import { pageNumberFor } from "@libraz/mejiro/render";
231
255
  import {
256
+ memo,
257
+ useCallback as useCallback2,
232
258
  useEffect as useEffect2,
233
259
  useLayoutEffect,
234
260
  useMemo as useMemo3,
@@ -648,6 +674,71 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
648
674
  // src/MejiroScrollView.tsx
649
675
  import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
650
676
  var NO_IMAGES = [];
677
+ var ScrollPage = memo(function ScrollPage2({
678
+ result,
679
+ index,
680
+ pageNumber,
681
+ pageWidth,
682
+ pageHeight,
683
+ contentHeight,
684
+ slotMode,
685
+ fontFamily,
686
+ lineSpacing,
687
+ contentStyle,
688
+ rects,
689
+ overlays,
690
+ overlaysLeft,
691
+ registerPage,
692
+ onImagePointerDown,
693
+ onImageResizePointerDown,
694
+ onImageClose
695
+ }) {
696
+ return /* @__PURE__ */ jsxs5(
697
+ "div",
698
+ {
699
+ ref: (el) => registerPage(index, el),
700
+ "data-page-idx": index,
701
+ className: "mejiro-reader-page",
702
+ style: {
703
+ width: pageWidth,
704
+ height: pageHeight,
705
+ flexShrink: 0,
706
+ ...overlays.length > 0 ? { overflow: "visible" } : null
707
+ },
708
+ children: [
709
+ /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-rule" }),
710
+ /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-header", children: [
711
+ /* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-title" }),
712
+ /* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-num", children: pageNumber != null ? String(pageNumber) : "" })
713
+ ] }),
714
+ /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
715
+ /* @__PURE__ */ jsx9(
716
+ MejiroPageView,
717
+ {
718
+ result,
719
+ slotMode,
720
+ fontFamily,
721
+ lineSpacing,
722
+ className: "mejiro-reader-page-content",
723
+ style: contentStyle
724
+ }
725
+ ),
726
+ rects && /* @__PURE__ */ jsx9(MejiroSelectionLayer, { rects, side: index % 2 === 0 ? "right" : "left" })
727
+ ] }) }),
728
+ overlays.map((item) => /* @__PURE__ */ jsx9(
729
+ MejiroImageOverlay,
730
+ {
731
+ rect: overlaysLeft ? { ...item.rect, x: item.rect.x + pageWidth } : item.rect,
732
+ onOverlayPointerDown: (e) => onImagePointerDown(item.id, e),
733
+ onResizePointerDown: (e) => onImageResizePointerDown(item.id, e),
734
+ onClose: () => onImageClose(item.id)
735
+ },
736
+ item.id
737
+ ))
738
+ ]
739
+ }
740
+ );
741
+ });
651
742
  function MejiroScrollView({
652
743
  layout,
653
744
  pageWidth,
@@ -663,6 +754,7 @@ function MejiroScrollView({
663
754
  selectionRects,
664
755
  spreadIdx,
665
756
  images = NO_IMAGES,
757
+ pageNumbers = "both",
666
758
  onImagePointerDown,
667
759
  onImageResizePointerDown,
668
760
  onImageClose
@@ -697,10 +789,30 @@ function MejiroScrollView({
697
789
  }
698
790
  return byPage;
699
791
  }, [selectionRects]);
700
- const contentStyle = { height: contentHeight };
701
- if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
702
- if (fontSize != null) contentStyle.fontSize = fontSize;
703
- if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
792
+ const contentStyle = useMemo3(() => {
793
+ const style = { height: contentHeight };
794
+ if (fontFamily) style.fontFamily = normalizeFontFamily2(fontFamily);
795
+ if (fontSize != null) style.fontSize = fontSize;
796
+ if (lineSpacing != null) style.lineHeight = String(lineSpacing);
797
+ return style;
798
+ }, [contentHeight, fontFamily, fontSize, lineSpacing]);
799
+ const registerPage = useCallback2((index, el) => {
800
+ pageRefs.current[index] = el;
801
+ }, []);
802
+ const imageHandlersRef = useRef3({ onImagePointerDown, onImageResizePointerDown, onImageClose });
803
+ imageHandlersRef.current = { onImagePointerDown, onImageResizePointerDown, onImageClose };
804
+ const handleImagePointerDown = useCallback2(
805
+ (id, e) => imageHandlersRef.current.onImagePointerDown?.(id, e),
806
+ []
807
+ );
808
+ const handleImageResizePointerDown = useCallback2(
809
+ (id, e) => imageHandlersRef.current.onImageResizePointerDown?.(id, e),
810
+ []
811
+ );
812
+ const handleImageClose = useCallback2(
813
+ (id) => imageHandlersRef.current.onImageClose?.(id),
814
+ []
815
+ );
704
816
  useEffect2(() => {
705
817
  const container = containerRef.current;
706
818
  if (!container) return;
@@ -758,67 +870,26 @@ function MejiroScrollView({
758
870
  {
759
871
  className: "mejiro-reader-scroll-track",
760
872
  style: { display: "flex", flexDirection: "column", gap: pageGap },
761
- children: pages.map((result, i) => /* @__PURE__ */ jsxs5(
762
- "div",
873
+ children: pages.map((result, i) => /* @__PURE__ */ jsx9(
874
+ ScrollPage,
763
875
  {
764
- ref: (el) => {
765
- pageRefs.current[i] = el;
766
- },
767
- "data-page-idx": i,
768
- className: "mejiro-reader-page",
769
- style: {
770
- width: pageWidth,
771
- height: pageHeight,
772
- flexShrink: 0,
773
- ...i === rightPage && rightImages.length > 0 || i === leftPage && leftImages.length > 0 ? { overflow: "visible" } : null
774
- },
775
- children: [
776
- /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-rule" }),
777
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-header", children: [
778
- /* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-title" }),
779
- /* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-num", children: i + 1 })
780
- ] }),
781
- /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
782
- /* @__PURE__ */ jsx9(
783
- MejiroPageView,
784
- {
785
- result,
786
- slotMode,
787
- fontFamily,
788
- lineSpacing,
789
- className: "mejiro-reader-page-content",
790
- style: contentStyle
791
- }
792
- ),
793
- rectsByPage[i] && /* @__PURE__ */ jsx9(
794
- MejiroSelectionLayer,
795
- {
796
- rects: rectsByPage[i],
797
- side: i % 2 === 0 ? "right" : "left"
798
- }
799
- )
800
- ] }) }),
801
- i === rightPage && rightImages.map((item) => /* @__PURE__ */ jsx9(
802
- MejiroImageOverlay,
803
- {
804
- rect: item.rect,
805
- onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
806
- onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
807
- onClose: () => onImageClose?.(item.id)
808
- },
809
- item.id
810
- )),
811
- i === leftPage && leftImages.map((item) => /* @__PURE__ */ jsx9(
812
- MejiroImageOverlay,
813
- {
814
- rect: { ...item.rect, x: item.rect.x + pageWidth },
815
- onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
816
- onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
817
- onClose: () => onImageClose?.(item.id)
818
- },
819
- item.id
820
- ))
821
- ]
876
+ result,
877
+ index: i,
878
+ pageNumber: pageNumberFor(pageNumbers, i),
879
+ pageWidth,
880
+ pageHeight,
881
+ contentHeight,
882
+ slotMode,
883
+ fontFamily,
884
+ lineSpacing,
885
+ contentStyle,
886
+ rects: rectsByPage[i],
887
+ overlays: i === rightPage ? rightImages : i === leftPage ? leftImages : NO_IMAGES,
888
+ overlaysLeft: i === leftPage,
889
+ registerPage,
890
+ onImagePointerDown: handleImagePointerDown,
891
+ onImageResizePointerDown: handleImageResizePointerDown,
892
+ onImageClose: handleImageClose
822
893
  },
823
894
  i
824
895
  ))
@@ -828,7 +899,7 @@ function MejiroScrollView({
828
899
 
829
900
  // src/MejiroSettingsPanel.tsx
830
901
  import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
831
- import { useEffect as useEffect3, useRef as useRef4, useState as useState3 } from "react";
902
+ import { useEffect as useEffect3, useId, useRef as useRef4, useState as useState3 } from "react";
832
903
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
833
904
  var DEFAULT_FONTS = [
834
905
  { value: "serif", label: "System Serif" },
@@ -897,15 +968,24 @@ function MejiroSettingsPanel({
897
968
  maxFontSize = 48
898
969
  }) {
899
970
  const messages = useI18n();
971
+ const uid = useId();
900
972
  const patch = (next) => onChange({ ...settings, ...next });
901
973
  const currentFont = normalizeFontFamily3(settings.fontFamily);
902
974
  const fontOptions = fonts.some((f) => f.value === currentFont) ? fonts : [{ value: currentFont, label: fontLabelFromCss(currentFont) }, ...fonts];
903
975
  return /* @__PURE__ */ jsx10("div", { className: `mejiro-reader-settings-panel${open ? " is-open" : ""}`, children: /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-settings-inner", children: /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-content", children: [
904
976
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
905
977
  /* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
906
- /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx10("select", { value: currentFont, onChange: (e) => patch({ fontFamily: e.target.value }), children: fontOptions.map((f) => /* @__PURE__ */ jsx10("option", { value: f.value, children: f.label }, f.value)) }) }),
978
+ /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx10(
979
+ "select",
980
+ {
981
+ "aria-label": messages.settingsFont,
982
+ value: currentFont,
983
+ onChange: (e) => patch({ fontFamily: e.target.value }),
984
+ children: fontOptions.map((f) => /* @__PURE__ */ jsx10("option", { value: f.value, children: f.label }, f.value))
985
+ }
986
+ ) }),
907
987
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
908
- /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-font-size", children: messages.settingsSize }),
988
+ /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-font-size`, children: messages.settingsSize }),
909
989
  /* @__PURE__ */ jsx10(
910
990
  "button",
911
991
  {
@@ -926,7 +1006,7 @@ function MejiroSettingsPanel({
926
1006
  /* @__PURE__ */ jsx10(
927
1007
  CommittedNumberInput,
928
1008
  {
929
- id: "mejiro-reader-font-size",
1009
+ id: `${uid}-font-size`,
930
1010
  value: settings.fontSize,
931
1011
  min: minFontSize,
932
1012
  max: maxFontSize,
@@ -955,11 +1035,11 @@ function MejiroSettingsPanel({
955
1035
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
956
1036
  /* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
957
1037
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
958
- /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-kinsoku", children: messages.settingsKinsoku }),
1038
+ /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-kinsoku`, children: messages.settingsKinsoku }),
959
1039
  /* @__PURE__ */ jsxs6(
960
1040
  "select",
961
1041
  {
962
- id: "mejiro-reader-kinsoku",
1042
+ id: `${uid}-kinsoku`,
963
1043
  value: settings.mode ?? "strict",
964
1044
  onChange: (e) => patch({ mode: e.target.value }),
965
1045
  children: [
@@ -970,11 +1050,11 @@ function MejiroSettingsPanel({
970
1050
  )
971
1051
  ] }),
972
1052
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
973
- /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-hanging", children: messages.settingsHanging }),
1053
+ /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-hanging`, children: messages.settingsHanging }),
974
1054
  /* @__PURE__ */ jsxs6(
975
1055
  "select",
976
1056
  {
977
- id: "mejiro-reader-hanging",
1057
+ id: `${uid}-hanging`,
978
1058
  value: String(settings.enableHanging ?? true),
979
1059
  onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
980
1060
  children: [
@@ -985,11 +1065,11 @@ function MejiroSettingsPanel({
985
1065
  )
986
1066
  ] }),
987
1067
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
988
- /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-line-spacing", children: messages.settingsLineSpacing }),
1068
+ /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: `${uid}-line-spacing`, children: messages.settingsLineSpacing }),
989
1069
  /* @__PURE__ */ jsx10(
990
1070
  CommittedNumberInput,
991
1071
  {
992
- id: "mejiro-reader-line-spacing",
1072
+ id: `${uid}-line-spacing`,
993
1073
  className: "mejiro-reader-control--wide",
994
1074
  value: settings.lineSpacing ?? 1.8,
995
1075
  min: 1,
@@ -1375,16 +1455,38 @@ function createDraftChangeTracker(chapters, key) {
1375
1455
  }
1376
1456
 
1377
1457
  // src/useChapterLayout.ts
1378
- import { useMemo as useMemo4, useRef as useRef7 } from "react";
1458
+ import { useMemo as useMemo4, useRef as useRef8 } from "react";
1379
1459
 
1380
1460
  // src/useSurfaceLayout.ts
1381
1461
  import {
1382
- useCallback as useCallback2,
1383
- useEffect as useEffect4,
1462
+ useCallback as useCallback3,
1463
+ useEffect as useEffect5,
1384
1464
  useLayoutEffect as useLayoutEffect2,
1385
- useRef as useRef6,
1465
+ useRef as useRef7,
1386
1466
  useState as useState4
1387
1467
  } from "react";
1468
+
1469
+ // src/useRequestToken.ts
1470
+ import { useEffect as useEffect4, useRef as useRef6 } from "react";
1471
+ function useRequestToken() {
1472
+ const ref = useRef6(null);
1473
+ if (!ref.current) {
1474
+ let latest = 0;
1475
+ ref.current = {
1476
+ begin: () => ++latest,
1477
+ current: () => latest,
1478
+ isCurrent: (token2) => token2 === latest,
1479
+ invalidate: () => {
1480
+ latest++;
1481
+ }
1482
+ };
1483
+ }
1484
+ const token = ref.current;
1485
+ useEffect4(() => () => token.invalidate(), [token]);
1486
+ return token;
1487
+ }
1488
+
1489
+ // src/useSurfaceLayout.ts
1388
1490
  function useSurfaceLayout(source, surface, options) {
1389
1491
  const { enableResize, resizeDebounce } = options;
1390
1492
  const [layout, setLayout] = useState4(null);
@@ -1392,19 +1494,23 @@ function useSurfaceLayout(source, surface, options) {
1392
1494
  const [pageHeight, setPageHeight] = useState4(0);
1393
1495
  const [contentHeight, setContentHeight] = useState4(0);
1394
1496
  const [elapsedMs, setElapsedMs] = useState4(0);
1395
- const layoutRef = useRef6(null);
1497
+ const [element, setElement] = useState4(surface.current);
1498
+ useLayoutEffect2(() => {
1499
+ if (surface.current !== element) setElement(surface.current);
1500
+ });
1501
+ const layoutRef = useRef7(null);
1396
1502
  layoutRef.current = layout;
1397
- const requestIdRef = useRef6(0);
1398
- const pendingRestore = useRef6(null);
1399
- const measuredBoxRef = useRef6(null);
1400
- const optionsRef = useRef6(options);
1503
+ const requests = useRequestToken();
1504
+ const pendingRestore = useRef7(null);
1505
+ const measuredBoxRef = useRef7(null);
1506
+ const optionsRef = useRef7(options);
1401
1507
  optionsRef.current = options;
1402
- const recompute = useCallback2(
1508
+ const recompute = useCallback3(
1403
1509
  async (opts = {}) => {
1404
1510
  const blank = opts.blank ?? true;
1405
1511
  const o = optionsRef.current;
1406
- const requestId = ++requestIdRef.current;
1407
- const el = surface.current;
1512
+ const requestId = requests.begin();
1513
+ const el = element;
1408
1514
  if (!(source && el)) {
1409
1515
  measuredBoxRef.current = null;
1410
1516
  setLayout(null);
@@ -1425,27 +1531,27 @@ function useSurfaceLayout(source, surface, options) {
1425
1531
  setContentHeight(dims.contentHeight);
1426
1532
  const t0 = performance.now();
1427
1533
  const next = await source.layout();
1428
- if (requestId !== requestIdRef.current) return;
1534
+ if (!requests.isCurrent(requestId)) return;
1429
1535
  layoutRef.current = next;
1430
1536
  setLayout(next);
1431
1537
  setElapsedMs(performance.now() - t0);
1432
1538
  } catch (err) {
1433
- if (requestId !== requestIdRef.current) return;
1539
+ if (!requests.isCurrent(requestId)) return;
1434
1540
  const handler = optionsRef.current.onError;
1435
1541
  if (!handler) throw err;
1436
1542
  handler(toError(err));
1437
1543
  }
1438
1544
  },
1439
- [source, surface]
1545
+ [source, element, requests]
1440
1546
  );
1441
- const recomputeRef = useRef6(recompute);
1547
+ const recomputeRef = useRef7(recompute);
1442
1548
  recomputeRef.current = recompute;
1443
- const trigger = useCallback2((opts) => {
1549
+ const trigger = useCallback3((opts) => {
1444
1550
  recomputeRef.current(opts).catch(() => {
1445
1551
  });
1446
1552
  }, []);
1447
1553
  useLayoutEffect2(() => {
1448
- requestIdRef.current++;
1554
+ requests.invalidate();
1449
1555
  pendingRestore.current = null;
1450
1556
  setLayout(null);
1451
1557
  setPageWidth(0);
@@ -1453,13 +1559,13 @@ function useSurfaceLayout(source, surface, options) {
1453
1559
  setContentHeight(0);
1454
1560
  setElapsedMs(0);
1455
1561
  }, [recompute]);
1456
- useEffect4(() => {
1562
+ useEffect5(() => {
1457
1563
  recompute().catch(() => {
1458
1564
  });
1459
1565
  return () => {
1460
- requestIdRef.current++;
1566
+ requests.invalidate();
1461
1567
  };
1462
- }, [recompute]);
1568
+ }, [recompute, requests]);
1463
1569
  useLayoutEffect2(() => {
1464
1570
  const anchor = pendingRestore.current;
1465
1571
  const restore = optionsRef.current.restorePosition;
@@ -1467,9 +1573,9 @@ function useSurfaceLayout(source, surface, options) {
1467
1573
  pendingRestore.current = null;
1468
1574
  restore(layout, anchor);
1469
1575
  }, [layout]);
1470
- useEffect4(() => {
1576
+ useEffect5(() => {
1471
1577
  if (!enableResize) return;
1472
- const el = surface.current;
1578
+ const el = element;
1473
1579
  if (!el) return;
1474
1580
  let timer = null;
1475
1581
  let observed = false;
@@ -1513,13 +1619,13 @@ function useSurfaceLayout(source, surface, options) {
1513
1619
  window.removeEventListener("resize", onWindowResize);
1514
1620
  clear();
1515
1621
  };
1516
- }, [surface, enableResize, resizeDebounce, trigger]);
1622
+ }, [element, enableResize, resizeDebounce, trigger]);
1517
1623
  return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
1518
1624
  }
1519
1625
 
1520
1626
  // src/useChapterLayout.ts
1521
1627
  function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1522
- const optionsRef = useRef7(options);
1628
+ const optionsRef = useRef8(options);
1523
1629
  optionsRef.current = options;
1524
1630
  const source = useMemo4(() => {
1525
1631
  const chapter = epub?.chapters[chapterIndex];
@@ -1538,92 +1644,92 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1538
1644
 
1539
1645
  // src/useEpub.ts
1540
1646
  import { parseEpub } from "@libraz/mejiro/epub";
1541
- import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef8, useState as useState5 } from "react";
1647
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef9, useState as useState5 } from "react";
1542
1648
  function useEpub(options = {}) {
1543
1649
  const [epub, setEpub] = useState5(null);
1544
1650
  const [loading, setLoading] = useState5(false);
1545
1651
  const [error, setError] = useState5(null);
1546
- const requestIdRef = useRef8(0);
1547
- const onLoadRef = useRef8(options.onLoad);
1652
+ const requests = useRequestToken();
1653
+ const onLoadRef = useRef9(options.onLoad);
1548
1654
  onLoadRef.current = options.onLoad;
1549
- const onErrorRef = useRef8(options.onError);
1655
+ const onErrorRef = useRef9(options.onError);
1550
1656
  onErrorRef.current = options.onError;
1551
- const fetchOptionsRef = useRef8(options.fetchOptions);
1657
+ const fetchOptionsRef = useRef9(options.fetchOptions);
1552
1658
  fetchOptionsRef.current = options.fetchOptions;
1553
- const fetchEpubRef = useRef8(options.fetchEpub);
1659
+ const fetchEpubRef = useRef9(options.fetchEpub);
1554
1660
  fetchEpubRef.current = options.fetchEpub;
1555
- const limitsRef = useRef8(options.limits);
1661
+ const limitsRef = useRef9(options.limits);
1556
1662
  limitsRef.current = options.limits;
1557
- const load = useCallback3(async (read) => {
1558
- const requestId = ++requestIdRef.current;
1559
- setLoading(true);
1560
- setError(null);
1561
- try {
1562
- const book = await parseEpub(await read(), { limits: limitsRef.current });
1563
- if (requestId !== requestIdRef.current) return null;
1564
- setEpub(book);
1565
- onLoadRef.current?.(book);
1566
- return book;
1567
- } catch (err) {
1568
- if (requestId === requestIdRef.current) {
1569
- const nextError = toError(err);
1570
- setError(nextError);
1571
- onErrorRef.current?.(nextError);
1663
+ const load = useCallback4(
1664
+ async (read) => {
1665
+ const requestId = requests.begin();
1666
+ setLoading(true);
1667
+ setError(null);
1668
+ try {
1669
+ const book = await parseEpub(await read(), { limits: limitsRef.current });
1670
+ if (!requests.isCurrent(requestId)) return null;
1671
+ setEpub(book);
1672
+ onLoadRef.current?.(book);
1673
+ return book;
1674
+ } catch (err) {
1675
+ if (requests.isCurrent(requestId)) {
1676
+ const nextError = toError(err);
1677
+ setError(nextError);
1678
+ onErrorRef.current?.(nextError);
1679
+ }
1680
+ return null;
1681
+ } finally {
1682
+ if (requests.isCurrent(requestId)) setLoading(false);
1572
1683
  }
1573
- return null;
1574
- } finally {
1575
- if (requestId === requestIdRef.current) setLoading(false);
1576
- }
1577
- }, []);
1578
- const loadBuffer = useCallback3((buffer) => load(async () => buffer), [load]);
1579
- const loadFile = useCallback3(
1684
+ },
1685
+ [requests]
1686
+ );
1687
+ const loadBuffer = useCallback4((buffer) => load(async () => buffer), [load]);
1688
+ const loadFile = useCallback4(
1580
1689
  (file) => load(() => readEpubFile(file, limitsRef.current)),
1581
1690
  [load]
1582
1691
  );
1583
- const loadUrl = useCallback3(
1692
+ const loadUrl = useCallback4(
1584
1693
  (url) => load(
1585
1694
  () => fetchEpubRef.current ? fetchEpubRef.current(url) : fetchEpubBuffer(url, fetchOptionsRef.current)
1586
1695
  ),
1587
1696
  [load]
1588
1697
  );
1589
1698
  const defaultUrl = options.defaultUrl;
1590
- useEffect5(() => {
1699
+ useEffect6(() => {
1591
1700
  if (!defaultUrl) return;
1592
1701
  void loadUrl(defaultUrl);
1593
- const requestId = requestIdRef.current;
1702
+ const requestId = requests.current();
1594
1703
  return () => {
1595
- if (requestIdRef.current !== requestId) return;
1596
- requestIdRef.current++;
1704
+ if (requests.current() !== requestId) return;
1705
+ requests.invalidate();
1597
1706
  setLoading(false);
1598
1707
  };
1599
- }, [defaultUrl, loadUrl]);
1600
- useEffect5(
1601
- () => () => {
1602
- requestIdRef.current++;
1708
+ }, [defaultUrl, loadUrl, requests]);
1709
+ const replaceEpub = useCallback4(
1710
+ (book) => {
1711
+ requests.invalidate();
1712
+ setLoading(false);
1713
+ setError(null);
1714
+ setEpub(book);
1603
1715
  },
1604
- []
1716
+ [requests]
1605
1717
  );
1606
- const replaceEpub = useCallback3((book) => {
1607
- requestIdRef.current++;
1608
- setLoading(false);
1609
- setError(null);
1610
- setEpub(book);
1611
- }, []);
1612
1718
  return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
1613
1719
  }
1614
1720
 
1615
1721
  // src/useMejiroBook.ts
1616
1722
  import { MejiroBook } from "@libraz/mejiro/book";
1617
- import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef9, useState as useState6 } from "react";
1723
+ import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef10, useState as useState6 } from "react";
1618
1724
  function useMejiroBook(initial, options = {}) {
1619
- const bookRef = useRef9(null);
1725
+ const bookRef = useRef10(null);
1620
1726
  if (!bookRef.current) bookRef.current = new MejiroBook(initial);
1621
1727
  const [snapshot, setLocal] = useState6(initial);
1622
- const optionsRef = useRef9(options);
1728
+ const optionsRef = useRef10(options);
1623
1729
  optionsRef.current = options;
1624
- const pendingRef = useRef9(null);
1625
- const flushGenerationRef = useRef9(0);
1626
- const flush = useCallback4(() => {
1730
+ const pendingRef = useRef10(null);
1731
+ const flushGenerationRef = useRef10(0);
1732
+ const flush = useCallback5(() => {
1627
1733
  const pending = pendingRef.current;
1628
1734
  if (!pending) return;
1629
1735
  pendingRef.current = null;
@@ -1654,7 +1760,7 @@ function useMejiroBook(initial, options = {}) {
1654
1760
  }
1655
1761
  );
1656
1762
  }, []);
1657
- const setOptions = useCallback4(
1763
+ const setOptions = useCallback5(
1658
1764
  (next) => {
1659
1765
  setLocal((prev) => mergeDefined(prev, next));
1660
1766
  const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
@@ -1678,7 +1784,7 @@ function useMejiroBook(initial, options = {}) {
1678
1784
  },
1679
1785
  [flush]
1680
1786
  );
1681
- useEffect6(
1787
+ useEffect7(
1682
1788
  () => () => {
1683
1789
  bookRef.current?.dispose();
1684
1790
  const pending = pendingRef.current;
@@ -1693,7 +1799,7 @@ function useMejiroBook(initial, options = {}) {
1693
1799
  }
1694
1800
 
1695
1801
  // src/useSpread.ts
1696
- import { useCallback as useCallback5, useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as useRef10, useState as useState7 } from "react";
1802
+ import { useCallback as useCallback6, useEffect as useEffect8, useLayoutEffect as useLayoutEffect3, useRef as useRef11, useState as useState7 } from "react";
1697
1803
  function ignoreNavigationKey(e) {
1698
1804
  if (e.defaultPrevented) return true;
1699
1805
  if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
@@ -1723,7 +1829,7 @@ function useSpread(layout, options = {}) {
1723
1829
  const enableKeyboard = options.enableKeyboard ?? true;
1724
1830
  const turnDuration = options.turnDuration ?? 180;
1725
1831
  const single = options.single ?? false;
1726
- const onChangeRef = useRef10(options.onChange);
1832
+ const onChangeRef = useRef11(options.onChange);
1727
1833
  onChangeRef.current = options.onChange;
1728
1834
  const [spreadIdx, setSpreadIdx] = useState7(0);
1729
1835
  const [indexSingle, setIndexSingle] = useState7(single);
@@ -1733,22 +1839,22 @@ function useSpread(layout, options = {}) {
1733
1839
  }
1734
1840
  const [spread, setSpread] = useState7(null);
1735
1841
  const [turning, setTurning] = useState7(false);
1736
- const layoutRef = useRef10(null);
1842
+ const layoutRef = useRef11(null);
1737
1843
  layoutRef.current = layout;
1738
- const spreadIdxRef = useRef10(0);
1844
+ const spreadIdxRef = useRef11(0);
1739
1845
  spreadIdxRef.current = spreadIdx;
1740
- const singleRef = useRef10(single);
1846
+ const singleRef = useRef11(single);
1741
1847
  singleRef.current = single;
1742
- const turnTimerRef = useRef10(null);
1743
- const turnTargetRef = useRef10(null);
1744
- const layoutGenerationRef = useRef10(0);
1745
- const lastEmittedRef = useRef10(null);
1848
+ const turnTimerRef = useRef11(null);
1849
+ const turnTargetRef = useRef11(null);
1850
+ const layoutGenerationRef = useRef11(0);
1851
+ const lastEmittedRef = useRef11(null);
1746
1852
  const totalPages = spread?.totalPages ?? 0;
1747
1853
  const totalSpreads = navigationCount(totalPages, single);
1748
1854
  const firstPage = firstPageOf(spreadIdx, single);
1749
1855
  const layoutSpreadIdx = Math.floor(firstPage / 2);
1750
1856
  const singleSide = single ? firstPage % 2 === 0 ? "right" : "left" : null;
1751
- const refresh = useCallback5(() => {
1857
+ const refresh = useCallback6(() => {
1752
1858
  if (!layoutRef.current) {
1753
1859
  setSpread(null);
1754
1860
  return;
@@ -1771,7 +1877,7 @@ function useSpread(layout, options = {}) {
1771
1877
  }
1772
1878
  setSpread(layout.getSpread(0));
1773
1879
  }, [layout]);
1774
- useEffect7(() => {
1880
+ useEffect8(() => {
1775
1881
  if (!layout) return;
1776
1882
  setSpread(layout.getSpread(layoutSpreadIdx));
1777
1883
  const last = lastEmittedRef.current;
@@ -1784,12 +1890,12 @@ function useSpread(layout, options = {}) {
1784
1890
  }
1785
1891
  lastEmittedRef.current = { layout, spreadIdx };
1786
1892
  }, [layout, spreadIdx, layoutSpreadIdx]);
1787
- const navigationTarget = useCallback5(() => {
1893
+ const navigationTarget = useCallback6(() => {
1788
1894
  const pending = turnTargetRef.current;
1789
1895
  if (!pending) return spreadIdxRef.current;
1790
1896
  return indexOfPageIn(firstPageOf(pending.index, pending.single), singleRef.current);
1791
1897
  }, []);
1792
- const goTo = useCallback5(
1898
+ const goTo = useCallback6(
1793
1899
  (index) => {
1794
1900
  if (!layoutRef.current) return;
1795
1901
  const targetSingle = singleRef.current;
@@ -1823,7 +1929,7 @@ function useSpread(layout, options = {}) {
1823
1929
  },
1824
1930
  [turnDuration, navigationTarget]
1825
1931
  );
1826
- const setSpreadIndex = useCallback5((index) => {
1932
+ const setSpreadIndex = useCallback6((index) => {
1827
1933
  if (!layoutRef.current) return;
1828
1934
  if (turnTimerRef.current) {
1829
1935
  clearTimeout(turnTimerRef.current);
@@ -1834,17 +1940,17 @@ function useSpread(layout, options = {}) {
1834
1940
  const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
1835
1941
  setSpreadIdx(Math.max(0, Math.min(max, index)));
1836
1942
  }, []);
1837
- const indexOfPage = useCallback5((pageIdx) => indexOfPageIn(pageIdx, single), [single]);
1838
- const anchorAt = useCallback5(
1943
+ const indexOfPage = useCallback6((pageIdx) => indexOfPageIn(pageIdx, single), [single]);
1944
+ const anchorAt = useCallback6(
1839
1945
  (index) => {
1840
1946
  if (!layout || index < 0 || index >= navigationCount(layout.totalPages, single)) return null;
1841
1947
  return firstTextAnchorFrom(layout, firstPageOf(index, single));
1842
1948
  },
1843
1949
  [layout, single]
1844
1950
  );
1845
- const next = useCallback5(() => goTo(navigationTarget() + 1), [goTo, navigationTarget]);
1846
- const prev = useCallback5(() => goTo(navigationTarget() - 1), [goTo, navigationTarget]);
1847
- useEffect7(() => {
1951
+ const next = useCallback6(() => goTo(navigationTarget() + 1), [goTo, navigationTarget]);
1952
+ const prev = useCallback6(() => goTo(navigationTarget() - 1), [goTo, navigationTarget]);
1953
+ useEffect8(() => {
1848
1954
  if (!enableKeyboard) return;
1849
1955
  const onKey = (e) => {
1850
1956
  if (!layoutRef.current) return;
@@ -1855,7 +1961,7 @@ function useSpread(layout, options = {}) {
1855
1961
  window.addEventListener("keydown", onKey);
1856
1962
  return () => window.removeEventListener("keydown", onKey);
1857
1963
  }, [enableKeyboard, next, prev]);
1858
- useEffect7(
1964
+ useEffect8(
1859
1965
  () => () => {
1860
1966
  if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
1861
1967
  },
@@ -1949,6 +2055,10 @@ function createAnchorResolver(nav) {
1949
2055
  const update = (target) => {
1950
2056
  const current = pending;
1951
2057
  if (!(current && target.book)) return;
2058
+ if (current.moved) {
2059
+ settle();
2060
+ return;
2061
+ }
1952
2062
  const { anchor } = current;
1953
2063
  if (!(anchor.chapter >= 0 && anchor.chapter < target.book.chapters.length)) {
1954
2064
  settle();
@@ -1965,13 +2075,16 @@ function createAnchorResolver(nav) {
1965
2075
  paragraph: anchor.paragraph,
1966
2076
  charIndex: anchor.charIndex
1967
2077
  });
1968
- if (loc) nav.goToPage(loc.pageIdx);
2078
+ if (loc && nav.goToPage(loc.pageIdx)) {
2079
+ current.moved = true;
2080
+ return;
2081
+ }
1969
2082
  settle();
1970
2083
  };
1971
2084
  return {
1972
2085
  request: (anchor, target) => new Promise((resolve) => {
1973
2086
  settle();
1974
- pending = { anchor, resolve, chapterRequested: false };
2087
+ pending = { anchor, resolve, chapterRequested: false, moved: false };
1975
2088
  update(target);
1976
2089
  }),
1977
2090
  update,
@@ -2024,8 +2137,8 @@ function MejiroReaderInner(props, ref) {
2024
2137
  onPageRead,
2025
2138
  onChapterCompleted
2026
2139
  } = props;
2027
- const surfaceRef = useRef11(null);
2028
- const fileRef = useRef11(null);
2140
+ const surfaceRef = useRef12(null);
2141
+ const fileRef = useRef12(null);
2029
2142
  const [settingsOpen, setSettingsOpen] = useState8(false);
2030
2143
  const [chapterState, setChapterState] = useState8(chapterProp ?? 0);
2031
2144
  const chapterIsUncontrolled = chapterProp == null;
@@ -2034,7 +2147,7 @@ function MejiroReaderInner(props, ref) {
2034
2147
  null
2035
2148
  );
2036
2149
  const [autoSingle, setAutoSingle] = useState8(false);
2037
- useEffect8(() => {
2150
+ useEffect9(() => {
2038
2151
  if (!settingsOpen) return;
2039
2152
  const onKey = (e2) => {
2040
2153
  if (e2.key === "Escape") setSettingsOpen(false);
@@ -2042,20 +2155,23 @@ function MejiroReaderInner(props, ref) {
2042
2155
  window.addEventListener("keydown", onKey);
2043
2156
  return () => window.removeEventListener("keydown", onKey);
2044
2157
  }, [settingsOpen]);
2045
- useEffect8(() => {
2158
+ useEffect9(() => {
2046
2159
  if (spreadMode !== "auto") return;
2047
2160
  const surface = surfaceRef.current;
2048
2161
  if (!surface) return;
2049
2162
  const update = () => {
2050
2163
  const rect = surface.getBoundingClientRect();
2051
- setAutoSingle(rect.width < rect.height);
2164
+ setAutoSingle(rect.width < (fit === "width" ? window.innerHeight : rect.height));
2052
2165
  };
2053
2166
  update();
2054
- if (typeof ResizeObserver === "undefined") return;
2055
- const observer = new ResizeObserver(update);
2056
- observer.observe(surface);
2057
- return () => observer.disconnect();
2058
- }, [spreadMode]);
2167
+ window.addEventListener("resize", update);
2168
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
2169
+ observer?.observe(surface);
2170
+ return () => {
2171
+ window.removeEventListener("resize", update);
2172
+ observer?.disconnect();
2173
+ };
2174
+ }, [spreadMode, fit]);
2059
2175
  const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
2060
2176
  const resolvedGeometry = useMemo5(() => {
2061
2177
  const columns = effectiveSingle ? 1 : 2;
@@ -2082,9 +2198,9 @@ function MejiroReaderInner(props, ref) {
2082
2198
  () => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
2083
2199
  [optionsProp]
2084
2200
  );
2085
- const onErrorRef = useRef11(onError);
2201
+ const onErrorRef = useRef12(onError);
2086
2202
  onErrorRef.current = onError;
2087
- const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
2203
+ const reportError = useCallback7((error) => onErrorRef.current?.(error), []);
2088
2204
  const {
2089
2205
  book,
2090
2206
  options: bookOptions,
@@ -2093,8 +2209,8 @@ function MejiroReaderInner(props, ref) {
2093
2209
  debounceMs: OPTIONS_DEBOUNCE_MS,
2094
2210
  onError: reportError
2095
2211
  });
2096
- const lastApplyRef = useRef11(Promise.resolve());
2097
- const setOptions = useCallback6(
2212
+ const lastApplyRef = useRef12(Promise.resolve());
2213
+ const setOptions = useCallback7(
2098
2214
  (partial) => {
2099
2215
  const applied = applyBookOptions(partial);
2100
2216
  lastApplyRef.current = applied;
@@ -2103,10 +2219,10 @@ function MejiroReaderInner(props, ref) {
2103
2219
  [applyBookOptions]
2104
2220
  );
2105
2221
  const optionsPropKey = JSON.stringify(resolvedOptions);
2106
- const syncedOptionsPropKeyRef = useRef11(optionsPropKey);
2107
- const resolvedOptionsRef = useRef11(resolvedOptions);
2222
+ const syncedOptionsPropKeyRef = useRef12(optionsPropKey);
2223
+ const resolvedOptionsRef = useRef12(resolvedOptions);
2108
2224
  resolvedOptionsRef.current = resolvedOptions;
2109
- useEffect8(() => {
2225
+ useEffect9(() => {
2110
2226
  if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
2111
2227
  syncedOptionsPropKeyRef.current = optionsPropKey;
2112
2228
  void setOptions(resolvedOptionsRef.current);
@@ -2125,7 +2241,6 @@ function MejiroReaderInner(props, ref) {
2125
2241
  limits,
2126
2242
  fetchEpub: fetchEpubFn,
2127
2243
  onLoad: (b) => {
2128
- if (chapterProp == null) setChapterState(0);
2129
2244
  clearImagesRef.current();
2130
2245
  onLoad?.(b);
2131
2246
  }
@@ -2133,22 +2248,43 @@ function MejiroReaderInner(props, ref) {
2133
2248
  const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
2134
2249
  const isManuscriptSource = manuscriptProp !== void 0;
2135
2250
  const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
2136
- const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
2251
+ const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : bookIdentity(epubProp);
2137
2252
  const bookKey = controlled ? sourceKey : epubCtx.epub;
2138
- const chapter = clampChapter(chapterProp ?? chapterState, e);
2253
+ const [seenBookKey, setSeenBookKey] = useState8(bookKey);
2254
+ let chapterRaw = chapterState;
2255
+ if (seenBookKey !== bookKey) {
2256
+ setSeenBookKey(bookKey);
2257
+ if (chapterIsUncontrolled) {
2258
+ setChapterState(0);
2259
+ chapterRaw = 0;
2260
+ }
2261
+ }
2262
+ const chapter = clampChapter(chapterProp ?? chapterRaw, e);
2263
+ const onChapterChangeRef = useRef12(onChapterChange);
2264
+ onChapterChangeRef.current = onChapterChange;
2265
+ const shownChapterRef = useRef12({ book: seenBookKey, chapter });
2266
+ useEffect9(() => {
2267
+ const prev = shownChapterRef.current;
2268
+ shownChapterRef.current = { book: seenBookKey, chapter };
2269
+ if (chapterIsUncontrolled && prev.book !== seenBookKey && prev.chapter !== chapter) {
2270
+ onChapterChangeRef.current?.(chapter);
2271
+ }
2272
+ }, [seenBookKey, chapter, chapterIsUncontrolled]);
2139
2273
  const filePicker = enableDropZone && !controlled;
2140
- useEffect8(() => {
2274
+ useEffect9(() => {
2141
2275
  if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
2142
2276
  }, [epubCtx.error]);
2143
- const pageGeometryRef = useRef11(resolvedGeometry);
2277
+ const pageGeometryRef = useRef12(resolvedGeometry);
2144
2278
  pageGeometryRef.current = resolvedGeometry;
2145
- const spreadAnchorRef = useRef11(() => null);
2146
- const spreadIdxPropRef = useRef11(spreadIdxProp);
2279
+ const spreadAnchorRef = useRef12(() => null);
2280
+ const spreadIdxPropRef = useRef12(spreadIdxProp);
2147
2281
  spreadIdxPropRef.current = spreadIdxProp;
2148
- const visibleAnchorRef = useRef11(
2282
+ const visibleAnchorRef = useRef12(
2149
2283
  null
2150
2284
  );
2151
- const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef, {
2285
+ const layoutMemoRef = useRef12(null);
2286
+ layoutMemoRef.current = resolveLayoutEpub(layoutMemoRef.current, e, bookKey, chapter);
2287
+ const layoutCtx = useChapterLayout(book, layoutMemoRef.current.epub, chapter, surfaceRef, {
2152
2288
  pageGeometry: () => pageGeometryRef.current,
2153
2289
  // Preserve the reading position across a reflow re-layout (size / option
2154
2290
  // changes), but only in uncontrolled mode — when `spreadIdx` is controlled
@@ -2161,8 +2297,8 @@ function MejiroReaderInner(props, ref) {
2161
2297
  },
2162
2298
  onError: reportError
2163
2299
  });
2164
- const reportedSpreadIdxRef = useRef11(null);
2165
- const spreadChangedRef = useRef11(void 0);
2300
+ const reportedSpreadIdxRef = useRef12(null);
2301
+ const spreadChangedRef = useRef12(void 0);
2166
2302
  spreadChangedRef.current = (i) => {
2167
2303
  reportedSpreadIdxRef.current = i;
2168
2304
  onSpreadChange?.(i);
@@ -2174,9 +2310,9 @@ function MejiroReaderInner(props, ref) {
2174
2310
  onChange: (i) => spreadChangedRef.current?.(i)
2175
2311
  });
2176
2312
  spreadAnchorRef.current = () => spreadCtx.anchorAt(spreadCtx.spreadIdx);
2177
- const indexOfPageRef = useRef11(spreadCtx.indexOfPage);
2313
+ const indexOfPageRef = useRef12(spreadCtx.indexOfPage);
2178
2314
  indexOfPageRef.current = spreadCtx.indexOfPage;
2179
- const anchorSingleRef = useRef11(effectiveSingle);
2315
+ const anchorSingleRef = useRef12(effectiveSingle);
2180
2316
  useLayoutEffect4(() => {
2181
2317
  const l = layoutCtx.layout;
2182
2318
  const flipped = anchorSingleRef.current !== effectiveSingle;
@@ -2184,7 +2320,7 @@ function MejiroReaderInner(props, ref) {
2184
2320
  if (flipped && l && visibleAnchorRef.current?.layout === l) return;
2185
2321
  visibleAnchorRef.current = l ? { layout: l, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
2186
2322
  }, [layoutCtx.layout, spreadCtx.spreadIdx, spreadCtx.anchorAt, effectiveSingle]);
2187
- const setSpreadRef = useRef11(spreadCtx.setSpread);
2323
+ const setSpreadRef = useRef12(spreadCtx.setSpread);
2188
2324
  setSpreadRef.current = spreadCtx.setSpread;
2189
2325
  useLayoutEffect4(() => {
2190
2326
  const anchor = layoutCtx.pendingRestore.current;
@@ -2195,10 +2331,10 @@ function MejiroReaderInner(props, ref) {
2195
2331
  visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
2196
2332
  }, [layoutCtx.layout]);
2197
2333
  const optionsKey = reflowOptionsKey(bookOptions);
2198
- const optionsKeyRef = useRef11(optionsKey);
2199
- const recomputeRef = useRef11(layoutCtx.recompute);
2334
+ const optionsKeyRef = useRef12(optionsKey);
2335
+ const recomputeRef = useRef12(layoutCtx.recompute);
2200
2336
  recomputeRef.current = layoutCtx.recompute;
2201
- useEffect8(() => {
2337
+ useEffect9(() => {
2202
2338
  if (optionsKeyRef.current === optionsKey) return;
2203
2339
  optionsKeyRef.current = optionsKey;
2204
2340
  const timer = setTimeout(() => {
@@ -2214,8 +2350,8 @@ function MejiroReaderInner(props, ref) {
2214
2350
  return () => clearTimeout(timer);
2215
2351
  }, [optionsKey, reportError]);
2216
2352
  const geometryKey = JSON.stringify(resolvedGeometry ?? null);
2217
- const geometryKeyRef = useRef11(geometryKey);
2218
- useEffect8(() => {
2353
+ const geometryKeyRef = useRef12(geometryKey);
2354
+ useEffect9(() => {
2219
2355
  if (geometryKeyRef.current === geometryKey) return;
2220
2356
  geometryKeyRef.current = geometryKey;
2221
2357
  void recomputeRef.current({ blank: false });
@@ -2237,30 +2373,29 @@ function MejiroReaderInner(props, ref) {
2237
2373
  }
2238
2374
  return result;
2239
2375
  }, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
2240
- const clearImagesRef = useRef11(imageCtx.clearImages);
2376
+ const clearImagesRef = useRef12(imageCtx.clearImages);
2241
2377
  clearImagesRef.current = imageCtx.clearImages;
2242
- const onLoadRef = useRef11(onLoad);
2378
+ const onLoadRef = useRef12(onLoad);
2243
2379
  onLoadRef.current = onLoad;
2244
- const eRef = useRef11(e);
2380
+ const eRef = useRef12(e);
2245
2381
  eRef.current = e;
2246
- useEffect8(() => {
2382
+ useEffect9(() => {
2247
2383
  if (!controlled) return;
2248
- if (chapterIsUncontrolled) setChapterState(0);
2249
2384
  clearImagesRef.current();
2250
2385
  book.clearCache();
2251
2386
  if (eRef.current) onLoadRef.current?.(eRef.current);
2252
- }, [controlled, sourceKey, book, chapterIsUncontrolled]);
2253
- const imagesChapterRef = useRef11(chapter);
2254
- useEffect8(() => {
2387
+ }, [controlled, sourceKey, book]);
2388
+ const imagesChapterRef = useRef12(chapter);
2389
+ useEffect9(() => {
2255
2390
  if (imagesChapterRef.current === chapter) return;
2256
2391
  imagesChapterRef.current = chapter;
2257
2392
  clearImagesRef.current();
2258
2393
  }, [chapter]);
2259
- const spreadGoToRef = useRef11(spreadCtx.goTo);
2394
+ const spreadGoToRef = useRef12(spreadCtx.goTo);
2260
2395
  spreadGoToRef.current = spreadCtx.goTo;
2261
- const appliedSpreadIdxPropRef = useRef11(spreadIdxProp);
2396
+ const appliedSpreadIdxPropRef = useRef12(spreadIdxProp);
2262
2397
  const [hostTurn, setHostTurn] = useState8(0);
2263
- useEffect8(() => {
2398
+ useEffect9(() => {
2264
2399
  const reported = reportedSpreadIdxRef.current;
2265
2400
  reportedSpreadIdxRef.current = null;
2266
2401
  if (spreadIdxProp == null) {
@@ -2274,12 +2409,17 @@ function MejiroReaderInner(props, ref) {
2274
2409
  else if (reported === spreadCtx.spreadIdx) setHostTurn((n) => n + 1);
2275
2410
  else setSpreadRef.current(spreadIdxProp);
2276
2411
  }, [spreadIdxProp, spreadCtx.spreadIdx, hostTurn]);
2412
+ const restoredForRef = useRef12(null);
2277
2413
  useLayoutEffect4(() => {
2414
+ if (!layoutCtx.layout) return;
2415
+ const previous = restoredForRef.current;
2416
+ restoredForRef.current = { chapter, book: bookKey };
2417
+ if (previous && (previous.chapter !== chapter || previous.book !== bookKey)) return;
2278
2418
  const next = spreadIdxPropRef.current;
2279
- if (next == null || !layoutCtx.layout) return;
2419
+ if (next == null) return;
2280
2420
  setSpreadRef.current(next);
2281
2421
  }, [layoutCtx.layout]);
2282
- const onChapter = useCallback6(
2422
+ const onChapter = useCallback7(
2283
2423
  (index) => {
2284
2424
  const i = clampChapter(index, e);
2285
2425
  if (i === chapter) return;
@@ -2288,10 +2428,10 @@ function MejiroReaderInner(props, ref) {
2288
2428
  },
2289
2429
  [chapter, chapterProp, onChapterChange, e]
2290
2430
  );
2291
- const onChapterRef = useRef11(onChapter);
2431
+ const onChapterRef = useRef12(onChapter);
2292
2432
  onChapterRef.current = onChapter;
2293
- const listenersRef = useRef11(/* @__PURE__ */ new Map());
2294
- const emit = useCallback6(
2433
+ const listenersRef = useRef12(/* @__PURE__ */ new Map());
2434
+ const emit = useCallback7(
2295
2435
  (event, payload) => {
2296
2436
  const set = listenersRef.current.get(event);
2297
2437
  if (!set) return;
@@ -2299,7 +2439,7 @@ function MejiroReaderInner(props, ref) {
2299
2439
  },
2300
2440
  []
2301
2441
  );
2302
- const layoutOwnerRef = useRef11({
2442
+ const layoutOwnerRef = useRef12({
2303
2443
  layout: null,
2304
2444
  chapter,
2305
2445
  book: bookKey
@@ -2310,22 +2450,29 @@ function MejiroReaderInner(props, ref) {
2310
2450
  const owner = layoutOwnerRef.current;
2311
2451
  const layout = owner.chapter === chapter && owner.book === bookKey ? layoutCtx.layout : null;
2312
2452
  const anchorTarget = { book: epubCtx.loading ? null : e, chapter, layout };
2313
- const anchorTargetRef = useRef11(anchorTarget);
2453
+ const anchorTargetRef = useRef12(anchorTarget);
2314
2454
  anchorTargetRef.current = anchorTarget;
2315
- const anchorResolverRef = useRef11(null);
2455
+ const spreadIdxNowRef = useRef12(spreadCtx.spreadIdx);
2456
+ spreadIdxNowRef.current = spreadCtx.spreadIdx;
2457
+ const anchorResolverRef = useRef12(null);
2316
2458
  anchorResolverRef.current ??= createAnchorResolver({
2317
2459
  goToChapter: (i) => onChapterRef.current(i),
2318
- goToPage: (pageIdx) => spreadGoToRef.current(indexOfPageRef.current(pageIdx))
2460
+ goToPage: (pageIdx) => {
2461
+ const target = indexOfPageRef.current(pageIdx);
2462
+ if (target === spreadIdxNowRef.current) return false;
2463
+ setSpreadRef.current(target);
2464
+ return true;
2465
+ }
2319
2466
  });
2320
- useEffect8(() => {
2467
+ useEffect9(() => {
2321
2468
  anchorResolverRef.current?.update(anchorTargetRef.current);
2322
- }, [anchorTarget.book, chapter, layout]);
2323
- useEffect8(() => () => anchorResolverRef.current?.dispose(), []);
2324
- const onPageReadRef = useRef11(onPageRead);
2469
+ }, [anchorTarget.book, chapter, layout, spreadCtx.spreadIdx]);
2470
+ useEffect9(() => () => anchorResolverRef.current?.dispose(), []);
2471
+ const onPageReadRef = useRef12(onPageRead);
2325
2472
  onPageReadRef.current = onPageRead;
2326
- const onChapterCompletedRef = useRef11(onChapterCompleted);
2473
+ const onChapterCompletedRef = useRef12(onChapterCompleted);
2327
2474
  onChapterCompletedRef.current = onChapterCompleted;
2328
- const trackLifecycleRef = useRef11(null);
2475
+ const trackLifecycleRef = useRef12(null);
2329
2476
  trackLifecycleRef.current ??= createLifecycleTracker({
2330
2477
  spreadChanged: (ch, spreadIdx) => emit("spreadChanged", { chapter: ch, spreadIdx }),
2331
2478
  chapterFinished: (ch) => {
@@ -2334,7 +2481,7 @@ function MejiroReaderInner(props, ref) {
2334
2481
  },
2335
2482
  pageRead: (anchor, dwellMs) => onPageReadRef.current?.(anchor, dwellMs)
2336
2483
  });
2337
- useEffect8(() => {
2484
+ useEffect9(() => {
2338
2485
  const view = layout ? { total: spreadCtx.totalSpreads, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
2339
2486
  trackLifecycleRef.current?.(bookKey, chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
2340
2487
  }, [
@@ -2346,8 +2493,8 @@ function MejiroReaderInner(props, ref) {
2346
2493
  layout,
2347
2494
  spreadIdxProp
2348
2495
  ]);
2349
- const prevTurningRef = useRef11(false);
2350
- useEffect8(() => {
2496
+ const prevTurningRef = useRef12(false);
2497
+ useEffect9(() => {
2351
2498
  if (spreadCtx.turning && !prevTurningRef.current) {
2352
2499
  emit("turnStart", { from: spreadCtx.spreadIdx });
2353
2500
  } else if (!spreadCtx.turning && prevTurningRef.current) {
@@ -2500,11 +2647,9 @@ function MejiroReaderInner(props, ref) {
2500
2647
  ] })
2501
2648
  ] }) : null;
2502
2649
  const currentSpread = spreadCtx.layoutSpreadIdx;
2503
- const rightPage = currentSpread * 2 + 1;
2504
2650
  const leftPage = currentSpread * 2 + 2;
2505
2651
  const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
2506
- const showRightNum = pageNumbers === "both" || pageNumbers === "right";
2507
- const showLeftNum = pageNumbers === "both" || pageNumbers === "left";
2652
+ const pageIndicator = enablePageIndicator ? /* @__PURE__ */ jsx13(MejiroPageIndicator, { current: spreadCtx.spreadIdx + 1, total: spreadCtx.totalSpreads }) : null;
2508
2653
  return /* @__PURE__ */ jsx13(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs8(
2509
2654
  "div",
2510
2655
  {
@@ -2549,18 +2694,25 @@ function MejiroReaderInner(props, ref) {
2549
2694
  ),
2550
2695
  onVisiblePageChange: (pageIdx, source) => {
2551
2696
  if (source === "programmatic") return;
2552
- if (layoutCtx.layout) setUserScroll({ layout: layoutCtx.layout, page: pageIdx });
2697
+ const scrolledLayout = layoutCtx.layout;
2698
+ if (scrolledLayout) {
2699
+ setUserScroll(
2700
+ (prev) => prev?.layout === scrolledLayout && prev.page === pageIdx ? prev : { layout: scrolledLayout, page: pageIdx }
2701
+ );
2702
+ }
2553
2703
  const target = spreadCtx.indexOfPage(pageIdx);
2554
2704
  if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
2555
2705
  },
2556
2706
  selectionRects: annotationRects.length ? annotationRects : void 0,
2557
2707
  spreadIdx: spreadCtx.layoutSpreadIdx,
2558
2708
  images: imageCtx.currentImages,
2709
+ pageNumbers,
2559
2710
  onImagePointerDown: imageCtx.onOverlayPointerDown,
2560
2711
  onImageResizePointerDown: imageCtx.onResizePointerDown,
2561
2712
  onImageClose: imageCtx.removeImage
2562
2713
  }
2563
2714
  ),
2715
+ layoutReady && mode === "scroll" && layoutCtx.layout && pageIndicator,
2564
2716
  layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
2565
2717
  MejiroSpread,
2566
2718
  {
@@ -2578,20 +2730,14 @@ function MejiroReaderInner(props, ref) {
2578
2730
  turning: spreadCtx.turning,
2579
2731
  rightHeader: {
2580
2732
  title: runningTitleRight,
2581
- pageNumber: showRightNum ? rightPage : null
2733
+ pageNumber: pageNumberFor2(pageNumbers, currentSpread * 2)
2582
2734
  },
2583
2735
  leftHeader: {
2584
2736
  title: runningTitleLeft,
2585
- pageNumber: showLeft && showLeftNum ? leftPage : null
2737
+ pageNumber: showLeft ? pageNumberFor2(pageNumbers, currentSpread * 2 + 1) : null
2586
2738
  },
2587
2739
  images: imageCtx.currentImages,
2588
- indicator: enablePageIndicator ? /* @__PURE__ */ jsx13(
2589
- MejiroPageIndicator,
2590
- {
2591
- current: spreadCtx.spreadIdx + 1,
2592
- total: spreadCtx.totalSpreads
2593
- }
2594
- ) : null,
2740
+ indicator: pageIndicator,
2595
2741
  onPrev: spreadCtx.prev,
2596
2742
  onNext: spreadCtx.next,
2597
2743
  onImagePointerDown: imageCtx.onOverlayPointerDown,
@@ -2697,6 +2843,14 @@ function watermarkedBook(book, watermark) {
2697
2843
  const copy = cloneEditableEpubBook(book);
2698
2844
  const text = `${WATERMARK_PREFIX} ${watermark.text}`;
2699
2845
  for (const chapter of copy.chapters) {
2846
+ if (!chapter.isDirty && chapter.originalXhtml) {
2847
+ const injected = injectWatermarkXhtml(chapter.originalXhtml, text);
2848
+ if (injected) {
2849
+ chapter.originalXhtml = injected;
2850
+ continue;
2851
+ }
2852
+ chapter.isDirty = true;
2853
+ }
2700
2854
  chapter.blocks.unshift({
2701
2855
  kind: "paragraph",
2702
2856
  id: watermarkBlockId(chapter),
@@ -2704,10 +2858,19 @@ function watermarkedBook(book, watermark) {
2704
2858
  inlineAnnotations: []
2705
2859
  });
2706
2860
  chapter.paragraphs.unshift({ text, inlineAnnotations: [] });
2707
- chapter.isDirty = true;
2708
2861
  }
2709
2862
  return copy;
2710
2863
  }
2864
+ function injectWatermarkXhtml(xhtml, text) {
2865
+ if (!/<html\b[^>]*\sxmlns\s*=\s*["']http:\/\/www\.w3\.org\/1999\/xhtml["']/i.test(xhtml)) {
2866
+ return null;
2867
+ }
2868
+ const body = /<body\b(?:[^>]*[^/>])?>/i.exec(xhtml);
2869
+ if (!body) return null;
2870
+ const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2871
+ const end = body.index + body[0].length;
2872
+ return `${xhtml.slice(0, end)}<p>${escaped}</p>${xhtml.slice(end)}`;
2873
+ }
2711
2874
  function watermarkBlockId(chapter) {
2712
2875
  const used = new Set(chapter.blocks.map((block) => block.id));
2713
2876
  let id = WATERMARK_BLOCK_ID;
@@ -2731,9 +2894,9 @@ function MejiroEditor({
2731
2894
  onError
2732
2895
  }) {
2733
2896
  const messages = useI18n();
2734
- const imageInputRef = useRef12(null);
2735
- const textareaRef = useRef12(null);
2736
- const readerRef = useRef12(null);
2897
+ const imageInputRef = useRef13(null);
2898
+ const textareaRef = useRef13(null);
2899
+ const readerRef = useRef13(null);
2737
2900
  const [editor, setEditor] = useState9(null);
2738
2901
  const [loading, setLoading] = useState9(false);
2739
2902
  const [error, setError] = useState9(null);
@@ -2743,10 +2906,10 @@ function MejiroEditor({
2743
2906
  const [rubyStart, setRubyStart] = useState9(0);
2744
2907
  const [rubyEnd, setRubyEnd] = useState9(1);
2745
2908
  const [rubyText, setRubyText] = useState9("");
2746
- const loadRequestIdRef = useRef12(0);
2747
- const onLoadRef = useRef12(onLoad);
2748
- const onErrorRef = useRef12(onError);
2749
- const limitsRef = useRef12(limits);
2909
+ const request = useRequestToken();
2910
+ const onLoadRef = useRef13(onLoad);
2911
+ const onErrorRef = useRef13(onError);
2912
+ const limitsRef = useRef13(limits);
2750
2913
  limitsRef.current = limits;
2751
2914
  const book = editor?.book ?? null;
2752
2915
  const chapter = book?.chapters[selection.chapter] ?? null;
@@ -2755,75 +2918,77 @@ function MejiroEditor({
2755
2918
  const rubyRange = clampRubyRange(rubyStart, rubyEnd, [...text].length);
2756
2919
  const previewBook = useMemo6(() => {
2757
2920
  void revision;
2758
- return book ? cloneEditableEpubBook(book) : null;
2759
- }, [book, revision]);
2760
- useEffect9(() => {
2921
+ return book && editor ? markBookIdentity(cloneEditableEpubBook(book), editor) : null;
2922
+ }, [book, editor, revision]);
2923
+ useEffect10(() => {
2761
2924
  onLoadRef.current = onLoad;
2762
2925
  }, [onLoad]);
2763
- useEffect9(() => {
2926
+ useEffect10(() => {
2764
2927
  onErrorRef.current = onError;
2765
2928
  }, [onError]);
2766
- const reportError = useCallback7((nextError) => {
2929
+ const reportError = useCallback8((nextError) => {
2767
2930
  setError(nextError);
2768
2931
  onErrorRef.current?.(nextError);
2769
2932
  }, []);
2770
- const loadBufferForRequest = useCallback7(
2771
- async (buffer, requestId) => {
2933
+ const clearError = useCallback8(() => setError(null), []);
2934
+ const loadBufferForRequest = useCallback8(
2935
+ async (buffer, token) => {
2772
2936
  setLoading(true);
2773
- setError(null);
2937
+ clearError();
2774
2938
  try {
2775
2939
  const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
2776
- if (requestId !== loadRequestIdRef.current) return;
2940
+ if (!request.isCurrent(token)) return;
2777
2941
  setEditor(next);
2778
2942
  setSelection({ chapter: 0, paragraph: 0 });
2779
2943
  setRevision((value) => value + 1);
2780
2944
  onLoadRef.current?.(next);
2781
2945
  } catch (err) {
2782
- if (requestId === loadRequestIdRef.current) reportError(toError(err));
2946
+ if (request.isCurrent(token)) reportError(toError(err));
2783
2947
  } finally {
2784
- if (requestId === loadRequestIdRef.current) setLoading(false);
2948
+ if (request.isCurrent(token)) setLoading(false);
2785
2949
  }
2786
2950
  },
2787
- [reportError]
2951
+ [clearError, reportError, request]
2788
2952
  );
2789
2953
  async function loadFile(file) {
2790
- const requestId = ++loadRequestIdRef.current;
2954
+ const token = request.begin();
2791
2955
  setLoading(true);
2792
- setError(null);
2956
+ clearError();
2793
2957
  try {
2794
- await loadBufferForRequest(await readEpubFile(file, limitsRef.current), requestId);
2958
+ await loadBufferForRequest(await readEpubFile(file, limitsRef.current), token);
2795
2959
  } catch (err) {
2796
- if (requestId === loadRequestIdRef.current) {
2960
+ if (request.isCurrent(token)) {
2797
2961
  reportError(toError(err));
2798
2962
  setLoading(false);
2799
2963
  }
2800
2964
  }
2801
2965
  }
2802
- useEffect9(() => {
2966
+ useEffect10(() => {
2803
2967
  if (!epubUrl) return;
2804
- let cancelled = false;
2805
- const requestId = ++loadRequestIdRef.current;
2968
+ const token = request.begin();
2806
2969
  void (async () => {
2807
2970
  setLoading(true);
2808
- setError(null);
2971
+ clearError();
2809
2972
  try {
2810
2973
  const buffer = await fetchEpubBuffer(epubUrl);
2811
- if (cancelled || requestId !== loadRequestIdRef.current) return;
2812
- await loadBufferForRequest(buffer, requestId);
2974
+ if (!request.isCurrent(token)) return;
2975
+ await loadBufferForRequest(buffer, token);
2813
2976
  } catch (err) {
2814
- if (!cancelled && requestId === loadRequestIdRef.current) reportError(toError(err));
2977
+ if (request.isCurrent(token)) reportError(toError(err));
2815
2978
  } finally {
2816
- if (!cancelled && requestId === loadRequestIdRef.current) setLoading(false);
2979
+ if (request.isCurrent(token)) setLoading(false);
2817
2980
  }
2818
2981
  })();
2819
2982
  return () => {
2820
- cancelled = true;
2983
+ if (!request.isCurrent(token)) return;
2984
+ request.invalidate();
2985
+ setLoading(false);
2821
2986
  };
2822
- }, [epubUrl, loadBufferForRequest, reportError]);
2823
- const syncedRef = useRef12(
2987
+ }, [epubUrl, clearError, loadBufferForRequest, reportError, request]);
2988
+ const syncedRef = useRef13(
2824
2989
  null
2825
2990
  );
2826
- useEffect9(() => {
2991
+ useEffect10(() => {
2827
2992
  const nextText = paragraph?.text ?? "";
2828
2993
  const target = `${selection.chapter}:${selection.paragraph}`;
2829
2994
  const prev = syncedRef.current;
@@ -2837,7 +3002,7 @@ function MejiroEditor({
2837
3002
  setRubyEnd(Math.min(1, [...nextText].length));
2838
3003
  setRubyText("");
2839
3004
  }, [editor, selection.chapter, selection.paragraph, paragraph]);
2840
- useEffect9(() => {
3005
+ useEffect10(() => {
2841
3006
  if (!previewBook) return;
2842
3007
  void readerRef.current?.goToAnchor({
2843
3008
  chapter: selection.chapter,
@@ -2885,6 +3050,7 @@ function MejiroEditor({
2885
3050
  afterBlockId
2886
3051
  });
2887
3052
  setRevision((value) => value + 1);
3053
+ clearError();
2888
3054
  }, reportError);
2889
3055
  }
2890
3056
  async function exportEpub() {
@@ -2898,10 +3064,20 @@ function MejiroEditor({
2898
3064
  const decision = await onBeforeExport?.(buffer);
2899
3065
  onExport?.(buffer);
2900
3066
  const allowDownload = exportPolicy ? exportPolicy.allowDownload !== false : decision !== false;
2901
- if (!allowDownload) return;
2902
- downloadEpub(buffer, `${editor.title || "edited"}.epub`);
3067
+ if (allowDownload) downloadEpub(buffer, `${editor.title || "edited"}.epub`);
3068
+ clearError();
2903
3069
  }, reportError);
2904
3070
  }
3071
+ const paragraphText = /* @__PURE__ */ jsx14(
3072
+ "textarea",
3073
+ {
3074
+ ref: textareaRef,
3075
+ value: text,
3076
+ readOnly: sceneBreak || !enableProofread,
3077
+ onChange: (event) => setText(event.target.value),
3078
+ onSelect: captureRubyRange
3079
+ }
3080
+ );
2905
3081
  return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
2906
3082
  /* @__PURE__ */ jsxs9("main", { className: "mejiro-editor-preview", children: [
2907
3083
  previewBook ? /* @__PURE__ */ jsx14(
@@ -2911,6 +3087,7 @@ function MejiroEditor({
2911
3087
  epub: previewBook,
2912
3088
  chapter: selection.chapter,
2913
3089
  onChapterChange: (index) => selectParagraph(index, 0),
3090
+ onError: reportError,
2914
3091
  fonts,
2915
3092
  subtitle: messages.editorPreviewSubtitle,
2916
3093
  chapterNavMode: "panel",
@@ -2948,16 +3125,7 @@ function MejiroEditor({
2948
3125
  ] }),
2949
3126
  enableProofread && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
2950
3127
  /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorProofread }),
2951
- /* @__PURE__ */ jsx14(
2952
- "textarea",
2953
- {
2954
- ref: textareaRef,
2955
- value: text,
2956
- readOnly: sceneBreak,
2957
- onChange: (event) => setText(event.target.value),
2958
- onSelect: captureRubyRange
2959
- }
2960
- ),
3128
+ paragraphText,
2961
3129
  /* @__PURE__ */ jsx14(
2962
3130
  "button",
2963
3131
  {
@@ -2972,6 +3140,7 @@ function MejiroEditor({
2972
3140
  enableRuby && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
2973
3141
  /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
2974
3142
  /* @__PURE__ */ jsx14("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
3143
+ !enableProofread && paragraphText,
2975
3144
  /* @__PURE__ */ jsx14("p", { className: "mejiro-editor-range", children: format(messages.editorRubyRange, {
2976
3145
  start: rubyRange?.start ?? 0,
2977
3146
  end: rubyRange?.end ?? 0,
@@ -3038,8 +3207,8 @@ function paragraphBlock(chapter, paragraphIndex) {
3038
3207
  }
3039
3208
 
3040
3209
  // src/MejiroManuscriptEditor.tsx
3041
- import { EpubProject } from "@libraz/mejiro/epub";
3042
- import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMemo8, useRef as useRef15, useState as useState12 } from "react";
3210
+ import { EpubProject as EpubProject2 } from "@libraz/mejiro/epub";
3211
+ import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef17, useState as useState13 } from "react";
3043
3212
 
3044
3213
  // src/MejiroNotationHighlighter.tsx
3045
3214
  import { tokenizeManuscriptSource } from "@libraz/mejiro";
@@ -3047,7 +3216,7 @@ import {
3047
3216
  forwardRef as forwardRef2,
3048
3217
  useImperativeHandle as useImperativeHandle2,
3049
3218
  useMemo as useMemo7,
3050
- useRef as useRef13,
3219
+ useRef as useRef14,
3051
3220
  useState as useState10
3052
3221
  } from "react";
3053
3222
  import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
@@ -3064,12 +3233,12 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
3064
3233
  onCompositionEnd,
3065
3234
  ...rest
3066
3235
  }, ref) {
3067
- const textareaRef = useRef13(null);
3068
- const overlayRef = useRef13(null);
3236
+ const textareaRef = useRef14(null);
3237
+ const overlayRef = useRef14(null);
3069
3238
  useImperativeHandle2(ref, () => textareaRef.current, []);
3070
3239
  const [isComposing, setIsComposing] = useState10(false);
3071
3240
  const [composingValue, setComposingValue] = useState10("");
3072
- const lastSegmentsRef = useRef13(null);
3241
+ const lastSegmentsRef = useRef14(null);
3073
3242
  const segments = useMemo7(() => {
3074
3243
  if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
3075
3244
  const next = buildSegments(value, dialect);
@@ -3162,83 +3331,349 @@ function buildSegments(text, dialect) {
3162
3331
  return segments;
3163
3332
  }
3164
3333
 
3165
- // src/useManuscriptDraft.ts
3166
- import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef14, useState as useState11 } from "react";
3167
- var DEFAULT_DELAY = 800;
3168
- function defaultChapter(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
3169
- const index = existing.length;
3170
- return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
3171
- }
3172
- function useManuscriptDraft(options = {}) {
3173
- const {
3174
- onAutosave,
3175
- autosaveDelay = DEFAULT_DELAY,
3176
- autosavePayload,
3177
- autosaveKey,
3178
- defaultChapterTitle,
3179
- defaultChapterBody
3180
- } = options;
3181
- const titleForRef = useRef14(defaultChapterTitle);
3182
- titleForRef.current = defaultChapterTitle;
3183
- const bodyForRef = useRef14(defaultChapterBody);
3184
- bodyForRef.current = defaultChapterBody;
3334
+ // src/useEpubProject.ts
3335
+ import {
3336
+ EpubProject,
3337
+ parseEpub as parseEpub2
3338
+ } from "@libraz/mejiro/epub";
3339
+ import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMemo8, useRef as useRef15, useState as useState11 } from "react";
3340
+ function useEpubProject(options = {}) {
3341
+ const defaultTitle = options.defaultChapterTitle;
3342
+ const defaultBody = options.defaultChapterBody;
3343
+ const [metadata, setMetadataState] = useState11(
3344
+ () => initialMetadata(options.metadata)
3345
+ );
3185
3346
  const [chapters, setChaptersState] = useState11(
3186
- () => options.initialChapters?.length ? options.initialChapters : [defaultChapter([], defaultChapterTitle, defaultChapterBody)]
3347
+ () => options.chapters?.length ? options.chapters : [defaultChapter([], defaultTitle, defaultBody)]
3187
3348
  );
3188
- const [selected, setSelectedState] = useState11(0);
3189
- const [autosaveError, setAutosaveError] = useState11(null);
3190
- const saveRef = useRef14(onAutosave);
3191
- saveRef.current = onAutosave;
3192
- const payloadRef = useRef14(autosavePayload);
3193
- payloadRef.current = autosavePayload;
3194
- const chaptersRef = useRef14(chapters);
3349
+ const [selectedChapter, setSelectedChapterState] = useState11(0);
3350
+ const [cover, setCoverState] = useState11(options.cover ?? null);
3351
+ const [assets, setAssetsState] = useState11(options.assets ?? []);
3352
+ const [previewBook, setPreviewBook] = useState11(null);
3353
+ const [previewError, setPreviewError] = useState11(null);
3354
+ const [previewing, setPreviewing] = useState11(false);
3355
+ const previewRequest = useRequestToken();
3356
+ const onPreviewRef = useRef15(options.onPreview);
3357
+ const onExportRef = useRef15(options.onExport);
3358
+ onPreviewRef.current = options.onPreview;
3359
+ onExportRef.current = options.onExport;
3360
+ const assetResolverRef = useRef15(options.assetResolver);
3361
+ assetResolverRef.current = options.assetResolver;
3362
+ const metadataRef = useRef15(metadata);
3363
+ metadataRef.current = metadata;
3364
+ const chaptersRef = useRef15(chapters);
3195
3365
  chaptersRef.current = chapters;
3196
- const selectedRef = useRef14(selected);
3197
- selectedRef.current = selected;
3198
- const trackerRef = useRef14(null);
3199
- if (!trackerRef.current)
3200
- trackerRef.current = createDraftChangeTracker(chapters, autosaveKey ?? "");
3201
- const dirtyRef = useRef14(false);
3202
- const mountedRef = useRef14(true);
3203
- const revisionRef = useRef14(0);
3204
- const inFlightRef = useRef14(false);
3205
- const flushQueuedRef = useRef14(false);
3206
- const flushAutosave = useCallback8(() => {
3207
- const callback = saveRef.current;
3208
- if (!(callback && dirtyRef.current)) return;
3209
- if (inFlightRef.current) {
3210
- flushQueuedRef.current = true;
3211
- return;
3212
- }
3213
- const revision = revisionRef.current;
3214
- const plain = snapshotChapters(chaptersRef.current);
3215
- const payload = payloadRef.current ? payloadRef.current(plain) : plain;
3216
- const markSaved = () => {
3217
- if (revisionRef.current === revision) dirtyRef.current = false;
3218
- };
3219
- let saved;
3220
- try {
3221
- saved = callback(payload);
3222
- } catch (err) {
3223
- saved = Promise.reject(err);
3224
- }
3225
- if (saved === void 0) {
3226
- markSaved();
3227
- return;
3228
- }
3229
- inFlightRef.current = true;
3230
- void Promise.resolve(saved).then(markSaved).catch((err) => {
3231
- if (!mountedRef.current) return;
3232
- setAutosaveError(toError(err));
3233
- }).finally(() => {
3234
- inFlightRef.current = false;
3235
- if (!flushQueuedRef.current) return;
3236
- flushQueuedRef.current = false;
3237
- flushAutosave();
3366
+ const selectedRef = useRef15(selectedChapter);
3367
+ selectedRef.current = selectedChapter;
3368
+ const coverRef = useRef15(cover);
3369
+ coverRef.current = cover;
3370
+ const assetsRef = useRef15(assets);
3371
+ assetsRef.current = assets;
3372
+ const buildProject = useCallback9(() => {
3373
+ const currentCover = coverRef.current;
3374
+ const project = EpubProject.fromManuscript({
3375
+ metadata: metadataRef.current,
3376
+ includeTitlePage: false,
3377
+ includeTitleInFirstChapter: true,
3378
+ chapters: chaptersRef.current.map((chapter) => ({
3379
+ id: chapter.id,
3380
+ title: chapter.title || "Untitled",
3381
+ body: chapter.body
3382
+ })),
3383
+ ...currentCover ? { cover: currentCover } : {}
3238
3384
  });
3385
+ for (const asset of assetsRef.current) project.addAsset(asset);
3386
+ return project;
3239
3387
  }, []);
3240
- useEffect10(() => {
3241
- if (trackerRef.current?.changed(chapters, autosaveKey ?? "")) {
3388
+ const exportOptions = useCallback9((signal) => {
3389
+ const resolver = assetResolverRef.current;
3390
+ return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
3391
+ }, []);
3392
+ useEffect11(() => {
3393
+ const token = previewRequest.begin();
3394
+ const controller = new AbortController();
3395
+ setPreviewing(true);
3396
+ const timer = setTimeout(() => {
3397
+ void (async () => {
3398
+ try {
3399
+ const book = await parseEpub2(
3400
+ await buildProject().export(exportOptions(controller.signal))
3401
+ );
3402
+ if (!previewRequest.isCurrent(token)) return;
3403
+ setPreviewBook(book);
3404
+ setPreviewError(null);
3405
+ onPreviewRef.current?.(book);
3406
+ } catch (err) {
3407
+ if (previewRequest.isCurrent(token)) setPreviewError(toError(err));
3408
+ } finally {
3409
+ if (previewRequest.isCurrent(token)) setPreviewing(false);
3410
+ }
3411
+ })();
3412
+ }, options.debounceMs ?? 250);
3413
+ return () => {
3414
+ previewRequest.invalidate();
3415
+ clearTimeout(timer);
3416
+ controller.abort();
3417
+ };
3418
+ }, [
3419
+ assets,
3420
+ buildProject,
3421
+ chapters,
3422
+ cover,
3423
+ exportOptions,
3424
+ metadata,
3425
+ options.debounceMs,
3426
+ previewRequest
3427
+ ]);
3428
+ const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
3429
+ const setMetadata = useCallback9((patch) => {
3430
+ const next = mergeMetadata(metadataRef.current, patch);
3431
+ metadataRef.current = next;
3432
+ setMetadataState(next);
3433
+ }, []);
3434
+ const commit = useCallback9((nextChapters, nextSelected) => {
3435
+ chaptersRef.current = nextChapters;
3436
+ selectedRef.current = nextSelected;
3437
+ setChaptersState(nextChapters);
3438
+ setSelectedChapterState(nextSelected);
3439
+ }, []);
3440
+ const setChapters = useCallback9(
3441
+ (next) => {
3442
+ const normalized = next.length ? next : [defaultChapter([], defaultTitle, defaultBody)];
3443
+ const prev = selectedRef.current;
3444
+ const selectedId = chaptersRef.current[prev]?.id;
3445
+ const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
3446
+ commit(
3447
+ normalized,
3448
+ nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
3449
+ );
3450
+ },
3451
+ [commit, defaultBody, defaultTitle]
3452
+ );
3453
+ const setCover = useCallback9((asset) => {
3454
+ coverRef.current = asset;
3455
+ setCoverState(asset);
3456
+ }, []);
3457
+ const setAssets = useCallback9((next) => {
3458
+ assetsRef.current = next;
3459
+ setAssetsState(next);
3460
+ }, []);
3461
+ const setSelectedChapter = useCallback9((index) => {
3462
+ const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
3463
+ selectedRef.current = next;
3464
+ setSelectedChapterState(next);
3465
+ }, []);
3466
+ const patchChapter = useCallback9((index, patch) => {
3467
+ const next = chaptersRef.current.map(
3468
+ (chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
3469
+ );
3470
+ chaptersRef.current = next;
3471
+ setChaptersState(next);
3472
+ }, []);
3473
+ const addChapter = useCallback9(
3474
+ (chapter = {}) => {
3475
+ const current = chaptersRef.current;
3476
+ const generated = defaultChapter(current, defaultTitle, defaultBody);
3477
+ const next = [
3478
+ ...current,
3479
+ {
3480
+ id: chapter.id ?? generated.id,
3481
+ title: chapter.title ?? generated.title,
3482
+ body: chapter.body ?? generated.body
3483
+ }
3484
+ ];
3485
+ commit(next, next.length - 1);
3486
+ },
3487
+ [commit, defaultBody, defaultTitle]
3488
+ );
3489
+ const removeChapter = useCallback9(
3490
+ (index = selectedRef.current) => {
3491
+ const current = chaptersRef.current;
3492
+ if (current.length <= 1) return;
3493
+ const next = current.filter((_, chapterIndex) => chapterIndex !== index);
3494
+ const prev = selectedRef.current;
3495
+ let nextSelected;
3496
+ if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
3497
+ else if (index < prev) nextSelected = prev - 1;
3498
+ else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
3499
+ commit(next, nextSelected);
3500
+ },
3501
+ [commit]
3502
+ );
3503
+ const reorderChapters = useCallback9(
3504
+ (from, to) => {
3505
+ const current = chaptersRef.current;
3506
+ if (from < 0 || from >= current.length) return;
3507
+ const next = [...current];
3508
+ const [moved] = next.splice(from, 1);
3509
+ const target = Math.max(0, Math.min(next.length, to));
3510
+ next.splice(target, 0, moved);
3511
+ const prev = selectedRef.current;
3512
+ let nextSelected = prev;
3513
+ if (prev === from) nextSelected = target;
3514
+ else if (from < prev && target >= prev) nextSelected = prev - 1;
3515
+ else if (from > prev && target <= prev) nextSelected = prev + 1;
3516
+ commit(next, nextSelected);
3517
+ },
3518
+ [commit]
3519
+ );
3520
+ const exportEpub = useCallback9(async () => {
3521
+ const buffer = await buildProject().export(exportOptions());
3522
+ onExportRef.current?.(buffer);
3523
+ return buffer;
3524
+ }, [buildProject, exportOptions]);
3525
+ return useMemo8(
3526
+ () => ({
3527
+ metadata,
3528
+ chapters,
3529
+ selectedChapter,
3530
+ currentChapter,
3531
+ cover,
3532
+ assets,
3533
+ previewBook,
3534
+ previewError,
3535
+ previewing,
3536
+ setMetadata,
3537
+ setChapters,
3538
+ setSelectedChapter,
3539
+ setCover,
3540
+ setAssets,
3541
+ patchChapter,
3542
+ addChapter,
3543
+ removeChapter,
3544
+ reorderChapters,
3545
+ buildProject,
3546
+ exportEpub
3547
+ }),
3548
+ [
3549
+ addChapter,
3550
+ assets,
3551
+ buildProject,
3552
+ chapters,
3553
+ cover,
3554
+ currentChapter,
3555
+ exportEpub,
3556
+ metadata,
3557
+ patchChapter,
3558
+ previewBook,
3559
+ previewError,
3560
+ previewing,
3561
+ reorderChapters,
3562
+ removeChapter,
3563
+ selectedChapter,
3564
+ setAssets,
3565
+ setChapters,
3566
+ setCover,
3567
+ setMetadata,
3568
+ setSelectedChapter
3569
+ ]
3570
+ );
3571
+ }
3572
+ function mergeMetadata(current, patch) {
3573
+ const next = { ...current, ...patch };
3574
+ if (!next.identifier?.trim()) next.identifier = current.identifier;
3575
+ return next;
3576
+ }
3577
+ function createPackageIdentifier() {
3578
+ if (typeof globalThis.crypto?.randomUUID === "function") {
3579
+ return `urn:uuid:${globalThis.crypto.randomUUID()}`;
3580
+ }
3581
+ const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
3582
+ const digit = Math.floor(Math.random() * 16);
3583
+ return (ch === "x" ? digit : digit & 3 | 8).toString(16);
3584
+ });
3585
+ return `urn:uuid:${uuid}`;
3586
+ }
3587
+ function initialMetadata(patch = {}) {
3588
+ const metadata = mergeDefined(
3589
+ { title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
3590
+ patch
3591
+ );
3592
+ if (!metadata.identifier?.trim()) metadata.identifier = createPackageIdentifier();
3593
+ return metadata;
3594
+ }
3595
+ function defaultChapter(existing, titleFor = (i) => i === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${i + 1}\u8A71`, bodyFor = (i) => i === 0 ? "\u3053\u308C\u306F\uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B\u306E\u30EB\u30D3\u4F8B\u3067\u3059\u3002\n\n\u672C\u6587\u3092\u3053\u3053\u306B\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002" : "") {
3596
+ const index = existing.length;
3597
+ return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
3598
+ }
3599
+
3600
+ // src/useManuscriptDraft.ts
3601
+ import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef16, useState as useState12 } from "react";
3602
+ var DEFAULT_DELAY = 800;
3603
+ function defaultChapter2(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
3604
+ const index = existing.length;
3605
+ return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
3606
+ }
3607
+ function useManuscriptDraft(options = {}) {
3608
+ const {
3609
+ onAutosave,
3610
+ autosaveDelay = DEFAULT_DELAY,
3611
+ autosavePayload,
3612
+ autosaveKey,
3613
+ defaultChapterTitle,
3614
+ defaultChapterBody
3615
+ } = options;
3616
+ const titleForRef = useRef16(defaultChapterTitle);
3617
+ titleForRef.current = defaultChapterTitle;
3618
+ const bodyForRef = useRef16(defaultChapterBody);
3619
+ bodyForRef.current = defaultChapterBody;
3620
+ const [chapters, setChaptersState] = useState12(
3621
+ () => options.initialChapters?.length ? options.initialChapters : [defaultChapter2([], defaultChapterTitle, defaultChapterBody)]
3622
+ );
3623
+ const [selected, setSelectedState] = useState12(0);
3624
+ const [autosaveError, setAutosaveError] = useState12(null);
3625
+ const saveRef = useRef16(onAutosave);
3626
+ saveRef.current = onAutosave;
3627
+ const payloadRef = useRef16(autosavePayload);
3628
+ payloadRef.current = autosavePayload;
3629
+ const chaptersRef = useRef16(chapters);
3630
+ chaptersRef.current = chapters;
3631
+ const selectedRef = useRef16(selected);
3632
+ selectedRef.current = selected;
3633
+ const trackerRef = useRef16(null);
3634
+ if (!trackerRef.current)
3635
+ trackerRef.current = createDraftChangeTracker(chapters, autosaveKey ?? "");
3636
+ const dirtyRef = useRef16(false);
3637
+ const mountedRef = useRef16(true);
3638
+ const revisionRef = useRef16(0);
3639
+ const inFlightRef = useRef16(false);
3640
+ const flushQueuedRef = useRef16(false);
3641
+ const flushAutosave = useCallback10(() => {
3642
+ const callback = saveRef.current;
3643
+ if (!(callback && dirtyRef.current)) return;
3644
+ if (inFlightRef.current) {
3645
+ flushQueuedRef.current = true;
3646
+ return;
3647
+ }
3648
+ const revision = revisionRef.current;
3649
+ const plain = snapshotChapters(chaptersRef.current);
3650
+ const payload = payloadRef.current ? payloadRef.current(plain) : plain;
3651
+ const markSaved = () => {
3652
+ if (revisionRef.current === revision) dirtyRef.current = false;
3653
+ };
3654
+ let saved;
3655
+ try {
3656
+ saved = callback(payload);
3657
+ } catch (err) {
3658
+ saved = Promise.reject(err);
3659
+ }
3660
+ if (saved === void 0) {
3661
+ markSaved();
3662
+ return;
3663
+ }
3664
+ inFlightRef.current = true;
3665
+ void Promise.resolve(saved).then(markSaved).catch((err) => {
3666
+ if (!mountedRef.current) return;
3667
+ setAutosaveError(toError(err));
3668
+ }).finally(() => {
3669
+ inFlightRef.current = false;
3670
+ if (!flushQueuedRef.current) return;
3671
+ flushQueuedRef.current = false;
3672
+ flushAutosave();
3673
+ });
3674
+ }, []);
3675
+ useEffect12(() => {
3676
+ if (trackerRef.current?.changed(chapters, autosaveKey ?? "")) {
3242
3677
  dirtyRef.current = true;
3243
3678
  revisionRef.current += 1;
3244
3679
  setAutosaveError(null);
@@ -3249,7 +3684,7 @@ function useManuscriptDraft(options = {}) {
3249
3684
  }, autosaveDelay);
3250
3685
  return () => clearTimeout(timer);
3251
3686
  }, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
3252
- useEffect10(() => {
3687
+ useEffect12(() => {
3253
3688
  mountedRef.current = true;
3254
3689
  const detachPageHide = flushOnPageHide(flushAutosave);
3255
3690
  return () => {
@@ -3258,20 +3693,20 @@ function useManuscriptDraft(options = {}) {
3258
3693
  detachPageHide();
3259
3694
  };
3260
3695
  }, [flushAutosave]);
3261
- const commit = useCallback8((nextChapters, nextSelected) => {
3696
+ const commit = useCallback10((nextChapters, nextSelected) => {
3262
3697
  chaptersRef.current = nextChapters;
3263
3698
  selectedRef.current = nextSelected;
3264
3699
  setChaptersState(nextChapters);
3265
3700
  setSelectedState(nextSelected);
3266
3701
  }, []);
3267
- const setSelected = useCallback8((index) => {
3702
+ const setSelected = useCallback10((index) => {
3268
3703
  const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
3269
3704
  selectedRef.current = next;
3270
3705
  setSelectedState(next);
3271
3706
  }, []);
3272
- const setChapters = useCallback8(
3707
+ const setChapters = useCallback10(
3273
3708
  (next) => {
3274
- const normalized = next.length ? next : [defaultChapter([], titleForRef.current, bodyForRef.current)];
3709
+ const normalized = next.length ? next : [defaultChapter2([], titleForRef.current, bodyForRef.current)];
3275
3710
  const prev = selectedRef.current;
3276
3711
  const selectedId = chaptersRef.current[prev]?.id;
3277
3712
  const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
@@ -3282,17 +3717,17 @@ function useManuscriptDraft(options = {}) {
3282
3717
  },
3283
3718
  [commit]
3284
3719
  );
3285
- const patchChapter = useCallback8((index, patch) => {
3720
+ const patchChapter = useCallback10((index, patch) => {
3286
3721
  const next = chaptersRef.current.map(
3287
3722
  (chapter, i) => i === index ? { ...chapter, ...patch } : chapter
3288
3723
  );
3289
3724
  chaptersRef.current = next;
3290
3725
  setChaptersState(next);
3291
3726
  }, []);
3292
- const addChapter = useCallback8(
3727
+ const addChapter = useCallback10(
3293
3728
  (chapter = {}) => {
3294
3729
  const current = chaptersRef.current;
3295
- const generated = defaultChapter(current, titleForRef.current, bodyForRef.current);
3730
+ const generated = defaultChapter2(current, titleForRef.current, bodyForRef.current);
3296
3731
  const next = [
3297
3732
  ...current,
3298
3733
  {
@@ -3305,7 +3740,7 @@ function useManuscriptDraft(options = {}) {
3305
3740
  },
3306
3741
  [commit]
3307
3742
  );
3308
- const removeChapter = useCallback8(
3743
+ const removeChapter = useCallback10(
3309
3744
  (index) => {
3310
3745
  const current = chaptersRef.current;
3311
3746
  if (current.length <= 1) return;
@@ -3319,7 +3754,7 @@ function useManuscriptDraft(options = {}) {
3319
3754
  },
3320
3755
  [commit]
3321
3756
  );
3322
- const reorderChapters = useCallback8(
3757
+ const reorderChapters = useCallback10(
3323
3758
  (from, to) => {
3324
3759
  const current = chaptersRef.current;
3325
3760
  if (from < 0 || from >= current.length) return;
@@ -3372,7 +3807,7 @@ function coverAssetHref(file) {
3372
3807
  const filename = file.name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3373
3808
  return `OPS/Images/${filename && !/^\.+$/u.test(filename) ? filename : `cover${coverExtension(file.type)}`}`;
3374
3809
  }
3375
- function defaultChapter2(messages) {
3810
+ function defaultChapter3(messages) {
3376
3811
  return {
3377
3812
  id: `chapter-${Date.now()}-0`,
3378
3813
  title: format(messages.manuscriptDefaultChapterTitle, { n: 1 }),
@@ -3398,41 +3833,41 @@ function MejiroManuscriptEditor({
3398
3833
  panelSide = "right"
3399
3834
  }) {
3400
3835
  const messages = useI18n();
3401
- const coverInputRef = useRef15(null);
3402
- const bodyTextareaRef = useRef15(null);
3836
+ const coverInputRef = useRef17(null);
3837
+ const bodyTextareaRef = useRef17(null);
3403
3838
  const titleControlled = onTitleChange !== void 0;
3404
3839
  const authorControlled = onAuthorChange !== void 0;
3405
3840
  const coverControlled = onCoverChange !== void 0;
3406
- const [titleState, setTitleState] = useState12(titleProp ?? messages.manuscriptDefaultTitle);
3407
- const [authorState, setAuthorState] = useState12(authorProp ?? "");
3408
- const [coverState, setCoverState] = useState12(coverProp ?? null);
3841
+ const [titleState, setTitleState] = useState13(titleProp ?? messages.manuscriptDefaultTitle);
3842
+ const [authorState, setAuthorState] = useState13(authorProp ?? "");
3843
+ const [coverState, setCoverState] = useState13(coverProp ?? null);
3409
3844
  const title = titleControlled ? titleProp ?? "" : titleState;
3410
3845
  const author = authorControlled ? authorProp ?? "" : authorState;
3411
3846
  const cover = coverControlled ? coverProp ?? null : coverState;
3412
- useEffect11(() => {
3847
+ useEffect13(() => {
3413
3848
  if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
3414
3849
  }, [titleControlled, titleProp]);
3415
- useEffect11(() => {
3850
+ useEffect13(() => {
3416
3851
  if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
3417
3852
  }, [authorControlled, authorProp]);
3418
- useEffect11(() => {
3853
+ useEffect13(() => {
3419
3854
  if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
3420
3855
  }, [coverControlled, coverProp]);
3421
- const setTitle = useCallback9(
3856
+ const setTitle = useCallback11(
3422
3857
  (next) => {
3423
3858
  if (!titleControlled) setTitleState(next);
3424
3859
  onTitleChange?.(next);
3425
3860
  },
3426
3861
  [titleControlled, onTitleChange]
3427
3862
  );
3428
- const setAuthor = useCallback9(
3863
+ const setAuthor = useCallback11(
3429
3864
  (next) => {
3430
3865
  if (!authorControlled) setAuthorState(next);
3431
3866
  onAuthorChange?.(next);
3432
3867
  },
3433
3868
  [authorControlled, onAuthorChange]
3434
3869
  );
3435
- const setCover = useCallback9(
3870
+ const setCover = useCallback11(
3436
3871
  (next) => {
3437
3872
  if (!coverControlled) setCoverState(next);
3438
3873
  onCoverChange?.(next);
@@ -3440,7 +3875,7 @@ function MejiroManuscriptEditor({
3440
3875
  [coverControlled, onCoverChange]
3441
3876
  );
3442
3877
  const draft = useManuscriptDraft({
3443
- initialChapters: initialChapters?.length ? initialChapters : [defaultChapter2(messages)],
3878
+ initialChapters: initialChapters?.length ? initialChapters : [defaultChapter3(messages)],
3444
3879
  onAutosave,
3445
3880
  autosavePayload: (savedChapters) => ({ title, author, cover, chapters: savedChapters }),
3446
3881
  autosaveKey: `${title}\0${author}\0${cover?.name ?? ""}\0${cover?.size ?? 0}\0${cover?.lastModified ?? 0}`,
@@ -3457,8 +3892,16 @@ function MejiroManuscriptEditor({
3457
3892
  reorderChapters
3458
3893
  } = draft;
3459
3894
  const current = chapters[selected] ?? chapters[0];
3460
- const [draggingIndex, setDraggingIndex] = useState12(null);
3461
- const [exportError, setExportError] = useState12(null);
3895
+ const [draggingIndex, setDraggingIndex] = useState13(null);
3896
+ const [exportError, setExportError] = useState13(null);
3897
+ const reportError = useCallback11(
3898
+ (error) => {
3899
+ setExportError(error);
3900
+ onError?.(error);
3901
+ },
3902
+ [onError]
3903
+ );
3904
+ const clearError = useCallback11(() => setExportError(null), []);
3462
3905
  function wrapSelection(open, close) {
3463
3906
  const el = bodyTextareaRef.current;
3464
3907
  if (!el) return;
@@ -3476,36 +3919,30 @@ function MejiroManuscriptEditor({
3476
3919
  bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
3477
3920
  });
3478
3921
  }
3479
- const [identifier] = useState12(() => `urn:uuid:${crypto.randomUUID()}`);
3480
- const exportEpub = useCallback9(async () => {
3481
- await withErrorReporting(
3482
- async () => {
3483
- const project = EpubProject.fromManuscript({
3484
- metadata: { title, author: author || void 0, identifier },
3485
- dialect,
3486
- chapters: chapters.map((chapter) => ({
3487
- id: chapter.id,
3488
- title: chapter.title || messages.untitled,
3489
- body: chapter.body
3490
- }))
3922
+ const [identifier] = useState13(createPackageIdentifier);
3923
+ const exportEpub = useCallback11(async () => {
3924
+ await withErrorReporting(async () => {
3925
+ const project = EpubProject2.fromManuscript({
3926
+ metadata: { title, author: author || void 0, identifier },
3927
+ dialect,
3928
+ chapters: chapters.map((chapter) => ({
3929
+ id: chapter.id,
3930
+ title: chapter.title || messages.untitled,
3931
+ body: chapter.body
3932
+ }))
3933
+ });
3934
+ if (cover) {
3935
+ project.setCover({
3936
+ href: coverAssetHref(cover),
3937
+ mediaType: cover.type || void 0,
3938
+ data: await cover.arrayBuffer()
3491
3939
  });
3492
- if (cover) {
3493
- project.setCover({
3494
- href: coverAssetHref(cover),
3495
- mediaType: cover.type || void 0,
3496
- data: await cover.arrayBuffer()
3497
- });
3498
- }
3499
- const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
3500
- setExportError(null);
3501
- onExport?.(buffer);
3502
- downloadEpub(buffer, `${title || "book"}.epub`);
3503
- },
3504
- (error) => {
3505
- setExportError(error);
3506
- onError?.(error);
3507
3940
  }
3508
- );
3941
+ const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
3942
+ clearError();
3943
+ onExport?.(buffer);
3944
+ downloadEpub(buffer, `${title || "book"}.epub`);
3945
+ }, reportError);
3509
3946
  }, [
3510
3947
  assetResolver,
3511
3948
  author,
@@ -3514,11 +3951,12 @@ function MejiroManuscriptEditor({
3514
3951
  dialect,
3515
3952
  identifier,
3516
3953
  messages.untitled,
3517
- onError,
3518
3954
  onExport,
3955
+ clearError,
3956
+ reportError,
3519
3957
  title
3520
3958
  ]);
3521
- const previewManuscript = useMemo8(
3959
+ const previewManuscript = useMemo9(
3522
3960
  () => chapters.map((chapter) => ({
3523
3961
  id: chapter.id,
3524
3962
  title: chapter.title || messages.untitled,
@@ -3543,6 +3981,7 @@ function MejiroManuscriptEditor({
3543
3981
  fonts,
3544
3982
  chapter: selected,
3545
3983
  onChapterChange: setSelected,
3984
+ onError: reportError,
3546
3985
  enableImageOverlay: false
3547
3986
  }
3548
3987
  ) }),
@@ -3566,7 +4005,11 @@ function MejiroManuscriptEditor({
3566
4005
  type: "file",
3567
4006
  accept: "image/*",
3568
4007
  hidden: true,
3569
- onChange: (event) => setCover(event.target.files?.[0] ?? null)
4008
+ onChange: (event) => {
4009
+ const file = event.target.files?.[0] ?? null;
4010
+ event.target.value = "";
4011
+ setCover(file);
4012
+ }
3570
4013
  }
3571
4014
  )
3572
4015
  ] }),
@@ -3585,6 +4028,7 @@ function MejiroManuscriptEditor({
3585
4028
  onDragStart: (event) => {
3586
4029
  setDraggingIndex(index);
3587
4030
  event.dataTransfer.effectAllowed = "move";
4031
+ event.dataTransfer.setData("text/plain", chapter.id);
3588
4032
  },
3589
4033
  onDragEnd: () => setDraggingIndex(null),
3590
4034
  onDragOver: (event) => {
@@ -3692,7 +4136,7 @@ function MejiroShelf({
3692
4136
  }
3693
4137
 
3694
4138
  // src/MejiroToc.tsx
3695
- import { useMemo as useMemo9, useState as useState13 } from "react";
4139
+ import { useMemo as useMemo10, useState as useState14 } from "react";
3696
4140
  import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3697
4141
  function buildEntries(epub, messages) {
3698
4142
  return epub.chapters.map((ch, i) => {
@@ -3715,9 +4159,9 @@ function MejiroToc({
3715
4159
  const messages = useI18n();
3716
4160
  const resolvedTitle = title ?? messages.tocTitle;
3717
4161
  const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
3718
- const [query, setQuery] = useState13("");
3719
- const entries = useMemo9(() => buildEntries(epub, messages), [epub, messages]);
3720
- const filtered = useMemo9(() => {
4162
+ const [query, setQuery] = useState14("");
4163
+ const entries = useMemo10(() => buildEntries(epub, messages), [epub, messages]);
4164
+ const filtered = useMemo10(() => {
3721
4165
  if (!query) return entries;
3722
4166
  const needle = query.toLowerCase();
3723
4167
  return entries.filter(
@@ -3767,34 +4211,41 @@ import {
3767
4211
  serializeAnnotations,
3768
4212
  sortAnnotations
3769
4213
  } from "@libraz/mejiro";
3770
- import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef16, useState as useState14 } from "react";
4214
+ import { useCallback as useCallback12, useEffect as useEffect14, useRef as useRef18, useState as useState15 } from "react";
3771
4215
  function parseSorted(raw) {
3772
4216
  return sortAnnotations(parseAnnotations(raw));
3773
4217
  }
3774
4218
  function useAnnotations(options) {
3775
4219
  const { key, throttleMs = 250 } = options;
3776
4220
  const storage = options.storage ?? resolveDefaultStorage();
3777
- const [annotations, setAnnotations] = useState14(
3778
- () => readStorage(storage, key, parseSorted, [])
3779
- );
3780
- const storageRef = useRef16(storage);
4221
+ const storageRef = useRef18(storage);
3781
4222
  storageRef.current = storage;
3782
- const annotationsRef = useRef16(annotations);
3783
- const applyNext = useCallback10((next) => {
3784
- annotationsRef.current = next;
3785
- setAnnotations(next);
3786
- }, []);
3787
- const pendingRef = useRef16(null);
4223
+ const [hydrated, setHydrated] = useState15(() => ({
4224
+ key,
4225
+ list: readStorage(storage, key, parseSorted, [])
4226
+ }));
4227
+ let annotations = hydrated.list;
4228
+ const annotationsRef = useRef18(annotations);
4229
+ if (hydrated.key !== key) {
4230
+ annotations = readStorage(storage, key, parseSorted, []);
4231
+ annotationsRef.current = annotations;
4232
+ setHydrated({ key, list: annotations });
4233
+ }
4234
+ const applyNext = useCallback12(
4235
+ (next) => {
4236
+ annotationsRef.current = next;
4237
+ setHydrated({ key, list: next });
4238
+ },
4239
+ [key]
4240
+ );
4241
+ const pendingRef = useRef18(null);
3788
4242
  if (!pendingRef.current) pendingRef.current = createPendingWrite();
3789
4243
  const pending = pendingRef.current;
3790
- useEffect12(() => flushOnPageHide(pending.flush), [pending]);
3791
- useEffect12(() => {
3792
- applyNext(readStorage(storageRef.current, key, parseSorted, []));
3793
- return pending.flush;
3794
- }, [key, applyNext, pending]);
3795
- const onChangeRef = useRef16(options.onChange);
4244
+ useEffect14(() => flushOnPageHide(pending.flush), [pending]);
4245
+ useEffect14(() => pending.flush, [key, pending]);
4246
+ const onChangeRef = useRef18(options.onChange);
3796
4247
  onChangeRef.current = options.onChange;
3797
- const commit = useCallback10(
4248
+ const commit = useCallback12(
3798
4249
  (next) => {
3799
4250
  const currentStorage = storageRef.current;
3800
4251
  if (currentStorage) {
@@ -3806,7 +4257,7 @@ function useAnnotations(options) {
3806
4257
  },
3807
4258
  [key, throttleMs, pending]
3808
4259
  );
3809
- const add = useCallback10(
4260
+ const add = useCallback12(
3810
4261
  (input) => {
3811
4262
  const annotation = {
3812
4263
  ...input,
@@ -3820,7 +4271,7 @@ function useAnnotations(options) {
3820
4271
  },
3821
4272
  [commit, applyNext]
3822
4273
  );
3823
- const remove = useCallback10(
4274
+ const remove = useCallback12(
3824
4275
  (id) => {
3825
4276
  const current = annotationsRef.current;
3826
4277
  const next = current.filter((annotation) => annotation.id !== id);
@@ -3830,7 +4281,7 @@ function useAnnotations(options) {
3830
4281
  },
3831
4282
  [commit, applyNext]
3832
4283
  );
3833
- const update = useCallback10(
4284
+ const update = useCallback12(
3834
4285
  (id, patch) => {
3835
4286
  let changed = false;
3836
4287
  const next = annotationsRef.current.map((annotation) => {
@@ -3845,7 +4296,8 @@ function useAnnotations(options) {
3845
4296
  },
3846
4297
  [commit, applyNext]
3847
4298
  );
3848
- const clear = useCallback10(() => {
4299
+ const clear = useCallback12(() => {
4300
+ if (annotationsRef.current.length === 0) return;
3849
4301
  applyNext([]);
3850
4302
  pending.cancel();
3851
4303
  removeStorage(storageRef.current, key);
@@ -3860,39 +4312,40 @@ import {
3860
4312
  cloneEditableEpubBook as cloneEditableEpubBook2,
3861
4313
  EditableEpub as EditableEpub2
3862
4314
  } from "@libraz/mejiro/epub";
3863
- import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo10, useRef as useRef17, useState as useState15 } from "react";
4315
+ import { useCallback as useCallback13, useEffect as useEffect15, useMemo as useMemo11, useRef as useRef19, useState as useState16 } from "react";
3864
4316
  function useEditableEpub(options = {}) {
3865
- const [editor, setEditor] = useState15(null);
3866
- const [loading, setLoading] = useState15(false);
3867
- const [exporting, setExporting] = useState15(false);
3868
- const [error, setError] = useState15(null);
3869
- const [revision, setRevision] = useState15(0);
3870
- const [selection, setSelectionState] = useState15({
4317
+ const [editor, setEditor] = useState16(null);
4318
+ const [loading, setLoading] = useState16(false);
4319
+ const [exporting, setExporting] = useState16(false);
4320
+ const [error, setError] = useState16(null);
4321
+ const [revision, setRevision] = useState16(0);
4322
+ const [selection, setSelectionState] = useState16({
3871
4323
  chapter: 0,
3872
4324
  paragraph: 0
3873
4325
  });
3874
- const requestIdRef = useRef17(0);
3875
- const onLoadRef = useRef17(options.onLoad);
3876
- const onErrorRef = useRef17(options.onError);
3877
- const onExportRef = useRef17(options.onExport);
3878
- const limitsRef = useRef17(options.limits);
3879
- const assetResolverRef = useRef17(options.assetResolver);
4326
+ const request = useRequestToken();
4327
+ const exportsInFlightRef = useRef19(0);
4328
+ const onLoadRef = useRef19(options.onLoad);
4329
+ const onErrorRef = useRef19(options.onError);
4330
+ const onExportRef = useRef19(options.onExport);
4331
+ const limitsRef = useRef19(options.limits);
4332
+ const assetResolverRef = useRef19(options.assetResolver);
3880
4333
  onLoadRef.current = options.onLoad;
3881
4334
  onErrorRef.current = options.onError;
3882
4335
  onExportRef.current = options.onExport;
3883
4336
  limitsRef.current = options.limits;
3884
4337
  assetResolverRef.current = options.assetResolver;
3885
- const editorRef = useRef17(editor);
4338
+ const editorRef = useRef19(editor);
3886
4339
  editorRef.current = editor;
3887
- const selectionRef = useRef17(selection);
4340
+ const selectionRef = useRef19(selection);
3888
4341
  selectionRef.current = selection;
3889
- const loadBufferWithRequest = useCallback11(
3890
- async (buffer, requestId) => {
4342
+ const loadBufferWithRequest = useCallback13(
4343
+ async (buffer, currentRequest) => {
3891
4344
  setLoading(true);
3892
4345
  setError(null);
3893
4346
  try {
3894
4347
  const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
3895
- if (requestId !== requestIdRef.current) return null;
4348
+ if (!request.isCurrent(currentRequest)) return null;
3896
4349
  const firstParagraph = { chapter: 0, paragraph: 0 };
3897
4350
  editorRef.current = next;
3898
4351
  selectionRef.current = firstParagraph;
@@ -3902,34 +4355,37 @@ function useEditableEpub(options = {}) {
3902
4355
  onLoadRef.current?.(next);
3903
4356
  return next;
3904
4357
  } catch (err) {
3905
- if (requestId === requestIdRef.current) {
4358
+ if (request.isCurrent(currentRequest)) {
3906
4359
  const nextError = toError(err);
3907
4360
  setError(nextError);
3908
4361
  onErrorRef.current?.(nextError);
3909
4362
  }
3910
4363
  return null;
3911
4364
  } finally {
3912
- if (requestId === requestIdRef.current) setLoading(false);
4365
+ if (request.isCurrent(currentRequest)) setLoading(false);
3913
4366
  }
3914
4367
  },
3915
- []
4368
+ [request]
3916
4369
  );
3917
- const loadBuffer = useCallback11(
4370
+ const loadBuffer = useCallback13(
3918
4371
  async (buffer) => {
3919
- const requestId = ++requestIdRef.current;
3920
- return loadBufferWithRequest(buffer, requestId);
4372
+ const currentRequest = request.begin();
4373
+ return loadBufferWithRequest(buffer, currentRequest);
3921
4374
  },
3922
- [loadBufferWithRequest]
4375
+ [loadBufferWithRequest, request]
3923
4376
  );
3924
- const loadFile = useCallback11(
4377
+ const loadFile = useCallback13(
3925
4378
  async (file) => {
3926
- const requestId = ++requestIdRef.current;
4379
+ const currentRequest = request.begin();
3927
4380
  setLoading(true);
3928
4381
  setError(null);
3929
4382
  try {
3930
- return await loadBufferWithRequest(await readEpubFile(file, limitsRef.current), requestId);
4383
+ return await loadBufferWithRequest(
4384
+ await readEpubFile(file, limitsRef.current),
4385
+ currentRequest
4386
+ );
3931
4387
  } catch (err) {
3932
- if (requestId === requestIdRef.current) {
4388
+ if (request.isCurrent(currentRequest)) {
3933
4389
  const nextError = toError(err);
3934
4390
  setError(nextError);
3935
4391
  onErrorRef.current?.(nextError);
@@ -3938,17 +4394,17 @@ function useEditableEpub(options = {}) {
3938
4394
  return null;
3939
4395
  }
3940
4396
  },
3941
- [loadBufferWithRequest]
4397
+ [loadBufferWithRequest, request]
3942
4398
  );
3943
- const loadUrl = useCallback11(
4399
+ const loadUrl = useCallback13(
3944
4400
  async (url) => {
3945
- const requestId = ++requestIdRef.current;
4401
+ const currentRequest = request.begin();
3946
4402
  setLoading(true);
3947
4403
  setError(null);
3948
4404
  try {
3949
- return await loadBufferWithRequest(await fetchEpubBuffer(url), requestId);
4405
+ return await loadBufferWithRequest(await fetchEpubBuffer(url), currentRequest);
3950
4406
  } catch (err) {
3951
- if (requestId === requestIdRef.current) {
4407
+ if (request.isCurrent(currentRequest)) {
3952
4408
  const nextError = toError(err);
3953
4409
  setError(nextError);
3954
4410
  onErrorRef.current?.(nextError);
@@ -3957,24 +4413,32 @@ function useEditableEpub(options = {}) {
3957
4413
  return null;
3958
4414
  }
3959
4415
  },
3960
- [loadBufferWithRequest]
4416
+ [loadBufferWithRequest, request]
3961
4417
  );
3962
- useEffect13(() => {
3963
- if (options.defaultUrl) void loadUrl(options.defaultUrl);
3964
- }, [options.defaultUrl, loadUrl]);
4418
+ const defaultUrl = options.defaultUrl;
4419
+ useEffect15(() => {
4420
+ if (!defaultUrl) return;
4421
+ void loadUrl(defaultUrl);
4422
+ const urlRequest = request.current();
4423
+ return () => {
4424
+ if (!request.isCurrent(urlRequest)) return;
4425
+ request.invalidate();
4426
+ setLoading(false);
4427
+ };
4428
+ }, [defaultUrl, loadUrl, request]);
3965
4429
  const book = editor?.book ?? null;
3966
4430
  const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
3967
- const previewBook = useMemo10(() => {
4431
+ const previewBook = useMemo11(() => {
3968
4432
  void revision;
3969
- return book ? cloneEditableEpubBook2(book) : null;
3970
- }, [book, revision]);
4433
+ return book && editor ? markBookIdentity(cloneEditableEpubBook2(book), editor) : null;
4434
+ }, [book, editor, revision]);
3971
4435
  const history = editor?.history ?? null;
3972
- const setSelection = useCallback11((nextSelection) => {
4436
+ const setSelection = useCallback13((nextSelection) => {
3973
4437
  const next = clampEditableEpubSelection(editorRef.current?.book ?? null, nextSelection);
3974
4438
  selectionRef.current = next;
3975
4439
  setSelectionState(next);
3976
4440
  }, []);
3977
- const updateParagraph = useCallback11(
4441
+ const updateParagraph = useCallback13(
3978
4442
  (text, inlineAnnotations) => {
3979
4443
  const current = editorRef.current;
3980
4444
  if (!current) return;
@@ -3984,37 +4448,38 @@ function useEditableEpub(options = {}) {
3984
4448
  },
3985
4449
  []
3986
4450
  );
3987
- const setInlineAnnotations = useCallback11((inlineAnnotations) => {
4451
+ const setInlineAnnotations = useCallback13((inlineAnnotations) => {
3988
4452
  const current = editorRef.current;
3989
4453
  if (!current) return;
3990
4454
  const { chapter, paragraph } = selectionRef.current;
3991
4455
  current.setInlineAnnotations(chapter, paragraph, inlineAnnotations);
3992
4456
  setRevision((value) => value + 1);
3993
4457
  }, []);
3994
- const addImage = useCallback11((image) => {
4458
+ const addImage = useCallback13((image) => {
3995
4459
  const current = editorRef.current;
3996
4460
  if (!current) return;
3997
4461
  current.addImage(selectionRef.current.chapter, image);
3998
4462
  setRevision((value) => value + 1);
3999
4463
  }, []);
4000
- const undo = useCallback11(() => {
4464
+ const undo = useCallback13(() => {
4001
4465
  const current = editorRef.current;
4002
4466
  if (!current) return false;
4003
4467
  const changed = current.undo();
4004
4468
  if (changed) setRevision((value) => value + 1);
4005
4469
  return changed;
4006
4470
  }, []);
4007
- const redo = useCallback11(() => {
4471
+ const redo = useCallback13(() => {
4008
4472
  const current = editorRef.current;
4009
4473
  if (!current) return false;
4010
4474
  const changed = current.redo();
4011
4475
  if (changed) setRevision((value) => value + 1);
4012
4476
  return changed;
4013
4477
  }, []);
4014
- const exportEpub = useCallback11(
4478
+ const exportEpub = useCallback13(
4015
4479
  async (exportOptions) => {
4016
4480
  const current = editorRef.current;
4017
4481
  if (!current) return null;
4482
+ exportsInFlightRef.current++;
4018
4483
  setExporting(true);
4019
4484
  try {
4020
4485
  const buffer = await current.export(
@@ -4023,7 +4488,7 @@ function useEditableEpub(options = {}) {
4023
4488
  onExportRef.current?.(buffer);
4024
4489
  return buffer;
4025
4490
  } finally {
4026
- setExporting(false);
4491
+ setExporting(--exportsInFlightRef.current > 0);
4027
4492
  }
4028
4493
  },
4029
4494
  []
@@ -4056,263 +4521,12 @@ function withAssetResolver(options, resolver) {
4056
4521
  return { ...options, assetResolver: resolver };
4057
4522
  }
4058
4523
 
4059
- // src/useEpubProject.ts
4060
- import {
4061
- EpubProject as EpubProject2,
4062
- parseEpub as parseEpub2
4063
- } from "@libraz/mejiro/epub";
4064
- import { useCallback as useCallback12, useEffect as useEffect14, useMemo as useMemo11, useRef as useRef18, useState as useState16 } from "react";
4065
- function useEpubProject(options = {}) {
4066
- const defaultTitle = options.defaultChapterTitle;
4067
- const defaultBody = options.defaultChapterBody;
4068
- const [metadata, setMetadataState] = useState16(
4069
- () => initialMetadata(options.metadata)
4070
- );
4071
- const [chapters, setChaptersState] = useState16(
4072
- () => options.chapters?.length ? options.chapters : [defaultChapter3([], defaultTitle, defaultBody)]
4073
- );
4074
- const [selectedChapter, setSelectedChapterState] = useState16(0);
4075
- const [cover, setCoverState] = useState16(options.cover ?? null);
4076
- const [assets, setAssetsState] = useState16(options.assets ?? []);
4077
- const [previewBook, setPreviewBook] = useState16(null);
4078
- const [previewError, setPreviewError] = useState16(null);
4079
- const [previewing, setPreviewing] = useState16(false);
4080
- const previewRequestIdRef = useRef18(0);
4081
- const onPreviewRef = useRef18(options.onPreview);
4082
- const onExportRef = useRef18(options.onExport);
4083
- onPreviewRef.current = options.onPreview;
4084
- onExportRef.current = options.onExport;
4085
- const assetResolverRef = useRef18(options.assetResolver);
4086
- assetResolverRef.current = options.assetResolver;
4087
- const metadataRef = useRef18(metadata);
4088
- metadataRef.current = metadata;
4089
- const chaptersRef = useRef18(chapters);
4090
- chaptersRef.current = chapters;
4091
- const selectedRef = useRef18(selectedChapter);
4092
- selectedRef.current = selectedChapter;
4093
- const coverRef = useRef18(cover);
4094
- coverRef.current = cover;
4095
- const assetsRef = useRef18(assets);
4096
- assetsRef.current = assets;
4097
- const buildProject = useCallback12(() => {
4098
- const currentCover = coverRef.current;
4099
- const project = EpubProject2.fromManuscript({
4100
- metadata: metadataRef.current,
4101
- includeTitlePage: false,
4102
- includeTitleInFirstChapter: true,
4103
- chapters: chaptersRef.current.map((chapter) => ({
4104
- id: chapter.id,
4105
- title: chapter.title || "Untitled",
4106
- body: chapter.body
4107
- })),
4108
- ...currentCover ? { cover: currentCover } : {}
4109
- });
4110
- for (const asset of assetsRef.current) project.addAsset(asset);
4111
- return project;
4112
- }, []);
4113
- const exportOptions = useCallback12((signal) => {
4114
- const resolver = assetResolverRef.current;
4115
- return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
4116
- }, []);
4117
- useEffect14(() => {
4118
- const requestId = ++previewRequestIdRef.current;
4119
- const controller = new AbortController();
4120
- let cancelled = false;
4121
- setPreviewing(true);
4122
- const timer = setTimeout(() => {
4123
- void (async () => {
4124
- try {
4125
- const book = await parseEpub2(
4126
- await buildProject().export(exportOptions(controller.signal))
4127
- );
4128
- if (cancelled || requestId !== previewRequestIdRef.current) return;
4129
- setPreviewBook(book);
4130
- setPreviewError(null);
4131
- onPreviewRef.current?.(book);
4132
- } catch (err) {
4133
- if (!cancelled && requestId === previewRequestIdRef.current) {
4134
- setPreviewError(toError(err));
4135
- }
4136
- } finally {
4137
- if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
4138
- }
4139
- })();
4140
- }, options.debounceMs ?? 250);
4141
- return () => {
4142
- cancelled = true;
4143
- previewRequestIdRef.current++;
4144
- clearTimeout(timer);
4145
- controller.abort();
4146
- };
4147
- }, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
4148
- const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
4149
- const setMetadata = useCallback12((patch) => {
4150
- const next = mergeMetadata(metadataRef.current, patch);
4151
- metadataRef.current = next;
4152
- setMetadataState(next);
4153
- }, []);
4154
- const commit = useCallback12((nextChapters, nextSelected) => {
4155
- chaptersRef.current = nextChapters;
4156
- selectedRef.current = nextSelected;
4157
- setChaptersState(nextChapters);
4158
- setSelectedChapterState(nextSelected);
4159
- }, []);
4160
- const setChapters = useCallback12(
4161
- (next) => {
4162
- const normalized = next.length ? next : [defaultChapter3([], defaultTitle, defaultBody)];
4163
- const prev = selectedRef.current;
4164
- const selectedId = chaptersRef.current[prev]?.id;
4165
- const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
4166
- commit(
4167
- normalized,
4168
- nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
4169
- );
4170
- },
4171
- [commit, defaultBody, defaultTitle]
4172
- );
4173
- const setCover = useCallback12((asset) => {
4174
- coverRef.current = asset;
4175
- setCoverState(asset);
4176
- }, []);
4177
- const setAssets = useCallback12((next) => {
4178
- assetsRef.current = next;
4179
- setAssetsState(next);
4180
- }, []);
4181
- const setSelectedChapter = useCallback12((index) => {
4182
- const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
4183
- selectedRef.current = next;
4184
- setSelectedChapterState(next);
4185
- }, []);
4186
- const patchChapter = useCallback12((index, patch) => {
4187
- const next = chaptersRef.current.map(
4188
- (chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
4189
- );
4190
- chaptersRef.current = next;
4191
- setChaptersState(next);
4192
- }, []);
4193
- const addChapter = useCallback12(
4194
- (chapter = {}) => {
4195
- const current = chaptersRef.current;
4196
- const generated = defaultChapter3(current, defaultTitle, defaultBody);
4197
- const next = [
4198
- ...current,
4199
- {
4200
- id: chapter.id ?? generated.id,
4201
- title: chapter.title ?? generated.title,
4202
- body: chapter.body ?? generated.body
4203
- }
4204
- ];
4205
- commit(next, next.length - 1);
4206
- },
4207
- [commit, defaultBody, defaultTitle]
4208
- );
4209
- const removeChapter = useCallback12(
4210
- (index = selectedRef.current) => {
4211
- const current = chaptersRef.current;
4212
- if (current.length <= 1) return;
4213
- const next = current.filter((_, chapterIndex) => chapterIndex !== index);
4214
- const prev = selectedRef.current;
4215
- let nextSelected;
4216
- if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
4217
- else if (index < prev) nextSelected = prev - 1;
4218
- else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
4219
- commit(next, nextSelected);
4220
- },
4221
- [commit]
4222
- );
4223
- const reorderChapters = useCallback12(
4224
- (from, to) => {
4225
- const current = chaptersRef.current;
4226
- if (from < 0 || from >= current.length) return;
4227
- const next = [...current];
4228
- const [moved] = next.splice(from, 1);
4229
- const target = Math.max(0, Math.min(next.length, to));
4230
- next.splice(target, 0, moved);
4231
- const prev = selectedRef.current;
4232
- let nextSelected = prev;
4233
- if (prev === from) nextSelected = target;
4234
- else if (from < prev && target >= prev) nextSelected = prev - 1;
4235
- else if (from > prev && target <= prev) nextSelected = prev + 1;
4236
- commit(next, nextSelected);
4237
- },
4238
- [commit]
4239
- );
4240
- const exportEpub = useCallback12(async () => {
4241
- const buffer = await buildProject().export(exportOptions());
4242
- onExportRef.current?.(buffer);
4243
- return buffer;
4244
- }, [buildProject, exportOptions]);
4245
- return useMemo11(
4246
- () => ({
4247
- metadata,
4248
- chapters,
4249
- selectedChapter,
4250
- currentChapter,
4251
- cover,
4252
- assets,
4253
- previewBook,
4254
- previewError,
4255
- previewing,
4256
- setMetadata,
4257
- setChapters,
4258
- setSelectedChapter,
4259
- setCover,
4260
- setAssets,
4261
- patchChapter,
4262
- addChapter,
4263
- removeChapter,
4264
- reorderChapters,
4265
- buildProject,
4266
- exportEpub
4267
- }),
4268
- [
4269
- addChapter,
4270
- assets,
4271
- buildProject,
4272
- chapters,
4273
- cover,
4274
- currentChapter,
4275
- exportEpub,
4276
- metadata,
4277
- patchChapter,
4278
- previewBook,
4279
- previewError,
4280
- previewing,
4281
- reorderChapters,
4282
- removeChapter,
4283
- selectedChapter,
4284
- setAssets,
4285
- setChapters,
4286
- setCover,
4287
- setMetadata,
4288
- setSelectedChapter
4289
- ]
4290
- );
4291
- }
4292
- function mergeMetadata(current, patch) {
4293
- const next = { ...current, ...patch };
4294
- if (!next.identifier?.trim()) next.identifier = current.identifier;
4295
- return next;
4296
- }
4297
- function initialMetadata(patch = {}) {
4298
- const metadata = mergeDefined(
4299
- { title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
4300
- patch
4301
- );
4302
- if (!metadata.identifier?.trim()) metadata.identifier = `urn:uuid:${crypto.randomUUID()}`;
4303
- return metadata;
4304
- }
4305
- function defaultChapter3(existing, titleFor = (i) => i === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${i + 1}\u8A71`, bodyFor = (i) => i === 0 ? "\u3053\u308C\u306F\uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B\u306E\u30EB\u30D3\u4F8B\u3067\u3059\u3002\n\n\u672C\u6587\u3092\u3053\u3053\u306B\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002" : "") {
4306
- const index = existing.length;
4307
- return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
4308
- }
4309
-
4310
4524
  // src/useImageOverlay.ts
4311
4525
  import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointerPress as isPrimaryPointerPress3 } from "@libraz/mejiro/browser";
4312
4526
  import {
4313
- useCallback as useCallback13,
4314
- useEffect as useEffect15,
4315
- useRef as useRef19,
4527
+ useCallback as useCallback14,
4528
+ useEffect as useEffect16,
4529
+ useRef as useRef20,
4316
4530
  useState as useState17
4317
4531
  } from "react";
4318
4532
  function useImageOverlay(layout, spreadIdx, onUpdate, options) {
@@ -4322,33 +4536,33 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4322
4536
  const defY = options?.defaultY ?? 100;
4323
4537
  const margin = options?.margin;
4324
4538
  const [imageRect, setImageRect] = useState17(null);
4325
- const rectRef = useRef19(imageRect);
4539
+ const rectRef = useRef20(imageRect);
4326
4540
  rectRef.current = imageRect;
4327
- const layoutRef = useRef19(layout);
4541
+ const layoutRef = useRef20(layout);
4328
4542
  layoutRef.current = layout;
4329
- const spreadRef = useRef19(spreadIdx);
4543
+ const spreadRef = useRef20(spreadIdx);
4330
4544
  spreadRef.current = spreadIdx;
4331
- const onUpdateRef = useRef19(onUpdate);
4545
+ const onUpdateRef = useRef20(onUpdate);
4332
4546
  onUpdateRef.current = onUpdate;
4333
- const marginRef = useRef19(margin);
4547
+ const marginRef = useRef20(margin);
4334
4548
  marginRef.current = margin;
4335
- const activeDragCleanupsRef = useRef19(/* @__PURE__ */ new Set());
4336
- useEffect15(
4549
+ const activeDragCleanupsRef = useRef20(/* @__PURE__ */ new Set());
4550
+ useEffect16(
4337
4551
  () => () => {
4338
4552
  for (const cleanup of activeDragCleanupsRef.current) cleanup();
4339
4553
  activeDragCleanupsRef.current.clear();
4340
4554
  },
4341
4555
  []
4342
4556
  );
4343
- const syncToLayout = useCallback13((rect) => {
4557
+ const syncToLayout = useCallback14((rect) => {
4344
4558
  const lo = layoutRef.current;
4345
4559
  if (!lo) return;
4346
4560
  const images = rect ? [{ x: rect.x, y: rect.y, w: rect.w, h: rect.h, margin: marginRef.current }] : void 0;
4347
4561
  const spread = lo.syncImages(spreadRef.current, images);
4348
4562
  onUpdateRef.current(spread);
4349
4563
  }, []);
4350
- const syncedRef = useRef19({ layout, spreadIdx, margin });
4351
- useEffect15(() => {
4564
+ const syncedRef = useRef20({ layout, spreadIdx, margin });
4565
+ useEffect16(() => {
4352
4566
  const prev = syncedRef.current;
4353
4567
  syncedRef.current = { layout, spreadIdx, margin };
4354
4568
  if (prev.layout === layout && prev.spreadIdx === spreadIdx && prev.margin === margin) return;
@@ -4358,7 +4572,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4358
4572
  }
4359
4573
  syncToLayout(rectRef.current);
4360
4574
  }, [layout, spreadIdx, margin, syncToLayout]);
4361
- const toggleImage = useCallback13(() => {
4575
+ const toggleImage = useCallback14(() => {
4362
4576
  if (rectRef.current) {
4363
4577
  for (const cancel of [...activeDragCleanupsRef.current]) cancel();
4364
4578
  rectRef.current = null;
@@ -4371,7 +4585,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4371
4585
  syncToLayout(r);
4372
4586
  }
4373
4587
  }, [defX, defY, defW, defH, syncToLayout]);
4374
- const applyRect = useCallback13(
4588
+ const applyRect = useCallback14(
4375
4589
  (rect) => {
4376
4590
  if (rectRef.current === null) return;
4377
4591
  rectRef.current = rect;
@@ -4380,7 +4594,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4380
4594
  },
4381
4595
  [syncToLayout]
4382
4596
  );
4383
- const onOverlayPointerDown = useCallback13(
4597
+ const onOverlayPointerDown = useCallback14(
4384
4598
  (e) => {
4385
4599
  if (!isPrimaryPointerPress3(e)) return;
4386
4600
  e.preventDefault();
@@ -4402,7 +4616,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4402
4616
  },
4403
4617
  [applyRect]
4404
4618
  );
4405
- const onResizePointerDown = useCallback13(
4619
+ const onResizePointerDown = useCallback14(
4406
4620
  (e) => {
4407
4621
  if (!isPrimaryPointerPress3(e)) return;
4408
4622
  e.preventDefault();
@@ -4435,7 +4649,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4435
4649
  }
4436
4650
 
4437
4651
  // src/useLibrary.ts
4438
- import { useCallback as useCallback14, useEffect as useEffect16, useMemo as useMemo12, useState as useState18 } from "react";
4652
+ import { useCallback as useCallback15, useEffect as useEffect17, useMemo as useMemo12, useState as useState18 } from "react";
4439
4653
  function resolveCurrentId(volumes, currentId, preferredId) {
4440
4654
  if (volumes.length === 0) return null;
4441
4655
  if (preferredId != null && volumes.some((v) => v.id === preferredId)) return preferredId;
@@ -4449,7 +4663,7 @@ function useLibrary(options) {
4449
4663
  );
4450
4664
  const [navigated, setNavigated] = useState18(false);
4451
4665
  const resolvedId = resolveCurrentId(volumes, currentId, navigated ? void 0 : initialVolumeId);
4452
- useEffect16(() => {
4666
+ useEffect17(() => {
4453
4667
  if (resolvedId === currentId) return;
4454
4668
  setCurrentId(resolvedId);
4455
4669
  const volume = volumes.find((v) => v.id === resolvedId);
@@ -4463,7 +4677,7 @@ function useLibrary(options) {
4463
4677
  () => index >= 0 && index < volumes.length ? volumes[index] : null,
4464
4678
  [index, volumes]
4465
4679
  );
4466
- const activate = useCallback14(
4680
+ const activate = useCallback15(
4467
4681
  (i) => {
4468
4682
  const v = volumes[i];
4469
4683
  if (!v) return;
@@ -4473,17 +4687,17 @@ function useLibrary(options) {
4473
4687
  },
4474
4688
  [volumes, onChange]
4475
4689
  );
4476
- const next = useCallback14(() => {
4690
+ const next = useCallback15(() => {
4477
4691
  if (index < 0) return;
4478
4692
  const ni = Math.min(volumes.length - 1, index + 1);
4479
4693
  if (ni !== index) activate(ni);
4480
4694
  }, [index, activate, volumes.length]);
4481
- const prev = useCallback14(() => {
4695
+ const prev = useCallback15(() => {
4482
4696
  if (index < 0) return;
4483
4697
  const ni = Math.max(0, index - 1);
4484
4698
  if (ni !== index) activate(ni);
4485
4699
  }, [index, activate]);
4486
- const goTo = useCallback14(
4700
+ const goTo = useCallback15(
4487
4701
  (id) => {
4488
4702
  const ni = volumes.findIndex((v) => v.id === id);
4489
4703
  if (ni < 0) return;
@@ -4495,19 +4709,27 @@ function useLibrary(options) {
4495
4709
  }
4496
4710
 
4497
4711
  // src/useManuscriptLayout.ts
4498
- import { useMemo as useMemo13 } from "react";
4712
+ import { useMemo as useMemo13, useRef as useRef21 } from "react";
4713
+ function chapterSignature(chapter) {
4714
+ return chapter ? JSON.stringify([chapter.id, chapter.title, chapter.body]) : null;
4715
+ }
4499
4716
  function useManuscriptLayout(book, chapter, surface, options = {}) {
4500
4717
  const dialect = options.dialect ?? "mejiro";
4718
+ const signature = chapterSignature(chapter);
4719
+ const chapterRef = useRef21(chapter);
4720
+ chapterRef.current = chapter;
4501
4721
  const source = useMemo13(() => {
4502
- if (!chapter) return null;
4722
+ if (signature === null) return null;
4503
4723
  return {
4504
4724
  measure: (el) => book.computePageSize(el),
4505
4725
  layout: async () => {
4506
- const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
4726
+ const current = chapterRef.current;
4727
+ if (!current) return null;
4728
+ const layouts = await book.layoutManuscript({ chapters: [current], dialect });
4507
4729
  return layouts.values().next().value ?? null;
4508
4730
  }
4509
4731
  };
4510
- }, [book, chapter, dialect]);
4732
+ }, [book, signature, dialect]);
4511
4733
  return useSurfaceLayout(source, surface, {
4512
4734
  ...options,
4513
4735
  enableResize: options.enableResize ?? true,
@@ -4520,28 +4742,30 @@ import {
4520
4742
  parseReadingPosition,
4521
4743
  serializeReadingPosition
4522
4744
  } from "@libraz/mejiro";
4523
- import { useCallback as useCallback15, useEffect as useEffect17, useRef as useRef20, useState as useState19 } from "react";
4745
+ import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef22, useState as useState19 } from "react";
4524
4746
  function useReadingPosition(options) {
4525
4747
  const { key, throttleMs = 250 } = options;
4526
4748
  const storage = options.storage ?? resolveDefaultStorage();
4527
- const [position, setPosition] = useState19(
4528
- () => readStorage(storage, key, parseReadingPosition, null)
4529
- );
4530
- const storageRef = useRef20(storage);
4749
+ const storageRef = useRef22(storage);
4531
4750
  storageRef.current = storage;
4532
- const pendingRef = useRef20(null);
4751
+ const [hydrated, setHydrated] = useState19(
4752
+ () => ({ key, value: readStorage(storage, key, parseReadingPosition, null) })
4753
+ );
4754
+ let position = hydrated.value;
4755
+ if (hydrated.key !== key) {
4756
+ position = readStorage(storage, key, parseReadingPosition, null);
4757
+ setHydrated({ key, value: position });
4758
+ }
4759
+ const pendingRef = useRef22(null);
4533
4760
  if (!pendingRef.current) pendingRef.current = createPendingWrite();
4534
4761
  const pending = pendingRef.current;
4535
- useEffect17(() => flushOnPageHide(pending.flush), [pending]);
4536
- useEffect17(() => {
4537
- setPosition(readStorage(storageRef.current, key, parseReadingPosition, null));
4538
- return pending.flush;
4539
- }, [key, pending]);
4540
- const onChangeRef = useRef20(options.onChange);
4762
+ useEffect18(() => flushOnPageHide(pending.flush), [pending]);
4763
+ useEffect18(() => pending.flush, [key, pending]);
4764
+ const onChangeRef = useRef22(options.onChange);
4541
4765
  onChangeRef.current = options.onChange;
4542
- const save = useCallback15(
4766
+ const save = useCallback16(
4543
4767
  (next) => {
4544
- setPosition(next);
4768
+ setHydrated({ key, value: next });
4545
4769
  const currentStorage = storageRef.current;
4546
4770
  if (currentStorage) {
4547
4771
  pending.schedule(() => {
@@ -4552,8 +4776,8 @@ function useReadingPosition(options) {
4552
4776
  },
4553
4777
  [key, throttleMs, pending]
4554
4778
  );
4555
- const clear = useCallback15(() => {
4556
- setPosition(null);
4779
+ const clear = useCallback16(() => {
4780
+ setHydrated({ key, value: null });
4557
4781
  pending.cancel();
4558
4782
  removeStorage(storageRef.current, key);
4559
4783
  onChangeRef.current?.(null);