@libraz/mejiro-react 0.9.1 → 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.
package/dist/index.js CHANGED
@@ -60,27 +60,29 @@ function MejiroChapterNav({
60
60
  /* @__PURE__ */ jsx2("ol", { className: "mejiro-reader-chapter-list", children: epub.chapters.map((ch, i) => {
61
61
  const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
62
62
  const preview = ch.paragraphs.find((p) => !p.headingLevel && p.text.trim())?.text;
63
- const chapterKey = `${title}-${ch.paragraphs.map((p) => p.text).join("|").slice(0, 120)}`;
64
63
  const headings = ch.paragraphs.flatMap(
65
64
  (p, paragraph) => p.headingLevel && p.text.trim() && p.text.trim() !== title ? [{ paragraph, text: p.text }] : []
66
65
  ).slice(0, 3);
67
- return /* @__PURE__ */ jsx2("li", { className: "mejiro-reader-chapter-list-item", children: /* @__PURE__ */ jsxs(
68
- "button",
69
- {
70
- type: "button",
71
- className: `mejiro-reader-chapter-card${i === chapter ? " is-active" : ""}`,
72
- "aria-current": i === chapter ? "true" : void 0,
73
- onClick: () => onChange(i),
74
- children: [
75
- /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-number", children: String(i + 1).padStart(2, "0") }),
76
- /* @__PURE__ */ jsxs("span", { className: "mejiro-reader-chapter-main", children: [
77
- /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-title", children: title }),
78
- preview && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-preview", children: textPreview(preview) }),
79
- headings.length > 0 && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-subheads", children: headings.map((heading) => /* @__PURE__ */ jsx2("span", { children: textPreview(heading.text, 30) }, heading.paragraph)) })
80
- ] })
81
- ]
82
- }
83
- ) }, chapterKey);
66
+ return (
67
+ // biome-ignore lint/suspicious/noArrayIndexKey: chapter titles and text are not guaranteed unique
68
+ /* @__PURE__ */ jsx2("li", { className: "mejiro-reader-chapter-list-item", children: /* @__PURE__ */ jsxs(
69
+ "button",
70
+ {
71
+ type: "button",
72
+ className: `mejiro-reader-chapter-card${i === chapter ? " is-active" : ""}`,
73
+ "aria-current": i === chapter ? "true" : void 0,
74
+ onClick: () => onChange(i),
75
+ children: [
76
+ /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-number", children: String(i + 1).padStart(2, "0") }),
77
+ /* @__PURE__ */ jsxs("span", { className: "mejiro-reader-chapter-main", children: [
78
+ /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-title", children: title }),
79
+ preview && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-preview", children: textPreview(preview) }),
80
+ headings.length > 0 && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-subheads", children: headings.map((heading) => /* @__PURE__ */ jsx2("span", { children: textPreview(heading.text, 30) }, heading.paragraph)) })
81
+ ] })
82
+ ]
83
+ }
84
+ ) }, i)
85
+ );
84
86
  }) })
85
87
  ] });
86
88
  }
@@ -158,6 +160,7 @@ function MejiroDropZone({
158
160
  hidden: true,
159
161
  onChange: (e) => {
160
162
  const file = e.target.files?.[0];
163
+ e.target.value = "";
161
164
  if (file && isValid(file)) onFile(file);
162
165
  }
163
166
  }
@@ -173,7 +176,29 @@ import {
173
176
  EditableEpub,
174
177
  exportEditableEpub
175
178
  } from "@libraz/mejiro/epub";
176
- import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef11, 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
+ }
177
202
 
178
203
  // src/errors.ts
179
204
  import { assertEpubInputSize } from "@libraz/mejiro/epub";
@@ -202,14 +227,15 @@ async function withErrorReporting(task, report) {
202
227
  import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
203
228
  import { normalizeFontFamily as normalizeFontFamily5 } from "@libraz/mejiro/browser";
204
229
  import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
230
+ import { pageNumberFor as pageNumberFor2 } from "@libraz/mejiro/render";
205
231
  import {
206
232
  forwardRef,
207
- useCallback as useCallback6,
208
- useEffect as useEffect8,
233
+ useCallback as useCallback7,
234
+ useEffect as useEffect9,
209
235
  useImperativeHandle,
210
236
  useLayoutEffect as useLayoutEffect4,
211
- useMemo as useMemo4,
212
- useRef as useRef10,
237
+ useMemo as useMemo5,
238
+ useRef as useRef12,
213
239
  useState as useState8
214
240
  } from "react";
215
241
 
@@ -225,7 +251,10 @@ function MejiroPageIndicator({ current, total }) {
225
251
 
226
252
  // src/MejiroScrollView.tsx
227
253
  import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
254
+ import { pageNumberFor } from "@libraz/mejiro/render";
228
255
  import {
256
+ memo,
257
+ useCallback as useCallback2,
229
258
  useEffect as useEffect2,
230
259
  useLayoutEffect,
231
260
  useMemo as useMemo3,
@@ -324,10 +353,7 @@ function paragraphScaleStyle(scale) {
324
353
  function MejiroPage({ page, className, style }) {
325
354
  const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
326
355
  return /* @__PURE__ */ jsx6("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
327
- const paraClass = paragraphClassName(
328
- paragraph.kind ?? (paragraph.isHeading ? "heading" : void 0),
329
- paragraph.headingLevel
330
- );
356
+ const paraClass = paragraphClassName(paragraph.kind, paragraph.headingLevel);
331
357
  return (
332
358
  // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
333
359
  /* @__PURE__ */ jsx6("div", { className: paraClass, style: paragraphScaleStyle(paragraph.scale), children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
@@ -468,6 +494,8 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
468
494
  spreadIdxRef.current = spreadIdx;
469
495
  const onUpdateRef = useRef2(options.onUpdate);
470
496
  onUpdateRef.current = options.onUpdate;
497
+ const marginRef = useRef2(margin);
498
+ marginRef.current = margin;
471
499
  const activeDragCleanupsRef = useRef2(/* @__PURE__ */ new Set());
472
500
  useEffect(
473
501
  () => () => {
@@ -476,15 +504,12 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
476
504
  },
477
505
  []
478
506
  );
479
- const syncSpreads = useCallback(
480
- (spreads) => {
481
- const lo = layoutRef.current;
482
- if (!lo) return;
483
- applyImages(lo, imagesRef.current, spreads, margin);
484
- for (const si of spreads) onUpdateRef.current?.(lo.getSpread(si));
485
- },
486
- [margin]
487
- );
507
+ const syncSpreads = useCallback((spreads) => {
508
+ const lo = layoutRef.current;
509
+ if (!lo) return;
510
+ applyImages(lo, imagesRef.current, spreads, marginRef.current);
511
+ for (const si of spreads) onUpdateRef.current?.(lo.getSpread(si));
512
+ }, []);
488
513
  const appliedRef = useRef2({
489
514
  layout: null,
490
515
  margin
@@ -649,6 +674,71 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
649
674
  // src/MejiroScrollView.tsx
650
675
  import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
651
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
+ });
652
742
  function MejiroScrollView({
653
743
  layout,
654
744
  pageWidth,
@@ -664,6 +754,7 @@ function MejiroScrollView({
664
754
  selectionRects,
665
755
  spreadIdx,
666
756
  images = NO_IMAGES,
757
+ pageNumbers = "both",
667
758
  onImagePointerDown,
668
759
  onImageResizePointerDown,
669
760
  onImageClose
@@ -698,10 +789,30 @@ function MejiroScrollView({
698
789
  }
699
790
  return byPage;
700
791
  }, [selectionRects]);
701
- const contentStyle = { height: contentHeight };
702
- if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
703
- if (fontSize != null) contentStyle.fontSize = fontSize;
704
- 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
+ );
705
816
  useEffect2(() => {
706
817
  const container = containerRef.current;
707
818
  if (!container) return;
@@ -759,67 +870,26 @@ function MejiroScrollView({
759
870
  {
760
871
  className: "mejiro-reader-scroll-track",
761
872
  style: { display: "flex", flexDirection: "column", gap: pageGap },
762
- children: pages.map((result, i) => /* @__PURE__ */ jsxs5(
763
- "div",
873
+ children: pages.map((result, i) => /* @__PURE__ */ jsx9(
874
+ ScrollPage,
764
875
  {
765
- ref: (el) => {
766
- pageRefs.current[i] = el;
767
- },
768
- "data-page-idx": i,
769
- className: "mejiro-reader-page",
770
- style: {
771
- width: pageWidth,
772
- height: pageHeight,
773
- flexShrink: 0,
774
- ...i === rightPage && rightImages.length > 0 || i === leftPage && leftImages.length > 0 ? { overflow: "visible" } : null
775
- },
776
- children: [
777
- /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-rule" }),
778
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-header", children: [
779
- /* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-title" }),
780
- /* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-num", children: i + 1 })
781
- ] }),
782
- /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
783
- /* @__PURE__ */ jsx9(
784
- MejiroPageView,
785
- {
786
- result,
787
- slotMode,
788
- fontFamily,
789
- lineSpacing,
790
- className: "mejiro-reader-page-content",
791
- style: contentStyle
792
- }
793
- ),
794
- rectsByPage[i] && /* @__PURE__ */ jsx9(
795
- MejiroSelectionLayer,
796
- {
797
- rects: rectsByPage[i],
798
- side: i % 2 === 0 ? "right" : "left"
799
- }
800
- )
801
- ] }) }),
802
- i === rightPage && rightImages.map((item) => /* @__PURE__ */ jsx9(
803
- MejiroImageOverlay,
804
- {
805
- rect: item.rect,
806
- onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
807
- onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
808
- onClose: () => onImageClose?.(item.id)
809
- },
810
- item.id
811
- )),
812
- i === leftPage && leftImages.map((item) => /* @__PURE__ */ jsx9(
813
- MejiroImageOverlay,
814
- {
815
- rect: { ...item.rect, x: item.rect.x + pageWidth },
816
- onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
817
- onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
818
- onClose: () => onImageClose?.(item.id)
819
- },
820
- item.id
821
- ))
822
- ]
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
823
893
  },
824
894
  i
825
895
  ))
@@ -829,7 +899,7 @@ function MejiroScrollView({
829
899
 
830
900
  // src/MejiroSettingsPanel.tsx
831
901
  import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
832
- 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";
833
903
  import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
834
904
  var DEFAULT_FONTS = [
835
905
  { value: "serif", label: "System Serif" },
@@ -898,15 +968,24 @@ function MejiroSettingsPanel({
898
968
  maxFontSize = 48
899
969
  }) {
900
970
  const messages = useI18n();
971
+ const uid = useId();
901
972
  const patch = (next) => onChange({ ...settings, ...next });
902
973
  const currentFont = normalizeFontFamily3(settings.fontFamily);
903
974
  const fontOptions = fonts.some((f) => f.value === currentFont) ? fonts : [{ value: currentFont, label: fontLabelFromCss(currentFont) }, ...fonts];
904
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: [
905
976
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
906
977
  /* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
907
- /* @__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
+ ) }),
908
987
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
909
- /* @__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 }),
910
989
  /* @__PURE__ */ jsx10(
911
990
  "button",
912
991
  {
@@ -927,7 +1006,7 @@ function MejiroSettingsPanel({
927
1006
  /* @__PURE__ */ jsx10(
928
1007
  CommittedNumberInput,
929
1008
  {
930
- id: "mejiro-reader-font-size",
1009
+ id: `${uid}-font-size`,
931
1010
  value: settings.fontSize,
932
1011
  min: minFontSize,
933
1012
  max: maxFontSize,
@@ -956,11 +1035,11 @@ function MejiroSettingsPanel({
956
1035
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
957
1036
  /* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
958
1037
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
959
- /* @__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 }),
960
1039
  /* @__PURE__ */ jsxs6(
961
1040
  "select",
962
1041
  {
963
- id: "mejiro-reader-kinsoku",
1042
+ id: `${uid}-kinsoku`,
964
1043
  value: settings.mode ?? "strict",
965
1044
  onChange: (e) => patch({ mode: e.target.value }),
966
1045
  children: [
@@ -971,11 +1050,11 @@ function MejiroSettingsPanel({
971
1050
  )
972
1051
  ] }),
973
1052
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
974
- /* @__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 }),
975
1054
  /* @__PURE__ */ jsxs6(
976
1055
  "select",
977
1056
  {
978
- id: "mejiro-reader-hanging",
1057
+ id: `${uid}-hanging`,
979
1058
  value: String(settings.enableHanging ?? true),
980
1059
  onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
981
1060
  children: [
@@ -986,11 +1065,11 @@ function MejiroSettingsPanel({
986
1065
  )
987
1066
  ] }),
988
1067
  /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
989
- /* @__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 }),
990
1069
  /* @__PURE__ */ jsx10(
991
1070
  CommittedNumberInput,
992
1071
  {
993
- id: "mejiro-reader-line-spacing",
1072
+ id: `${uid}-line-spacing`,
994
1073
  className: "mejiro-reader-control--wide",
995
1074
  value: settings.lineSpacing ?? 1.8,
996
1075
  min: 1,
@@ -1061,11 +1140,13 @@ function MejiroSpread({
1061
1140
  const selectionRef = useRef5(null);
1062
1141
  const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
1063
1142
  const gestureRef = useRef5(null);
1143
+ const swipedRef = useRef5(false);
1064
1144
  const gestureEnabled = onSwipe != null || onSurfaceTap != null;
1065
1145
  const SWIPE_THRESHOLD = 40;
1066
1146
  const TAP_MOVE_THRESHOLD = 8;
1067
1147
  function handleGesturePointerDown(e) {
1068
1148
  if (!gestureEnabled) return;
1149
+ swipedRef.current = false;
1069
1150
  if (!isPrimaryPointerPress2(e)) return;
1070
1151
  if (e.target?.closest(OVERLAY_SELECTOR)) return;
1071
1152
  gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
@@ -1080,6 +1161,7 @@ function MejiroSpread({
1080
1161
  const ax = Math.abs(dx);
1081
1162
  const ay = Math.abs(dy);
1082
1163
  if (ax >= SWIPE_THRESHOLD && ax >= ay * 1.4) {
1164
+ swipedRef.current = true;
1083
1165
  onSwipe?.(dx < 0 ? "next" : "prev");
1084
1166
  return;
1085
1167
  }
@@ -1089,6 +1171,12 @@ function MejiroSpread({
1089
1171
  onSurfaceTap?.();
1090
1172
  }
1091
1173
  }
1174
+ function suppressSwipeClick(e) {
1175
+ if (!swipedRef.current) return;
1176
+ swipedRef.current = false;
1177
+ e.preventDefault();
1178
+ e.stopPropagation();
1179
+ }
1092
1180
  function resolvePointer(e) {
1093
1181
  if (!anchorAtCoord) return null;
1094
1182
  const content = contentAtPoint(e.currentTarget, e.clientX, e.clientY);
@@ -1207,6 +1295,7 @@ function MejiroSpread({
1207
1295
  onPointerUp: useCombined ? combinedPointerUp : void 0,
1208
1296
  onPointerCancel: useCombined ? combinedPointerCancel : void 0,
1209
1297
  onLostPointerCapture: selectionEnabled ? endSelection : void 0,
1298
+ onClickCapture: gestureEnabled ? suppressSwipeClick : void 0,
1210
1299
  children: [
1211
1300
  renderPage(singlePage ? singleSide : "right"),
1212
1301
  !singlePage && renderPage("left"),
@@ -1366,34 +1455,64 @@ function createDraftChangeTracker(chapters, key) {
1366
1455
  }
1367
1456
 
1368
1457
  // src/useChapterLayout.ts
1458
+ import { useMemo as useMemo4, useRef as useRef8 } from "react";
1459
+
1460
+ // src/useSurfaceLayout.ts
1369
1461
  import {
1370
- useCallback as useCallback2,
1371
- useEffect as useEffect4,
1462
+ useCallback as useCallback3,
1463
+ useEffect as useEffect5,
1372
1464
  useLayoutEffect as useLayoutEffect2,
1373
- useRef as useRef6,
1465
+ useRef as useRef7,
1374
1466
  useState as useState4
1375
1467
  } from "react";
1376
- function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1377
- const enableResize = options.enableResize ?? true;
1378
- const resizeDebounce = options.resizeDebounce ?? 120;
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
1490
+ function useSurfaceLayout(source, surface, options) {
1491
+ const { enableResize, resizeDebounce } = options;
1379
1492
  const [layout, setLayout] = useState4(null);
1380
1493
  const [pageWidth, setPageWidth] = useState4(0);
1381
1494
  const [pageHeight, setPageHeight] = useState4(0);
1382
1495
  const [contentHeight, setContentHeight] = useState4(0);
1383
1496
  const [elapsedMs, setElapsedMs] = useState4(0);
1384
- const layoutRef = useRef6(null);
1385
- const requestIdRef = useRef6(0);
1386
- const pendingRestore = 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);
1387
1502
  layoutRef.current = layout;
1388
- const optionsRef = useRef6(options);
1503
+ const requests = useRequestToken();
1504
+ const pendingRestore = useRef7(null);
1505
+ const measuredBoxRef = useRef7(null);
1506
+ const optionsRef = useRef7(options);
1389
1507
  optionsRef.current = options;
1390
- const recompute = useCallback2(
1508
+ const recompute = useCallback3(
1391
1509
  async (opts = {}) => {
1392
1510
  const blank = opts.blank ?? true;
1393
1511
  const o = optionsRef.current;
1394
- const requestId = ++requestIdRef.current;
1395
- const chapter = epub?.chapters[chapterIndex];
1396
- if (!(chapter && surface.current)) {
1512
+ const requestId = requests.begin();
1513
+ const el = element;
1514
+ if (!(source && el)) {
1515
+ measuredBoxRef.current = null;
1397
1516
  setLayout(null);
1398
1517
  setPageWidth(0);
1399
1518
  setPageHeight(0);
@@ -1401,24 +1520,38 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1401
1520
  setElapsedMs(0);
1402
1521
  return;
1403
1522
  }
1404
- const captured = !blank && layoutRef.current ? o.capturePosition?.(layoutRef.current) ?? null : null;
1405
- pendingRestore.current = captured;
1406
- if (blank) setLayout(null);
1407
- const dims = book.computePageSize(surface.current, o.pageGeometry?.());
1408
- setPageWidth(dims.pageWidth);
1409
- setPageHeight(dims.pageHeight);
1410
- setContentHeight(dims.contentHeight);
1411
- const t0 = performance.now();
1412
- const l = await book.layoutChapter(chapter);
1413
- if (requestId !== requestIdRef.current) return;
1414
- layoutRef.current = l;
1415
- setLayout(l);
1416
- setElapsedMs(performance.now() - t0);
1523
+ try {
1524
+ const captured = !blank && layoutRef.current ? o.capturePosition?.(layoutRef.current) ?? null : null;
1525
+ pendingRestore.current = captured;
1526
+ if (blank) setLayout(null);
1527
+ measuredBoxRef.current = { width: el.clientWidth, height: el.clientHeight };
1528
+ const dims = source.measure(el);
1529
+ setPageWidth(dims.pageWidth);
1530
+ setPageHeight(dims.pageHeight);
1531
+ setContentHeight(dims.contentHeight);
1532
+ const t0 = performance.now();
1533
+ const next = await source.layout();
1534
+ if (!requests.isCurrent(requestId)) return;
1535
+ layoutRef.current = next;
1536
+ setLayout(next);
1537
+ setElapsedMs(performance.now() - t0);
1538
+ } catch (err) {
1539
+ if (!requests.isCurrent(requestId)) return;
1540
+ const handler = optionsRef.current.onError;
1541
+ if (!handler) throw err;
1542
+ handler(toError(err));
1543
+ }
1417
1544
  },
1418
- [book, epub, chapterIndex, surface]
1545
+ [source, element, requests]
1419
1546
  );
1547
+ const recomputeRef = useRef7(recompute);
1548
+ recomputeRef.current = recompute;
1549
+ const trigger = useCallback3((opts) => {
1550
+ recomputeRef.current(opts).catch(() => {
1551
+ });
1552
+ }, []);
1420
1553
  useLayoutEffect2(() => {
1421
- requestIdRef.current++;
1554
+ requests.invalidate();
1422
1555
  pendingRestore.current = null;
1423
1556
  setLayout(null);
1424
1557
  setPageWidth(0);
@@ -1426,12 +1559,13 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1426
1559
  setContentHeight(0);
1427
1560
  setElapsedMs(0);
1428
1561
  }, [recompute]);
1429
- useEffect4(() => {
1430
- void recompute();
1562
+ useEffect5(() => {
1563
+ recompute().catch(() => {
1564
+ });
1431
1565
  return () => {
1432
- requestIdRef.current++;
1566
+ requests.invalidate();
1433
1567
  };
1434
- }, [recompute]);
1568
+ }, [recompute, requests]);
1435
1569
  useLayoutEffect2(() => {
1436
1570
  const anchor = pendingRestore.current;
1437
1571
  const restore = optionsRef.current.restorePosition;
@@ -1439,9 +1573,9 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1439
1573
  pendingRestore.current = null;
1440
1574
  restore(layout, anchor);
1441
1575
  }, [layout]);
1442
- useEffect4(() => {
1576
+ useEffect5(() => {
1443
1577
  if (!enableResize) return;
1444
- const el = surface.current;
1578
+ const el = element;
1445
1579
  if (!el) return;
1446
1580
  let timer = null;
1447
1581
  let observed = false;
@@ -1454,18 +1588,23 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1454
1588
  const scheduleReflow = (immediate) => {
1455
1589
  clear();
1456
1590
  if (immediate) {
1457
- void recompute({ blank: false });
1591
+ trigger({ blank: false });
1458
1592
  return;
1459
1593
  }
1460
1594
  timer = setTimeout(() => {
1461
1595
  timer = null;
1462
- void recompute({ blank: false });
1596
+ trigger({ blank: false });
1463
1597
  }, resizeDebounce);
1464
1598
  };
1465
1599
  if (typeof ResizeObserver !== "undefined") {
1466
1600
  const observer = new ResizeObserver(() => {
1467
1601
  const immediate = !observed;
1468
1602
  observed = true;
1603
+ const box = measuredBoxRef.current;
1604
+ if (box && box.width === el.clientWidth && box.height === el.clientHeight) {
1605
+ clear();
1606
+ return;
1607
+ }
1469
1608
  scheduleReflow(immediate);
1470
1609
  });
1471
1610
  observer.observe(el);
@@ -1480,103 +1619,136 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1480
1619
  window.removeEventListener("resize", onWindowResize);
1481
1620
  clear();
1482
1621
  };
1483
- }, [surface, enableResize, resizeDebounce, recompute]);
1484
- return {
1485
- layout,
1486
- pageWidth,
1487
- pageHeight,
1488
- contentHeight,
1489
- elapsedMs,
1490
- recompute,
1491
- pendingRestore
1492
- };
1622
+ }, [element, enableResize, resizeDebounce, trigger]);
1623
+ return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
1624
+ }
1625
+
1626
+ // src/useChapterLayout.ts
1627
+ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1628
+ const optionsRef = useRef8(options);
1629
+ optionsRef.current = options;
1630
+ const source = useMemo4(() => {
1631
+ const chapter = epub?.chapters[chapterIndex];
1632
+ if (!chapter) return null;
1633
+ return {
1634
+ measure: (el) => book.computePageSize(el, optionsRef.current.pageGeometry?.()),
1635
+ layout: () => book.layoutChapter(chapter)
1636
+ };
1637
+ }, [book, epub, chapterIndex]);
1638
+ return useSurfaceLayout(source, surface, {
1639
+ ...options,
1640
+ enableResize: options.enableResize ?? true,
1641
+ resizeDebounce: options.resizeDebounce ?? 120
1642
+ });
1493
1643
  }
1494
1644
 
1495
1645
  // src/useEpub.ts
1496
1646
  import { parseEpub } from "@libraz/mejiro/epub";
1497
- import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef7, useState as useState5 } from "react";
1647
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef9, useState as useState5 } from "react";
1498
1648
  function useEpub(options = {}) {
1499
1649
  const [epub, setEpub] = useState5(null);
1500
1650
  const [loading, setLoading] = useState5(false);
1501
1651
  const [error, setError] = useState5(null);
1502
- const requestIdRef = useRef7(0);
1503
- const onLoadRef = useRef7(options.onLoad);
1652
+ const requests = useRequestToken();
1653
+ const onLoadRef = useRef9(options.onLoad);
1504
1654
  onLoadRef.current = options.onLoad;
1505
- const onErrorRef = useRef7(options.onError);
1655
+ const onErrorRef = useRef9(options.onError);
1506
1656
  onErrorRef.current = options.onError;
1507
- const fetchOptionsRef = useRef7(options.fetchOptions);
1657
+ const fetchOptionsRef = useRef9(options.fetchOptions);
1508
1658
  fetchOptionsRef.current = options.fetchOptions;
1509
- const fetchEpubRef = useRef7(options.fetchEpub);
1659
+ const fetchEpubRef = useRef9(options.fetchEpub);
1510
1660
  fetchEpubRef.current = options.fetchEpub;
1511
- const limitsRef = useRef7(options.limits);
1661
+ const limitsRef = useRef9(options.limits);
1512
1662
  limitsRef.current = options.limits;
1513
- const load = useCallback3(async (read) => {
1514
- const requestId = ++requestIdRef.current;
1515
- setLoading(true);
1516
- setError(null);
1517
- try {
1518
- const book = await parseEpub(await read(), { limits: limitsRef.current });
1519
- if (requestId !== requestIdRef.current) return null;
1520
- setEpub(book);
1521
- onLoadRef.current?.(book);
1522
- return book;
1523
- } catch (err) {
1524
- if (requestId === requestIdRef.current) {
1525
- const nextError = toError(err);
1526
- setError(nextError);
1527
- 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);
1528
1683
  }
1529
- return null;
1530
- } finally {
1531
- if (requestId === requestIdRef.current) setLoading(false);
1532
- }
1533
- }, []);
1534
- const loadBuffer = useCallback3((buffer) => load(async () => buffer), [load]);
1535
- const loadFile = useCallback3(
1684
+ },
1685
+ [requests]
1686
+ );
1687
+ const loadBuffer = useCallback4((buffer) => load(async () => buffer), [load]);
1688
+ const loadFile = useCallback4(
1536
1689
  (file) => load(() => readEpubFile(file, limitsRef.current)),
1537
1690
  [load]
1538
1691
  );
1539
- const loadUrl = useCallback3(
1692
+ const loadUrl = useCallback4(
1540
1693
  (url) => load(
1541
1694
  () => fetchEpubRef.current ? fetchEpubRef.current(url) : fetchEpubBuffer(url, fetchOptionsRef.current)
1542
1695
  ),
1543
1696
  [load]
1544
1697
  );
1545
1698
  const defaultUrl = options.defaultUrl;
1546
- useEffect5(() => {
1547
- if (defaultUrl) void loadUrl(defaultUrl);
1548
- }, [defaultUrl, loadUrl]);
1549
- const replaceEpub = useCallback3((book) => {
1550
- requestIdRef.current++;
1551
- setLoading(false);
1552
- setError(null);
1553
- setEpub(book);
1554
- }, []);
1699
+ useEffect6(() => {
1700
+ if (!defaultUrl) return;
1701
+ void loadUrl(defaultUrl);
1702
+ const requestId = requests.current();
1703
+ return () => {
1704
+ if (requests.current() !== requestId) return;
1705
+ requests.invalidate();
1706
+ setLoading(false);
1707
+ };
1708
+ }, [defaultUrl, loadUrl, requests]);
1709
+ const replaceEpub = useCallback4(
1710
+ (book) => {
1711
+ requests.invalidate();
1712
+ setLoading(false);
1713
+ setError(null);
1714
+ setEpub(book);
1715
+ },
1716
+ [requests]
1717
+ );
1555
1718
  return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
1556
1719
  }
1557
1720
 
1558
1721
  // src/useMejiroBook.ts
1559
1722
  import { MejiroBook } from "@libraz/mejiro/book";
1560
- import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef8, useState as useState6 } from "react";
1723
+ import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef10, useState as useState6 } from "react";
1561
1724
  function useMejiroBook(initial, options = {}) {
1562
- const bookRef = useRef8(null);
1725
+ const bookRef = useRef10(null);
1563
1726
  if (!bookRef.current) bookRef.current = new MejiroBook(initial);
1564
1727
  const [snapshot, setLocal] = useState6(initial);
1565
- const optionsRef = useRef8(options);
1728
+ const optionsRef = useRef10(options);
1566
1729
  optionsRef.current = options;
1567
- const pendingRef = useRef8(null);
1568
- const flush = useCallback4(() => {
1730
+ const pendingRef = useRef10(null);
1731
+ const flushGenerationRef = useRef10(0);
1732
+ const flush = useCallback5(() => {
1569
1733
  const pending = pendingRef.current;
1570
1734
  if (!pending) return;
1571
1735
  pendingRef.current = null;
1572
1736
  if (pending.timer) clearTimeout(pending.timer);
1573
1737
  const { waiters } = pending;
1738
+ const generation = ++flushGenerationRef.current;
1574
1739
  const applied = bookRef.current?.setOptions(pending.patch) ?? Promise.resolve();
1740
+ const syncSnapshot = () => {
1741
+ const book = bookRef.current;
1742
+ if (!book || generation !== flushGenerationRef.current) return;
1743
+ setLocal((prev) => ({ ...prev, ...book.getOptions(), ...pendingRef.current?.patch }));
1744
+ };
1575
1745
  void applied.then(
1576
1746
  () => {
1747
+ syncSnapshot();
1577
1748
  for (const waiter of waiters) waiter.resolve();
1578
1749
  },
1579
1750
  (err) => {
1751
+ syncSnapshot();
1580
1752
  const error = toError(err);
1581
1753
  const handler = optionsRef.current.onError;
1582
1754
  if (handler) {
@@ -1588,7 +1760,7 @@ function useMejiroBook(initial, options = {}) {
1588
1760
  }
1589
1761
  );
1590
1762
  }, []);
1591
- const setOptions = useCallback4(
1763
+ const setOptions = useCallback5(
1592
1764
  (next) => {
1593
1765
  setLocal((prev) => mergeDefined(prev, next));
1594
1766
  const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
@@ -1612,7 +1784,7 @@ function useMejiroBook(initial, options = {}) {
1612
1784
  },
1613
1785
  [flush]
1614
1786
  );
1615
- useEffect6(
1787
+ useEffect7(
1616
1788
  () => () => {
1617
1789
  bookRef.current?.dispose();
1618
1790
  const pending = pendingRef.current;
@@ -1627,7 +1799,7 @@ function useMejiroBook(initial, options = {}) {
1627
1799
  }
1628
1800
 
1629
1801
  // src/useSpread.ts
1630
- import { useCallback as useCallback5, useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as useRef9, useState as useState7 } from "react";
1802
+ import { useCallback as useCallback6, useEffect as useEffect8, useLayoutEffect as useLayoutEffect3, useRef as useRef11, useState as useState7 } from "react";
1631
1803
  function ignoreNavigationKey(e) {
1632
1804
  if (e.defaultPrevented) return true;
1633
1805
  if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
@@ -1657,7 +1829,7 @@ function useSpread(layout, options = {}) {
1657
1829
  const enableKeyboard = options.enableKeyboard ?? true;
1658
1830
  const turnDuration = options.turnDuration ?? 180;
1659
1831
  const single = options.single ?? false;
1660
- const onChangeRef = useRef9(options.onChange);
1832
+ const onChangeRef = useRef11(options.onChange);
1661
1833
  onChangeRef.current = options.onChange;
1662
1834
  const [spreadIdx, setSpreadIdx] = useState7(0);
1663
1835
  const [indexSingle, setIndexSingle] = useState7(single);
@@ -1667,21 +1839,22 @@ function useSpread(layout, options = {}) {
1667
1839
  }
1668
1840
  const [spread, setSpread] = useState7(null);
1669
1841
  const [turning, setTurning] = useState7(false);
1670
- const layoutRef = useRef9(null);
1842
+ const layoutRef = useRef11(null);
1671
1843
  layoutRef.current = layout;
1672
- const spreadIdxRef = useRef9(0);
1844
+ const spreadIdxRef = useRef11(0);
1673
1845
  spreadIdxRef.current = spreadIdx;
1674
- const singleRef = useRef9(single);
1846
+ const singleRef = useRef11(single);
1675
1847
  singleRef.current = single;
1676
- const turnTimerRef = useRef9(null);
1677
- const layoutGenerationRef = useRef9(0);
1678
- const lastEmittedRef = useRef9(null);
1848
+ const turnTimerRef = useRef11(null);
1849
+ const turnTargetRef = useRef11(null);
1850
+ const layoutGenerationRef = useRef11(0);
1851
+ const lastEmittedRef = useRef11(null);
1679
1852
  const totalPages = spread?.totalPages ?? 0;
1680
1853
  const totalSpreads = navigationCount(totalPages, single);
1681
1854
  const firstPage = firstPageOf(spreadIdx, single);
1682
1855
  const layoutSpreadIdx = Math.floor(firstPage / 2);
1683
1856
  const singleSide = single ? firstPage % 2 === 0 ? "right" : "left" : null;
1684
- const refresh = useCallback5(() => {
1857
+ const refresh = useCallback6(() => {
1685
1858
  if (!layoutRef.current) {
1686
1859
  setSpread(null);
1687
1860
  return;
@@ -1695,6 +1868,7 @@ function useSpread(layout, options = {}) {
1695
1868
  clearTimeout(turnTimerRef.current);
1696
1869
  turnTimerRef.current = null;
1697
1870
  }
1871
+ turnTargetRef.current = null;
1698
1872
  setTurning(false);
1699
1873
  setSpreadIdx(0);
1700
1874
  if (!layout) {
@@ -1703,7 +1877,7 @@ function useSpread(layout, options = {}) {
1703
1877
  }
1704
1878
  setSpread(layout.getSpread(0));
1705
1879
  }, [layout]);
1706
- useEffect7(() => {
1880
+ useEffect8(() => {
1707
1881
  if (!layout) return;
1708
1882
  setSpread(layout.getSpread(layoutSpreadIdx));
1709
1883
  const last = lastEmittedRef.current;
@@ -1716,50 +1890,67 @@ function useSpread(layout, options = {}) {
1716
1890
  }
1717
1891
  lastEmittedRef.current = { layout, spreadIdx };
1718
1892
  }, [layout, spreadIdx, layoutSpreadIdx]);
1719
- const goTo = useCallback5(
1893
+ const navigationTarget = useCallback6(() => {
1894
+ const pending = turnTargetRef.current;
1895
+ if (!pending) return spreadIdxRef.current;
1896
+ return indexOfPageIn(firstPageOf(pending.index, pending.single), singleRef.current);
1897
+ }, []);
1898
+ const goTo = useCallback6(
1720
1899
  (index) => {
1721
1900
  if (!layoutRef.current) return;
1722
1901
  const targetSingle = singleRef.current;
1723
1902
  const max = navigationCount(layoutRef.current.totalPages, targetSingle) - 1;
1724
1903
  const target = Math.max(0, Math.min(max, index));
1904
+ if (turnTimerRef.current) {
1905
+ if (target === navigationTarget()) return;
1906
+ clearTimeout(turnTimerRef.current);
1907
+ turnTimerRef.current = null;
1908
+ const landed = navigationTarget();
1909
+ turnTargetRef.current = null;
1910
+ spreadIdxRef.current = landed;
1911
+ setSpreadIdx(landed);
1912
+ setTurning(false);
1913
+ }
1725
1914
  if (target === spreadIdxRef.current) return;
1726
1915
  if (turnDuration > 0) {
1727
1916
  const generation = layoutGenerationRef.current;
1728
- if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
1917
+ turnTargetRef.current = { index: target, single: targetSingle };
1729
1918
  setTurning(true);
1730
1919
  turnTimerRef.current = setTimeout(() => {
1731
1920
  if (generation !== layoutGenerationRef.current) return;
1732
- setSpreadIdx(indexOfPageIn(firstPageOf(target, targetSingle), singleRef.current));
1921
+ setSpreadIdx(navigationTarget());
1733
1922
  setTurning(false);
1923
+ turnTargetRef.current = null;
1734
1924
  turnTimerRef.current = null;
1735
1925
  }, turnDuration);
1736
1926
  } else {
1737
1927
  setSpreadIdx(target);
1738
1928
  }
1739
1929
  },
1740
- [turnDuration]
1930
+ [turnDuration, navigationTarget]
1741
1931
  );
1742
- const setSpreadIndex = useCallback5((index) => {
1932
+ const setSpreadIndex = useCallback6((index) => {
1743
1933
  if (!layoutRef.current) return;
1744
1934
  if (turnTimerRef.current) {
1745
1935
  clearTimeout(turnTimerRef.current);
1746
1936
  turnTimerRef.current = null;
1747
1937
  }
1938
+ turnTargetRef.current = null;
1748
1939
  setTurning(false);
1749
1940
  const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
1750
1941
  setSpreadIdx(Math.max(0, Math.min(max, index)));
1751
1942
  }, []);
1752
- const indexOfPage = useCallback5((pageIdx) => indexOfPageIn(pageIdx, single), [single]);
1753
- const anchorAt = useCallback5(
1943
+ const indexOfPage = useCallback6((pageIdx) => indexOfPageIn(pageIdx, single), [single]);
1944
+ const anchorAt = useCallback6(
1754
1945
  (index) => {
1755
1946
  if (!layout || index < 0 || index >= navigationCount(layout.totalPages, single)) return null;
1756
1947
  return firstTextAnchorFrom(layout, firstPageOf(index, single));
1757
1948
  },
1758
1949
  [layout, single]
1759
1950
  );
1760
- const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
1761
- const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
1762
- useEffect7(() => {
1951
+ const next = useCallback6(() => goTo(navigationTarget() + 1), [goTo, navigationTarget]);
1952
+ const prev = useCallback6(() => goTo(navigationTarget() - 1), [goTo, navigationTarget]);
1953
+ useEffect8(() => {
1763
1954
  if (!enableKeyboard) return;
1764
1955
  const onKey = (e) => {
1765
1956
  if (!layoutRef.current) return;
@@ -1770,7 +1961,7 @@ function useSpread(layout, options = {}) {
1770
1961
  window.addEventListener("keydown", onKey);
1771
1962
  return () => window.removeEventListener("keydown", onKey);
1772
1963
  }, [enableKeyboard, next, prev]);
1773
- useEffect7(
1964
+ useEffect8(
1774
1965
  () => () => {
1775
1966
  if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
1776
1967
  },
@@ -1823,10 +2014,16 @@ function scrollTargetPage(spread, userPage) {
1823
2014
  return userPage != null && spread.indexOfPage(userPage) === spread.spreadIdx ? userPage : spread.firstPage;
1824
2015
  }
1825
2016
  function createLifecycleTracker(sink) {
2017
+ let lastBook = null;
1826
2018
  let last = null;
1827
2019
  let dwell = null;
1828
- return (chapter, spreadIdx, view, controlledSpreadIdx) => {
2020
+ return (book, chapter, spreadIdx, view, controlledSpreadIdx) => {
1829
2021
  if (!view) return;
2022
+ if (book !== lastBook) {
2023
+ lastBook = book;
2024
+ last = null;
2025
+ dwell = null;
2026
+ }
1830
2027
  const totalSpreads = view.total;
1831
2028
  if (controlledSpreadIdx != null) {
1832
2029
  const target = Math.max(0, Math.min(totalSpreads - 1, controlledSpreadIdx));
@@ -1844,6 +2041,56 @@ function createLifecycleTracker(sink) {
1844
2041
  if (spreadIdx === totalSpreads - 1) sink.chapterFinished(chapter);
1845
2042
  };
1846
2043
  }
2044
+ function clampChapter(index, book) {
2045
+ if (!book) return index;
2046
+ return Math.max(0, Math.min(book.chapters.length - 1, index));
2047
+ }
2048
+ function createAnchorResolver(nav) {
2049
+ let pending = null;
2050
+ const settle = () => {
2051
+ const settled = pending;
2052
+ pending = null;
2053
+ settled?.resolve();
2054
+ };
2055
+ const update = (target) => {
2056
+ const current = pending;
2057
+ if (!(current && target.book)) return;
2058
+ if (current.moved) {
2059
+ settle();
2060
+ return;
2061
+ }
2062
+ const { anchor } = current;
2063
+ if (!(anchor.chapter >= 0 && anchor.chapter < target.book.chapters.length)) {
2064
+ settle();
2065
+ return;
2066
+ }
2067
+ if (anchor.chapter !== target.chapter) {
2068
+ if (current.chapterRequested) return;
2069
+ current.chapterRequested = true;
2070
+ nav.goToChapter(anchor.chapter);
2071
+ return;
2072
+ }
2073
+ if (!target.layout) return;
2074
+ const loc = target.layout.locateAnchor({
2075
+ paragraph: anchor.paragraph,
2076
+ charIndex: anchor.charIndex
2077
+ });
2078
+ if (loc && nav.goToPage(loc.pageIdx)) {
2079
+ current.moved = true;
2080
+ return;
2081
+ }
2082
+ settle();
2083
+ };
2084
+ return {
2085
+ request: (anchor, target) => new Promise((resolve) => {
2086
+ settle();
2087
+ pending = { anchor, resolve, chapterRequested: false, moved: false };
2088
+ update(target);
2089
+ }),
2090
+ update,
2091
+ dispose: settle
2092
+ };
2093
+ }
1847
2094
  function MejiroReaderInner(props, ref) {
1848
2095
  const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
1849
2096
  const dialectProp = "dialect" in props ? props.dialect : void 0;
@@ -1890,18 +2137,17 @@ function MejiroReaderInner(props, ref) {
1890
2137
  onPageRead,
1891
2138
  onChapterCompleted
1892
2139
  } = props;
1893
- const surfaceRef = useRef10(null);
1894
- const fileRef = useRef10(null);
2140
+ const surfaceRef = useRef12(null);
2141
+ const fileRef = useRef12(null);
1895
2142
  const [settingsOpen, setSettingsOpen] = useState8(false);
1896
2143
  const [chapterState, setChapterState] = useState8(chapterProp ?? 0);
1897
2144
  const chapterIsUncontrolled = chapterProp == null;
1898
- const chapter = chapterProp ?? chapterState;
1899
2145
  const [chromeHidden, setChromeHidden] = useState8(false);
1900
2146
  const [userScroll, setUserScroll] = useState8(
1901
2147
  null
1902
2148
  );
1903
2149
  const [autoSingle, setAutoSingle] = useState8(false);
1904
- useEffect8(() => {
2150
+ useEffect9(() => {
1905
2151
  if (!settingsOpen) return;
1906
2152
  const onKey = (e2) => {
1907
2153
  if (e2.key === "Escape") setSettingsOpen(false);
@@ -1909,22 +2155,25 @@ function MejiroReaderInner(props, ref) {
1909
2155
  window.addEventListener("keydown", onKey);
1910
2156
  return () => window.removeEventListener("keydown", onKey);
1911
2157
  }, [settingsOpen]);
1912
- useEffect8(() => {
2158
+ useEffect9(() => {
1913
2159
  if (spreadMode !== "auto") return;
1914
2160
  const surface = surfaceRef.current;
1915
2161
  if (!surface) return;
1916
2162
  const update = () => {
1917
2163
  const rect = surface.getBoundingClientRect();
1918
- setAutoSingle(rect.width < rect.height);
2164
+ setAutoSingle(rect.width < (fit === "width" ? window.innerHeight : rect.height));
1919
2165
  };
1920
2166
  update();
1921
- if (typeof ResizeObserver === "undefined") return;
1922
- const observer = new ResizeObserver(update);
1923
- observer.observe(surface);
1924
- return () => observer.disconnect();
1925
- }, [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]);
1926
2175
  const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
1927
- const resolvedGeometry = useMemo4(() => {
2176
+ const resolvedGeometry = useMemo5(() => {
1928
2177
  const columns = effectiveSingle ? 1 : 2;
1929
2178
  const geometry = pageGeometryProp ?? {};
1930
2179
  if (fit !== "width") return mergeDefined({ columns }, geometry);
@@ -1940,37 +2189,46 @@ function MejiroReaderInner(props, ref) {
1940
2189
  geometry
1941
2190
  );
1942
2191
  }, [fit, pageGeometryProp, effectiveSingle]);
1943
- const surfaceAspect = useMemo4(() => {
2192
+ const surfaceAspect = useMemo5(() => {
1944
2193
  const columns = effectiveSingle ? 1 : 2;
1945
2194
  const aspect = resolvedGeometry?.aspect ?? DEFAULT_PAGE_GEOMETRY.aspect;
1946
2195
  return `${columns} / ${aspect}`;
1947
2196
  }, [effectiveSingle, resolvedGeometry]);
1948
- const resolvedOptions = useMemo4(
2197
+ const resolvedOptions = useMemo5(
1949
2198
  () => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
1950
2199
  [optionsProp]
1951
2200
  );
1952
- const onErrorRef = useRef10(onError);
2201
+ const onErrorRef = useRef12(onError);
1953
2202
  onErrorRef.current = onError;
1954
- const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
2203
+ const reportError = useCallback7((error) => onErrorRef.current?.(error), []);
1955
2204
  const {
1956
2205
  book,
1957
2206
  options: bookOptions,
1958
- setOptions
2207
+ setOptions: applyBookOptions
1959
2208
  } = useMejiroBook(resolvedOptions, {
1960
2209
  debounceMs: OPTIONS_DEBOUNCE_MS,
1961
2210
  onError: reportError
1962
2211
  });
2212
+ const lastApplyRef = useRef12(Promise.resolve());
2213
+ const setOptions = useCallback7(
2214
+ (partial) => {
2215
+ const applied = applyBookOptions(partial);
2216
+ lastApplyRef.current = applied;
2217
+ return applied;
2218
+ },
2219
+ [applyBookOptions]
2220
+ );
1963
2221
  const optionsPropKey = JSON.stringify(resolvedOptions);
1964
- const syncedOptionsPropKeyRef = useRef10(optionsPropKey);
1965
- const resolvedOptionsRef = useRef10(resolvedOptions);
2222
+ const syncedOptionsPropKeyRef = useRef12(optionsPropKey);
2223
+ const resolvedOptionsRef = useRef12(resolvedOptions);
1966
2224
  resolvedOptionsRef.current = resolvedOptions;
1967
- useEffect8(() => {
2225
+ useEffect9(() => {
1968
2226
  if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
1969
2227
  syncedOptionsPropKeyRef.current = optionsPropKey;
1970
2228
  void setOptions(resolvedOptionsRef.current);
1971
2229
  }, [optionsPropKey, setOptions]);
1972
2230
  const manuscriptSourceKey = manuscriptKey(manuscriptProp);
1973
- const synthesizedEpub = useMemo4(() => {
2231
+ const synthesizedEpub = useMemo5(() => {
1974
2232
  if (manuscriptSourceKey === void 0) return null;
1975
2233
  const chapters = JSON.parse(manuscriptSourceKey);
1976
2234
  return manuscriptToEpubBook(chapters, { dialect: dialectProp });
@@ -1983,7 +2241,6 @@ function MejiroReaderInner(props, ref) {
1983
2241
  limits,
1984
2242
  fetchEpub: fetchEpubFn,
1985
2243
  onLoad: (b) => {
1986
- if (chapterProp == null) setChapterState(0);
1987
2244
  clearImagesRef.current();
1988
2245
  onLoad?.(b);
1989
2246
  }
@@ -1991,18 +2248,43 @@ function MejiroReaderInner(props, ref) {
1991
2248
  const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
1992
2249
  const isManuscriptSource = manuscriptProp !== void 0;
1993
2250
  const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
1994
- useEffect8(() => {
2251
+ const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : bookIdentity(epubProp);
2252
+ const bookKey = controlled ? sourceKey : epubCtx.epub;
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]);
2273
+ const filePicker = enableDropZone && !controlled;
2274
+ useEffect9(() => {
1995
2275
  if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
1996
2276
  }, [epubCtx.error]);
1997
- const pageGeometryRef = useRef10(resolvedGeometry);
2277
+ const pageGeometryRef = useRef12(resolvedGeometry);
1998
2278
  pageGeometryRef.current = resolvedGeometry;
1999
- const spreadAnchorRef = useRef10(() => null);
2000
- const spreadIdxPropRef = useRef10(spreadIdxProp);
2279
+ const spreadAnchorRef = useRef12(() => null);
2280
+ const spreadIdxPropRef = useRef12(spreadIdxProp);
2001
2281
  spreadIdxPropRef.current = spreadIdxProp;
2002
- const visibleAnchorRef = useRef10(
2282
+ const visibleAnchorRef = useRef12(
2003
2283
  null
2004
2284
  );
2005
- 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, {
2006
2288
  pageGeometry: () => pageGeometryRef.current,
2007
2289
  // Preserve the reading position across a reflow re-layout (size / option
2008
2290
  // changes), but only in uncontrolled mode — when `spreadIdx` is controlled
@@ -2012,10 +2294,13 @@ function MejiroReaderInner(props, ref) {
2012
2294
  const visible = visibleAnchorRef.current;
2013
2295
  if (visible?.layout === layout2) return visible.anchor;
2014
2296
  return spreadAnchorRef.current();
2015
- }
2297
+ },
2298
+ onError: reportError
2016
2299
  });
2017
- const spreadChangedRef = useRef10(void 0);
2300
+ const reportedSpreadIdxRef = useRef12(null);
2301
+ const spreadChangedRef = useRef12(void 0);
2018
2302
  spreadChangedRef.current = (i) => {
2303
+ reportedSpreadIdxRef.current = i;
2019
2304
  onSpreadChange?.(i);
2020
2305
  onSpreadIdxChange?.(i);
2021
2306
  };
@@ -2025,9 +2310,9 @@ function MejiroReaderInner(props, ref) {
2025
2310
  onChange: (i) => spreadChangedRef.current?.(i)
2026
2311
  });
2027
2312
  spreadAnchorRef.current = () => spreadCtx.anchorAt(spreadCtx.spreadIdx);
2028
- const indexOfPageRef = useRef10(spreadCtx.indexOfPage);
2313
+ const indexOfPageRef = useRef12(spreadCtx.indexOfPage);
2029
2314
  indexOfPageRef.current = spreadCtx.indexOfPage;
2030
- const anchorSingleRef = useRef10(effectiveSingle);
2315
+ const anchorSingleRef = useRef12(effectiveSingle);
2031
2316
  useLayoutEffect4(() => {
2032
2317
  const l = layoutCtx.layout;
2033
2318
  const flipped = anchorSingleRef.current !== effectiveSingle;
@@ -2035,7 +2320,7 @@ function MejiroReaderInner(props, ref) {
2035
2320
  if (flipped && l && visibleAnchorRef.current?.layout === l) return;
2036
2321
  visibleAnchorRef.current = l ? { layout: l, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
2037
2322
  }, [layoutCtx.layout, spreadCtx.spreadIdx, spreadCtx.anchorAt, effectiveSingle]);
2038
- const setSpreadRef = useRef10(spreadCtx.setSpread);
2323
+ const setSpreadRef = useRef12(spreadCtx.setSpread);
2039
2324
  setSpreadRef.current = spreadCtx.setSpread;
2040
2325
  useLayoutEffect4(() => {
2041
2326
  const anchor = layoutCtx.pendingRestore.current;
@@ -2046,18 +2331,16 @@ function MejiroReaderInner(props, ref) {
2046
2331
  visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
2047
2332
  }, [layoutCtx.layout]);
2048
2333
  const optionsKey = reflowOptionsKey(bookOptions);
2049
- const optionsKeyRef = useRef10(optionsKey);
2050
- const bookOptionsRef = useRef10(bookOptions);
2051
- bookOptionsRef.current = bookOptions;
2052
- const recomputeRef = useRef10(layoutCtx.recompute);
2334
+ const optionsKeyRef = useRef12(optionsKey);
2335
+ const recomputeRef = useRef12(layoutCtx.recompute);
2053
2336
  recomputeRef.current = layoutCtx.recompute;
2054
- useEffect8(() => {
2337
+ useEffect9(() => {
2055
2338
  if (optionsKeyRef.current === optionsKey) return;
2056
2339
  optionsKeyRef.current = optionsKey;
2057
2340
  const timer = setTimeout(() => {
2058
2341
  void (async () => {
2059
2342
  try {
2060
- await setOptions({ ...bookOptionsRef.current });
2343
+ await lastApplyRef.current;
2061
2344
  await recomputeRef.current({ blank: false });
2062
2345
  } catch (err) {
2063
2346
  reportError(toError(err));
@@ -2065,10 +2348,10 @@ function MejiroReaderInner(props, ref) {
2065
2348
  })();
2066
2349
  }, OPTIONS_DEBOUNCE_MS);
2067
2350
  return () => clearTimeout(timer);
2068
- }, [optionsKey, setOptions, reportError]);
2351
+ }, [optionsKey, reportError]);
2069
2352
  const geometryKey = JSON.stringify(resolvedGeometry ?? null);
2070
- const geometryKeyRef = useRef10(geometryKey);
2071
- useEffect8(() => {
2353
+ const geometryKeyRef = useRef12(geometryKey);
2354
+ useEffect9(() => {
2072
2355
  if (geometryKeyRef.current === geometryKey) return;
2073
2356
  geometryKeyRef.current = geometryKey;
2074
2357
  void recomputeRef.current({ blank: false });
@@ -2077,7 +2360,7 @@ function MejiroReaderInner(props, ref) {
2077
2360
  defaultX: IMAGE_DEFAULT_X,
2078
2361
  onUpdate: () => spreadCtx.refresh()
2079
2362
  });
2080
- const annotationRects = useMemo4(() => {
2363
+ const annotationRects = useMemo5(() => {
2081
2364
  if (!(annotations && layoutCtx.layout)) return [];
2082
2365
  const result = [];
2083
2366
  for (const annotation of annotations) {
@@ -2090,30 +2373,31 @@ function MejiroReaderInner(props, ref) {
2090
2373
  }
2091
2374
  return result;
2092
2375
  }, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
2093
- const clearImagesRef = useRef10(imageCtx.clearImages);
2376
+ const clearImagesRef = useRef12(imageCtx.clearImages);
2094
2377
  clearImagesRef.current = imageCtx.clearImages;
2095
- const onLoadRef = useRef10(onLoad);
2378
+ const onLoadRef = useRef12(onLoad);
2096
2379
  onLoadRef.current = onLoad;
2097
- const eRef = useRef10(e);
2380
+ const eRef = useRef12(e);
2098
2381
  eRef.current = e;
2099
- const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
2100
- useEffect8(() => {
2382
+ useEffect9(() => {
2101
2383
  if (!controlled) return;
2102
- if (chapterIsUncontrolled) setChapterState(0);
2103
2384
  clearImagesRef.current();
2104
2385
  book.clearCache();
2105
2386
  if (eRef.current) onLoadRef.current?.(eRef.current);
2106
- }, [controlled, sourceKey, book, chapterIsUncontrolled]);
2107
- const imagesChapterRef = useRef10(chapter);
2108
- useEffect8(() => {
2387
+ }, [controlled, sourceKey, book]);
2388
+ const imagesChapterRef = useRef12(chapter);
2389
+ useEffect9(() => {
2109
2390
  if (imagesChapterRef.current === chapter) return;
2110
2391
  imagesChapterRef.current = chapter;
2111
2392
  clearImagesRef.current();
2112
2393
  }, [chapter]);
2113
- const spreadGoToRef = useRef10(spreadCtx.goTo);
2394
+ const spreadGoToRef = useRef12(spreadCtx.goTo);
2114
2395
  spreadGoToRef.current = spreadCtx.goTo;
2115
- const appliedSpreadIdxPropRef = useRef10(spreadIdxProp);
2116
- useEffect8(() => {
2396
+ const appliedSpreadIdxPropRef = useRef12(spreadIdxProp);
2397
+ const [hostTurn, setHostTurn] = useState8(0);
2398
+ useEffect9(() => {
2399
+ const reported = reportedSpreadIdxRef.current;
2400
+ reportedSpreadIdxRef.current = null;
2117
2401
  if (spreadIdxProp == null) {
2118
2402
  appliedSpreadIdxPropRef.current = spreadIdxProp;
2119
2403
  return;
@@ -2122,23 +2406,32 @@ function MejiroReaderInner(props, ref) {
2122
2406
  appliedSpreadIdxPropRef.current = spreadIdxProp;
2123
2407
  if (spreadCtx.spreadIdx === spreadIdxProp) return;
2124
2408
  if (propChanged) spreadGoToRef.current(spreadIdxProp);
2409
+ else if (reported === spreadCtx.spreadIdx) setHostTurn((n) => n + 1);
2125
2410
  else setSpreadRef.current(spreadIdxProp);
2126
- }, [spreadIdxProp, spreadCtx.spreadIdx]);
2411
+ }, [spreadIdxProp, spreadCtx.spreadIdx, hostTurn]);
2412
+ const restoredForRef = useRef12(null);
2127
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;
2128
2418
  const next = spreadIdxPropRef.current;
2129
- if (next == null || !layoutCtx.layout) return;
2419
+ if (next == null) return;
2130
2420
  setSpreadRef.current(next);
2131
2421
  }, [layoutCtx.layout]);
2132
- const onChapter = useCallback6(
2133
- (i) => {
2422
+ const onChapter = useCallback7(
2423
+ (index) => {
2424
+ const i = clampChapter(index, e);
2134
2425
  if (i === chapter) return;
2135
2426
  if (chapterProp == null) setChapterState(i);
2136
2427
  onChapterChange?.(i);
2137
2428
  },
2138
- [chapter, chapterProp, onChapterChange]
2429
+ [chapter, chapterProp, onChapterChange, e]
2139
2430
  );
2140
- const listenersRef = useRef10(/* @__PURE__ */ new Map());
2141
- const emit = useCallback6(
2431
+ const onChapterRef = useRef12(onChapter);
2432
+ onChapterRef.current = onChapter;
2433
+ const listenersRef = useRef12(/* @__PURE__ */ new Map());
2434
+ const emit = useCallback7(
2142
2435
  (event, payload) => {
2143
2436
  const set = listenersRef.current.get(event);
2144
2437
  if (!set) return;
@@ -2146,44 +2439,40 @@ function MejiroReaderInner(props, ref) {
2146
2439
  },
2147
2440
  []
2148
2441
  );
2149
- const pendingAnchorRef = useRef10(null);
2150
- const layoutOwnerRef = useRef10({
2442
+ const layoutOwnerRef = useRef12({
2151
2443
  layout: null,
2152
- chapter
2444
+ chapter,
2445
+ book: bookKey
2153
2446
  });
2154
2447
  if (layoutOwnerRef.current.layout !== layoutCtx.layout) {
2155
- layoutOwnerRef.current = { layout: layoutCtx.layout, chapter };
2448
+ layoutOwnerRef.current = { layout: layoutCtx.layout, chapter, book: bookKey };
2156
2449
  }
2157
- const layout = layoutOwnerRef.current.chapter === chapter ? layoutCtx.layout : null;
2158
- const tryApplyPendingAnchor = useCallback6(() => {
2159
- const pending = pendingAnchorRef.current;
2160
- if (!pending) return;
2161
- if (pending.anchor.chapter !== chapter) return;
2162
- if (!layout) return;
2163
- const loc = layout.locateAnchor({
2164
- paragraph: pending.anchor.paragraph,
2165
- charIndex: pending.anchor.charIndex
2166
- });
2167
- if (!loc) return;
2168
- pendingAnchorRef.current = null;
2169
- spreadGoToRef.current(indexOfPageRef.current(loc.pageIdx));
2170
- pending.resolve();
2171
- }, [chapter, layout]);
2172
- useEffect8(() => {
2173
- tryApplyPendingAnchor();
2174
- }, [tryApplyPendingAnchor]);
2175
- useEffect8(
2176
- () => () => {
2177
- pendingAnchorRef.current?.resolve();
2178
- pendingAnchorRef.current = null;
2179
- },
2180
- []
2181
- );
2182
- const onPageReadRef = useRef10(onPageRead);
2450
+ const owner = layoutOwnerRef.current;
2451
+ const layout = owner.chapter === chapter && owner.book === bookKey ? layoutCtx.layout : null;
2452
+ const anchorTarget = { book: epubCtx.loading ? null : e, chapter, layout };
2453
+ const anchorTargetRef = useRef12(anchorTarget);
2454
+ anchorTargetRef.current = anchorTarget;
2455
+ const spreadIdxNowRef = useRef12(spreadCtx.spreadIdx);
2456
+ spreadIdxNowRef.current = spreadCtx.spreadIdx;
2457
+ const anchorResolverRef = useRef12(null);
2458
+ anchorResolverRef.current ??= createAnchorResolver({
2459
+ goToChapter: (i) => onChapterRef.current(i),
2460
+ goToPage: (pageIdx) => {
2461
+ const target = indexOfPageRef.current(pageIdx);
2462
+ if (target === spreadIdxNowRef.current) return false;
2463
+ setSpreadRef.current(target);
2464
+ return true;
2465
+ }
2466
+ });
2467
+ useEffect9(() => {
2468
+ anchorResolverRef.current?.update(anchorTargetRef.current);
2469
+ }, [anchorTarget.book, chapter, layout, spreadCtx.spreadIdx]);
2470
+ useEffect9(() => () => anchorResolverRef.current?.dispose(), []);
2471
+ const onPageReadRef = useRef12(onPageRead);
2183
2472
  onPageReadRef.current = onPageRead;
2184
- const onChapterCompletedRef = useRef10(onChapterCompleted);
2473
+ const onChapterCompletedRef = useRef12(onChapterCompleted);
2185
2474
  onChapterCompletedRef.current = onChapterCompleted;
2186
- const trackLifecycleRef = useRef10(null);
2475
+ const trackLifecycleRef = useRef12(null);
2187
2476
  trackLifecycleRef.current ??= createLifecycleTracker({
2188
2477
  spreadChanged: (ch, spreadIdx) => emit("spreadChanged", { chapter: ch, spreadIdx }),
2189
2478
  chapterFinished: (ch) => {
@@ -2192,10 +2481,11 @@ function MejiroReaderInner(props, ref) {
2192
2481
  },
2193
2482
  pageRead: (anchor, dwellMs) => onPageReadRef.current?.(anchor, dwellMs)
2194
2483
  });
2195
- useEffect8(() => {
2484
+ useEffect9(() => {
2196
2485
  const view = layout ? { total: spreadCtx.totalSpreads, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
2197
- trackLifecycleRef.current?.(chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
2486
+ trackLifecycleRef.current?.(bookKey, chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
2198
2487
  }, [
2488
+ bookKey,
2199
2489
  chapter,
2200
2490
  spreadCtx.spreadIdx,
2201
2491
  spreadCtx.totalSpreads,
@@ -2203,8 +2493,8 @@ function MejiroReaderInner(props, ref) {
2203
2493
  layout,
2204
2494
  spreadIdxProp
2205
2495
  ]);
2206
- const prevTurningRef = useRef10(false);
2207
- useEffect8(() => {
2496
+ const prevTurningRef = useRef12(false);
2497
+ useEffect9(() => {
2208
2498
  if (spreadCtx.turning && !prevTurningRef.current) {
2209
2499
  emit("turnStart", { from: spreadCtx.spreadIdx });
2210
2500
  } else if (!spreadCtx.turning && prevTurningRef.current) {
@@ -2225,12 +2515,7 @@ function MejiroReaderInner(props, ref) {
2225
2515
  totalPages: spreadCtx.totalPages,
2226
2516
  totalSpreads: spreadCtx.totalSpreads
2227
2517
  }),
2228
- goToAnchor: (anchor) => new Promise((resolve) => {
2229
- pendingAnchorRef.current?.resolve();
2230
- pendingAnchorRef.current = { anchor, resolve };
2231
- if (anchor.chapter !== chapter) onChapter(anchor.chapter);
2232
- tryApplyPendingAnchor();
2233
- }),
2518
+ goToAnchor: (anchor) => anchorResolverRef.current?.request(anchor, anchorTargetRef.current) ?? Promise.resolve(),
2234
2519
  getAnchor: () => {
2235
2520
  if (!layout) return null;
2236
2521
  const inCh = spreadCtx.anchorAt(spreadCtx.spreadIdx);
@@ -2269,8 +2554,7 @@ function MejiroReaderInner(props, ref) {
2269
2554
  onChapter,
2270
2555
  chapter,
2271
2556
  layout,
2272
- setOptions,
2273
- tryApplyPendingAnchor
2557
+ setOptions
2274
2558
  ]
2275
2559
  );
2276
2560
  const editable = {
@@ -2293,7 +2577,7 @@ function MejiroReaderInner(props, ref) {
2293
2577
  const layoutReady = e && spreadCtx.spread && layoutCtx.layout && layoutCtx.pageWidth > 0;
2294
2578
  const themeName = typeof theme === "string" ? theme : theme.name;
2295
2579
  const themeOverride = typeof theme === "string" ? void 0 : theme.override;
2296
- const themeStyle = useMemo4(() => {
2580
+ const themeStyle = useMemo5(() => {
2297
2581
  const pad = pageGeometryProp?.padding;
2298
2582
  const styleVars = {};
2299
2583
  if (pad?.x != null) styleVars["--mejiro-page-pad-x"] = `${pad.x}px`;
@@ -2325,7 +2609,7 @@ function MejiroReaderInner(props, ref) {
2325
2609
  fontLabel
2326
2610
  }
2327
2611
  ),
2328
- enableDropZone && /* @__PURE__ */ jsx13(
2612
+ filePicker && /* @__PURE__ */ jsx13(
2329
2613
  "button",
2330
2614
  {
2331
2615
  type: "button",
@@ -2363,11 +2647,9 @@ function MejiroReaderInner(props, ref) {
2363
2647
  ] })
2364
2648
  ] }) : null;
2365
2649
  const currentSpread = spreadCtx.layoutSpreadIdx;
2366
- const rightPage = currentSpread * 2 + 1;
2367
2650
  const leftPage = currentSpread * 2 + 2;
2368
2651
  const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
2369
- const showRightNum = pageNumbers === "both" || pageNumbers === "right";
2370
- const showLeftNum = pageNumbers === "both" || pageNumbers === "left";
2652
+ const pageIndicator = enablePageIndicator ? /* @__PURE__ */ jsx13(MejiroPageIndicator, { current: spreadCtx.spreadIdx + 1, total: spreadCtx.totalSpreads }) : null;
2371
2653
  return /* @__PURE__ */ jsx13(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs8(
2372
2654
  "div",
2373
2655
  {
@@ -2393,7 +2675,7 @@ function MejiroReaderInner(props, ref) {
2393
2675
  /* @__PURE__ */ jsxs8("div", { className: `mejiro-reader-body${showChapterPanel ? " has-chapter-panel" : ""}`, children: [
2394
2676
  showChapterPanel && /* @__PURE__ */ jsx13(MejiroChapterNav, { epub: e, chapter, onChange: onChapter, variant: "panel" }),
2395
2677
  /* @__PURE__ */ jsxs8("div", { ref: surfaceRef, className: "mejiro-reader-surface", children: [
2396
- !(e || epubCtx.loading) && enableDropZone && /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
2678
+ !(e || epubCtx.loading) && filePicker && /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
2397
2679
  epubCtx.loading && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-loading", children: resolvedMessages.loading }),
2398
2680
  !layoutReady && fallback && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-fallback", children: fallback }),
2399
2681
  layoutReady && mode === "scroll" && layoutCtx.layout && /* @__PURE__ */ jsx13(
@@ -2412,18 +2694,25 @@ function MejiroReaderInner(props, ref) {
2412
2694
  ),
2413
2695
  onVisiblePageChange: (pageIdx, source) => {
2414
2696
  if (source === "programmatic") return;
2415
- 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
+ }
2416
2703
  const target = spreadCtx.indexOfPage(pageIdx);
2417
2704
  if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
2418
2705
  },
2419
2706
  selectionRects: annotationRects.length ? annotationRects : void 0,
2420
2707
  spreadIdx: spreadCtx.layoutSpreadIdx,
2421
2708
  images: imageCtx.currentImages,
2709
+ pageNumbers,
2422
2710
  onImagePointerDown: imageCtx.onOverlayPointerDown,
2423
2711
  onImageResizePointerDown: imageCtx.onResizePointerDown,
2424
2712
  onImageClose: imageCtx.removeImage
2425
2713
  }
2426
2714
  ),
2715
+ layoutReady && mode === "scroll" && layoutCtx.layout && pageIndicator,
2427
2716
  layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
2428
2717
  MejiroSpread,
2429
2718
  {
@@ -2441,20 +2730,14 @@ function MejiroReaderInner(props, ref) {
2441
2730
  turning: spreadCtx.turning,
2442
2731
  rightHeader: {
2443
2732
  title: runningTitleRight,
2444
- pageNumber: showRightNum ? rightPage : null
2733
+ pageNumber: pageNumberFor2(pageNumbers, currentSpread * 2)
2445
2734
  },
2446
2735
  leftHeader: {
2447
2736
  title: runningTitleLeft,
2448
- pageNumber: showLeft && showLeftNum ? leftPage : null
2737
+ pageNumber: showLeft ? pageNumberFor2(pageNumbers, currentSpread * 2 + 1) : null
2449
2738
  },
2450
2739
  images: imageCtx.currentImages,
2451
- indicator: enablePageIndicator ? /* @__PURE__ */ jsx13(
2452
- MejiroPageIndicator,
2453
- {
2454
- current: spreadCtx.spreadIdx + 1,
2455
- total: spreadCtx.totalSpreads
2456
- }
2457
- ) : null,
2740
+ indicator: pageIndicator,
2458
2741
  onPrev: spreadCtx.prev,
2459
2742
  onNext: spreadCtx.next,
2460
2743
  onImagePointerDown: imageCtx.onOverlayPointerDown,
@@ -2476,6 +2759,7 @@ function MejiroReaderInner(props, ref) {
2476
2759
  hidden: true,
2477
2760
  onChange: (ev) => {
2478
2761
  const file = ev.target.files?.[0];
2762
+ ev.target.value = "";
2479
2763
  if (file) void epubCtx.loadFile(file);
2480
2764
  }
2481
2765
  }
@@ -2493,6 +2777,55 @@ MejiroReader.displayName = "MejiroReader";
2493
2777
 
2494
2778
  // src/MejiroEditor.tsx
2495
2779
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
2780
+ function clampRubyRange(start, end, length) {
2781
+ if (length <= 0) return null;
2782
+ const from = Math.max(0, Math.min(start, length - 1));
2783
+ return { start: from, end: Math.max(from + 1, Math.min(end, length)) };
2784
+ }
2785
+ function commitPendingEdit(editor, chapterIndex, paragraphIndex, text, ruby) {
2786
+ const paragraphs = editor.book.chapters[chapterIndex]?.paragraphs;
2787
+ if (!paragraphs?.[paragraphIndex]) return false;
2788
+ const textChanged = text !== paragraphs[paragraphIndex].text;
2789
+ const rubyText = ruby?.rubyText.trim();
2790
+ const range = ruby ? clampRubyRange(ruby.start, ruby.end, [...text].length) : null;
2791
+ if (!(rubyText && range)) {
2792
+ if (textChanged) editor.updateParagraph(chapterIndex, paragraphIndex, { text });
2793
+ return textChanged;
2794
+ }
2795
+ editor.transaction(() => {
2796
+ if (textChanged) editor.updateParagraph(chapterIndex, paragraphIndex, { text });
2797
+ const committed = editor.book.chapters[chapterIndex].paragraphs[paragraphIndex];
2798
+ const newRuby = {
2799
+ kind: "ruby",
2800
+ startIndex: range.start,
2801
+ endIndex: range.end,
2802
+ rubyText,
2803
+ type: range.end - range.start === 1 ? "mono" : "group"
2804
+ };
2805
+ const inlineAnnotations = [
2806
+ ...committed.inlineAnnotations.filter(
2807
+ (ann) => ann.kind !== "ruby" || ann.endIndex <= range.start || ann.startIndex >= range.end
2808
+ ),
2809
+ newRuby
2810
+ ].sort((a, b) => a.startIndex - b.startIndex);
2811
+ editor.updateParagraph(chapterIndex, paragraphIndex, { inlineAnnotations });
2812
+ });
2813
+ return true;
2814
+ }
2815
+ function downloadEpub(buffer, filename) {
2816
+ const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2817
+ const a = document.createElement("a");
2818
+ a.href = url;
2819
+ a.download = filename;
2820
+ a.hidden = true;
2821
+ document.body.append(a);
2822
+ try {
2823
+ a.click();
2824
+ } finally {
2825
+ a.remove();
2826
+ setTimeout(() => URL.revokeObjectURL(url), 0);
2827
+ }
2828
+ }
2496
2829
  function utf16ToCodepoint(text, utf16Offset) {
2497
2830
  let cp = 0;
2498
2831
  let i = 0;
@@ -2510,6 +2843,14 @@ function watermarkedBook(book, watermark) {
2510
2843
  const copy = cloneEditableEpubBook(book);
2511
2844
  const text = `${WATERMARK_PREFIX} ${watermark.text}`;
2512
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
+ }
2513
2854
  chapter.blocks.unshift({
2514
2855
  kind: "paragraph",
2515
2856
  id: watermarkBlockId(chapter),
@@ -2517,10 +2858,19 @@ function watermarkedBook(book, watermark) {
2517
2858
  inlineAnnotations: []
2518
2859
  });
2519
2860
  chapter.paragraphs.unshift({ text, inlineAnnotations: [] });
2520
- chapter.isDirty = true;
2521
2861
  }
2522
2862
  return copy;
2523
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
+ }
2524
2874
  function watermarkBlockId(chapter) {
2525
2875
  const used = new Set(chapter.blocks.map((block) => block.id));
2526
2876
  let id = WATERMARK_BLOCK_ID;
@@ -2544,9 +2894,9 @@ function MejiroEditor({
2544
2894
  onError
2545
2895
  }) {
2546
2896
  const messages = useI18n();
2547
- const imageInputRef = useRef11(null);
2548
- const textareaRef = useRef11(null);
2549
- const readerRef = useRef11(null);
2897
+ const imageInputRef = useRef13(null);
2898
+ const textareaRef = useRef13(null);
2899
+ const readerRef = useRef13(null);
2550
2900
  const [editor, setEditor] = useState9(null);
2551
2901
  const [loading, setLoading] = useState9(false);
2552
2902
  const [error, setError] = useState9(null);
@@ -2556,86 +2906,89 @@ function MejiroEditor({
2556
2906
  const [rubyStart, setRubyStart] = useState9(0);
2557
2907
  const [rubyEnd, setRubyEnd] = useState9(1);
2558
2908
  const [rubyText, setRubyText] = useState9("");
2559
- const loadRequestIdRef = useRef11(0);
2560
- const onLoadRef = useRef11(onLoad);
2561
- const onErrorRef = useRef11(onError);
2562
- const limitsRef = useRef11(limits);
2909
+ const request = useRequestToken();
2910
+ const onLoadRef = useRef13(onLoad);
2911
+ const onErrorRef = useRef13(onError);
2912
+ const limitsRef = useRef13(limits);
2563
2913
  limitsRef.current = limits;
2564
2914
  const book = editor?.book ?? null;
2565
2915
  const chapter = book?.chapters[selection.chapter] ?? null;
2566
2916
  const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
2567
- const textDirty = paragraph ? text !== paragraph.text : false;
2568
- const previewBook = useMemo5(() => {
2917
+ const sceneBreak = chapter ? paragraphBlock(chapter, selection.paragraph)?.paragraphKind === "sceneBreak" : false;
2918
+ const rubyRange = clampRubyRange(rubyStart, rubyEnd, [...text].length);
2919
+ const previewBook = useMemo6(() => {
2569
2920
  void revision;
2570
- return book ? cloneEditableEpubBook(book) : null;
2571
- }, [book, revision]);
2572
- useEffect9(() => {
2921
+ return book && editor ? markBookIdentity(cloneEditableEpubBook(book), editor) : null;
2922
+ }, [book, editor, revision]);
2923
+ useEffect10(() => {
2573
2924
  onLoadRef.current = onLoad;
2574
2925
  }, [onLoad]);
2575
- useEffect9(() => {
2926
+ useEffect10(() => {
2576
2927
  onErrorRef.current = onError;
2577
2928
  }, [onError]);
2578
- const reportError = useCallback7((nextError) => {
2929
+ const reportError = useCallback8((nextError) => {
2579
2930
  setError(nextError);
2580
2931
  onErrorRef.current?.(nextError);
2581
2932
  }, []);
2582
- const loadBufferForRequest = useCallback7(
2583
- async (buffer, requestId) => {
2933
+ const clearError = useCallback8(() => setError(null), []);
2934
+ const loadBufferForRequest = useCallback8(
2935
+ async (buffer, token) => {
2584
2936
  setLoading(true);
2585
- setError(null);
2937
+ clearError();
2586
2938
  try {
2587
2939
  const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
2588
- if (requestId !== loadRequestIdRef.current) return;
2940
+ if (!request.isCurrent(token)) return;
2589
2941
  setEditor(next);
2590
2942
  setSelection({ chapter: 0, paragraph: 0 });
2591
2943
  setRevision((value) => value + 1);
2592
2944
  onLoadRef.current?.(next);
2593
2945
  } catch (err) {
2594
- if (requestId === loadRequestIdRef.current) reportError(toError(err));
2946
+ if (request.isCurrent(token)) reportError(toError(err));
2595
2947
  } finally {
2596
- if (requestId === loadRequestIdRef.current) setLoading(false);
2948
+ if (request.isCurrent(token)) setLoading(false);
2597
2949
  }
2598
2950
  },
2599
- [reportError]
2951
+ [clearError, reportError, request]
2600
2952
  );
2601
2953
  async function loadFile(file) {
2602
- const requestId = ++loadRequestIdRef.current;
2954
+ const token = request.begin();
2603
2955
  setLoading(true);
2604
- setError(null);
2956
+ clearError();
2605
2957
  try {
2606
- await loadBufferForRequest(await readEpubFile(file, limitsRef.current), requestId);
2958
+ await loadBufferForRequest(await readEpubFile(file, limitsRef.current), token);
2607
2959
  } catch (err) {
2608
- if (requestId === loadRequestIdRef.current) {
2960
+ if (request.isCurrent(token)) {
2609
2961
  reportError(toError(err));
2610
2962
  setLoading(false);
2611
2963
  }
2612
2964
  }
2613
2965
  }
2614
- useEffect9(() => {
2966
+ useEffect10(() => {
2615
2967
  if (!epubUrl) return;
2616
- let cancelled = false;
2617
- const requestId = ++loadRequestIdRef.current;
2968
+ const token = request.begin();
2618
2969
  void (async () => {
2619
2970
  setLoading(true);
2620
- setError(null);
2971
+ clearError();
2621
2972
  try {
2622
2973
  const buffer = await fetchEpubBuffer(epubUrl);
2623
- if (cancelled || requestId !== loadRequestIdRef.current) return;
2624
- await loadBufferForRequest(buffer, requestId);
2974
+ if (!request.isCurrent(token)) return;
2975
+ await loadBufferForRequest(buffer, token);
2625
2976
  } catch (err) {
2626
- if (!cancelled && requestId === loadRequestIdRef.current) reportError(toError(err));
2977
+ if (request.isCurrent(token)) reportError(toError(err));
2627
2978
  } finally {
2628
- if (!cancelled && requestId === loadRequestIdRef.current) setLoading(false);
2979
+ if (request.isCurrent(token)) setLoading(false);
2629
2980
  }
2630
2981
  })();
2631
2982
  return () => {
2632
- cancelled = true;
2983
+ if (!request.isCurrent(token)) return;
2984
+ request.invalidate();
2985
+ setLoading(false);
2633
2986
  };
2634
- }, [epubUrl, loadBufferForRequest, reportError]);
2635
- const syncedRef = useRef11(
2987
+ }, [epubUrl, clearError, loadBufferForRequest, reportError, request]);
2988
+ const syncedRef = useRef13(
2636
2989
  null
2637
2990
  );
2638
- useEffect9(() => {
2991
+ useEffect10(() => {
2639
2992
  const nextText = paragraph?.text ?? "";
2640
2993
  const target = `${selection.chapter}:${selection.paragraph}`;
2641
2994
  const prev = syncedRef.current;
@@ -2649,7 +3002,7 @@ function MejiroEditor({
2649
3002
  setRubyEnd(Math.min(1, [...nextText].length));
2650
3003
  setRubyText("");
2651
3004
  }, [editor, selection.chapter, selection.paragraph, paragraph]);
2652
- useEffect9(() => {
3005
+ useEffect10(() => {
2653
3006
  if (!previewBook) return;
2654
3007
  void readerRef.current?.goToAnchor({
2655
3008
  chapter: selection.chapter,
@@ -2659,7 +3012,7 @@ function MejiroEditor({
2659
3012
  }, [previewBook, selection.chapter, selection.paragraph]);
2660
3013
  function selectParagraph(chapterIndex, paragraphIndex) {
2661
3014
  if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
2662
- if (textDirty) applyText();
3015
+ commitEdit();
2663
3016
  setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
2664
3017
  }
2665
3018
  function captureRubyRange() {
@@ -2672,41 +3025,22 @@ function MejiroEditor({
2672
3025
  setRubyStart(start);
2673
3026
  setRubyEnd(Math.max(start + 1, end));
2674
3027
  }
2675
- function applyText() {
3028
+ function commitEdit(ruby) {
2676
3029
  if (!editor) return;
2677
- editor.updateParagraph(selection.chapter, selection.paragraph, { text });
2678
- setRevision((value) => value + 1);
3030
+ if (commitPendingEdit(editor, selection.chapter, selection.paragraph, text, ruby)) {
3031
+ setRevision((value) => value + 1);
3032
+ }
2679
3033
  }
2680
3034
  function applyRuby() {
2681
- if (!(editor && paragraph && rubyText.trim()) || textDirty) return;
2682
- const len = [...text].length;
2683
- const start = Math.max(0, Math.min(rubyStart, len));
2684
- const end = Math.max(start + 1, Math.min(rubyEnd, len));
2685
- const newRuby = {
2686
- kind: "ruby",
2687
- startIndex: start,
2688
- endIndex: end,
2689
- rubyText: rubyText.trim(),
2690
- type: end - start === 1 ? "mono" : "group"
2691
- };
2692
- const nextInline = [
2693
- ...paragraph.inlineAnnotations.filter(
2694
- (ann) => ann.endIndex <= start || ann.startIndex >= end
2695
- ),
2696
- newRuby
2697
- ].sort((a, b) => a.startIndex - b.startIndex);
2698
- editor.updateParagraph(selection.chapter, selection.paragraph, {
2699
- text,
2700
- inlineAnnotations: nextInline
2701
- });
3035
+ if (!(rubyRange && rubyText.trim())) return;
3036
+ commitEdit({ start: rubyStart, end: rubyEnd, rubyText });
2702
3037
  setRubyText("");
2703
- setRevision((value) => value + 1);
2704
3038
  }
2705
3039
  async function addImage(file) {
2706
3040
  await withErrorReporting(async () => {
2707
3041
  if (!(editor && chapter)) return;
2708
3042
  const chapterIndex = selection.chapter;
2709
- const afterBlockId = paragraphBlockId(chapter, selection.paragraph);
3043
+ const afterBlockId = paragraphBlock(chapter, selection.paragraph)?.id;
2710
3044
  const data = await file.arrayBuffer();
2711
3045
  editor.addImage(chapterIndex, {
2712
3046
  filename: file.name,
@@ -2716,11 +3050,13 @@ function MejiroEditor({
2716
3050
  afterBlockId
2717
3051
  });
2718
3052
  setRevision((value) => value + 1);
3053
+ clearError();
2719
3054
  }, reportError);
2720
3055
  }
2721
3056
  async function exportEpub() {
2722
3057
  await withErrorReporting(async () => {
2723
3058
  if (!editor) return;
3059
+ commitEdit();
2724
3060
  const watermark = exportPolicy?.watermark;
2725
3061
  const source = watermark ? watermarkedBook(editor.book, watermark) : editor.book;
2726
3062
  let buffer = await exportEditableEpub(source, assetResolver ? { assetResolver } : void 0);
@@ -2728,15 +3064,20 @@ function MejiroEditor({
2728
3064
  const decision = await onBeforeExport?.(buffer);
2729
3065
  onExport?.(buffer);
2730
3066
  const allowDownload = exportPolicy ? exportPolicy.allowDownload !== false : decision !== false;
2731
- if (!allowDownload) return;
2732
- const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2733
- const a = document.createElement("a");
2734
- a.href = url;
2735
- a.download = `${editor.title || "edited"}.epub`;
2736
- a.click();
2737
- URL.revokeObjectURL(url);
3067
+ if (allowDownload) downloadEpub(buffer, `${editor.title || "edited"}.epub`);
3068
+ clearError();
2738
3069
  }, reportError);
2739
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
+ );
2740
3081
  return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
2741
3082
  /* @__PURE__ */ jsxs9("main", { className: "mejiro-editor-preview", children: [
2742
3083
  previewBook ? /* @__PURE__ */ jsx14(
@@ -2746,6 +3087,7 @@ function MejiroEditor({
2746
3087
  epub: previewBook,
2747
3088
  chapter: selection.chapter,
2748
3089
  onChapterChange: (index) => selectParagraph(index, 0),
3090
+ onError: reportError,
2749
3091
  fonts,
2750
3092
  subtitle: messages.editorPreviewSubtitle,
2751
3093
  chapterNavMode: "panel",
@@ -2783,24 +3125,26 @@ function MejiroEditor({
2783
3125
  ] }),
2784
3126
  enableProofread && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
2785
3127
  /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorProofread }),
3128
+ paragraphText,
2786
3129
  /* @__PURE__ */ jsx14(
2787
- "textarea",
3130
+ "button",
2788
3131
  {
2789
- ref: textareaRef,
2790
- value: text,
2791
- onChange: (event) => setText(event.target.value),
2792
- onSelect: captureRubyRange
3132
+ type: "button",
3133
+ className: "mejiro-editor-primary",
3134
+ disabled: sceneBreak,
3135
+ onClick: () => commitEdit(),
3136
+ children: messages.editorApplyText
2793
3137
  }
2794
- ),
2795
- /* @__PURE__ */ jsx14("button", { type: "button", className: "mejiro-editor-primary", onClick: applyText, children: messages.editorApplyText })
3138
+ )
2796
3139
  ] }),
2797
3140
  enableRuby && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
2798
3141
  /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
2799
3142
  /* @__PURE__ */ jsx14("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
3143
+ !enableProofread && paragraphText,
2800
3144
  /* @__PURE__ */ jsx14("p", { className: "mejiro-editor-range", children: format(messages.editorRubyRange, {
2801
- start: rubyStart,
2802
- end: rubyEnd,
2803
- count: Math.max(0, rubyEnd - rubyStart)
3145
+ start: rubyRange?.start ?? 0,
3146
+ end: rubyRange?.end ?? 0,
3147
+ count: rubyRange ? rubyRange.end - rubyRange.start : 0
2804
3148
  }) }),
2805
3149
  /* @__PURE__ */ jsx14(
2806
3150
  "input",
@@ -2815,7 +3159,7 @@ function MejiroEditor({
2815
3159
  {
2816
3160
  type: "button",
2817
3161
  className: "mejiro-editor-primary",
2818
- disabled: textDirty || !rubyText.trim(),
3162
+ disabled: !(rubyRange && rubyText.trim()),
2819
3163
  onClick: applyRuby,
2820
3164
  children: messages.editorApplyRuby
2821
3165
  }
@@ -2833,6 +3177,7 @@ function MejiroEditor({
2833
3177
  hidden: true,
2834
3178
  onChange: (event) => {
2835
3179
  const file = event.target.files?.[0];
3180
+ event.target.value = "";
2836
3181
  if (file) void addImage(file);
2837
3182
  }
2838
3183
  }
@@ -2851,27 +3196,27 @@ function MejiroEditor({
2851
3196
  ] })
2852
3197
  ] });
2853
3198
  }
2854
- function paragraphBlockId(chapter, paragraphIndex) {
3199
+ function paragraphBlock(chapter, paragraphIndex) {
2855
3200
  let current = 0;
2856
3201
  for (const block of chapter.blocks) {
2857
3202
  if (block.kind !== "paragraph") continue;
2858
- if (current === paragraphIndex) return block.id;
3203
+ if (current === paragraphIndex) return block;
2859
3204
  current++;
2860
3205
  }
2861
3206
  return void 0;
2862
3207
  }
2863
3208
 
2864
3209
  // src/MejiroManuscriptEditor.tsx
2865
- import { EpubProject } from "@libraz/mejiro/epub";
2866
- import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMemo7, useRef as useRef14, 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";
2867
3212
 
2868
3213
  // src/MejiroNotationHighlighter.tsx
2869
3214
  import { tokenizeManuscriptSource } from "@libraz/mejiro";
2870
3215
  import {
2871
3216
  forwardRef as forwardRef2,
2872
3217
  useImperativeHandle as useImperativeHandle2,
2873
- useMemo as useMemo6,
2874
- useRef as useRef12,
3218
+ useMemo as useMemo7,
3219
+ useRef as useRef14,
2875
3220
  useState as useState10
2876
3221
  } from "react";
2877
3222
  import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
@@ -2888,12 +3233,13 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
2888
3233
  onCompositionEnd,
2889
3234
  ...rest
2890
3235
  }, ref) {
2891
- const textareaRef = useRef12(null);
2892
- const overlayRef = useRef12(null);
3236
+ const textareaRef = useRef14(null);
3237
+ const overlayRef = useRef14(null);
2893
3238
  useImperativeHandle2(ref, () => textareaRef.current, []);
2894
3239
  const [isComposing, setIsComposing] = useState10(false);
2895
- const lastSegmentsRef = useRef12(null);
2896
- const segments = useMemo6(() => {
3240
+ const [composingValue, setComposingValue] = useState10("");
3241
+ const lastSegmentsRef = useRef14(null);
3242
+ const segments = useMemo7(() => {
2897
3243
  if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
2898
3244
  const next = buildSegments(value, dialect);
2899
3245
  lastSegmentsRef.current = next;
@@ -2932,14 +3278,19 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
2932
3278
  ref: textareaRef,
2933
3279
  className: `mejiro-notation-textarea${className ? ` ${className}` : ""}`,
2934
3280
  style,
2935
- value,
2936
- onChange: (event) => onChange(event.target.value),
3281
+ value: isComposing ? composingValue : value,
3282
+ onChange: (event) => {
3283
+ if (isComposing) setComposingValue(event.target.value);
3284
+ onChange(event.target.value);
3285
+ },
2937
3286
  onCompositionStart: (event) => {
3287
+ setComposingValue(event.currentTarget.value);
2938
3288
  setIsComposing(true);
2939
3289
  onCompositionStart?.(event);
2940
3290
  },
2941
3291
  onCompositionEnd: (event) => {
2942
3292
  setIsComposing(false);
3293
+ onChange(event.currentTarget.value);
2943
3294
  onCompositionEnd?.(event);
2944
3295
  },
2945
3296
  onScroll: (event) => {
@@ -2980,10 +3331,276 @@ function buildSegments(text, dialect) {
2980
3331
  return segments;
2981
3332
  }
2982
3333
 
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
+ );
3346
+ const [chapters, setChaptersState] = useState11(
3347
+ () => options.chapters?.length ? options.chapters : [defaultChapter([], defaultTitle, defaultBody)]
3348
+ );
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);
3365
+ chaptersRef.current = chapters;
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 } : {}
3384
+ });
3385
+ for (const asset of assetsRef.current) project.addAsset(asset);
3386
+ return project;
3387
+ }, []);
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
+
2983
3600
  // src/useManuscriptDraft.ts
2984
- import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef13, useState as useState11 } from "react";
3601
+ import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef16, useState as useState12 } from "react";
2985
3602
  var DEFAULT_DELAY = 800;
2986
- function defaultChapter(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
3603
+ function defaultChapter2(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
2987
3604
  const index = existing.length;
2988
3605
  return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
2989
3606
  }
@@ -2996,49 +3613,66 @@ function useManuscriptDraft(options = {}) {
2996
3613
  defaultChapterTitle,
2997
3614
  defaultChapterBody
2998
3615
  } = options;
2999
- const titleForRef = useRef13(defaultChapterTitle);
3616
+ const titleForRef = useRef16(defaultChapterTitle);
3000
3617
  titleForRef.current = defaultChapterTitle;
3001
- const bodyForRef = useRef13(defaultChapterBody);
3618
+ const bodyForRef = useRef16(defaultChapterBody);
3002
3619
  bodyForRef.current = defaultChapterBody;
3003
- const [chapters, setChaptersState] = useState11(
3004
- () => options.initialChapters?.length ? options.initialChapters : [defaultChapter([], defaultChapterTitle, defaultChapterBody)]
3620
+ const [chapters, setChaptersState] = useState12(
3621
+ () => options.initialChapters?.length ? options.initialChapters : [defaultChapter2([], defaultChapterTitle, defaultChapterBody)]
3005
3622
  );
3006
- const [selected, setSelectedState] = useState11(0);
3007
- const [autosaveError, setAutosaveError] = useState11(null);
3008
- const saveRef = useRef13(onAutosave);
3623
+ const [selected, setSelectedState] = useState12(0);
3624
+ const [autosaveError, setAutosaveError] = useState12(null);
3625
+ const saveRef = useRef16(onAutosave);
3009
3626
  saveRef.current = onAutosave;
3010
- const payloadRef = useRef13(autosavePayload);
3627
+ const payloadRef = useRef16(autosavePayload);
3011
3628
  payloadRef.current = autosavePayload;
3012
- const chaptersRef = useRef13(chapters);
3629
+ const chaptersRef = useRef16(chapters);
3013
3630
  chaptersRef.current = chapters;
3014
- const selectedRef = useRef13(selected);
3631
+ const selectedRef = useRef16(selected);
3015
3632
  selectedRef.current = selected;
3016
- const trackerRef = useRef13(null);
3633
+ const trackerRef = useRef16(null);
3017
3634
  if (!trackerRef.current)
3018
3635
  trackerRef.current = createDraftChangeTracker(chapters, autosaveKey ?? "");
3019
- const dirtyRef = useRef13(false);
3020
- const mountedRef = useRef13(true);
3021
- const revisionRef = useRef13(0);
3022
- const inFlightRevisionRef = useRef13(-1);
3023
- const flushAutosave = useCallback8(() => {
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(() => {
3024
3642
  const callback = saveRef.current;
3025
- if (!(callback && dirtyRef.current) || inFlightRevisionRef.current === revisionRef.current) {
3643
+ if (!(callback && dirtyRef.current)) return;
3644
+ if (inFlightRef.current) {
3645
+ flushQueuedRef.current = true;
3026
3646
  return;
3027
3647
  }
3028
3648
  const revision = revisionRef.current;
3029
- inFlightRevisionRef.current = revision;
3030
3649
  const plain = snapshotChapters(chaptersRef.current);
3031
3650
  const payload = payloadRef.current ? payloadRef.current(plain) : plain;
3032
- void Promise.resolve(callback(payload)).then(() => {
3651
+ const markSaved = () => {
3033
3652
  if (revisionRef.current === revision) dirtyRef.current = false;
3034
- }).catch((err) => {
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) => {
3035
3666
  if (!mountedRef.current) return;
3036
3667
  setAutosaveError(toError(err));
3037
3668
  }).finally(() => {
3038
- if (inFlightRevisionRef.current === revision) inFlightRevisionRef.current = -1;
3669
+ inFlightRef.current = false;
3670
+ if (!flushQueuedRef.current) return;
3671
+ flushQueuedRef.current = false;
3672
+ flushAutosave();
3039
3673
  });
3040
3674
  }, []);
3041
- useEffect10(() => {
3675
+ useEffect12(() => {
3042
3676
  if (trackerRef.current?.changed(chapters, autosaveKey ?? "")) {
3043
3677
  dirtyRef.current = true;
3044
3678
  revisionRef.current += 1;
@@ -3050,7 +3684,7 @@ function useManuscriptDraft(options = {}) {
3050
3684
  }, autosaveDelay);
3051
3685
  return () => clearTimeout(timer);
3052
3686
  }, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
3053
- useEffect10(() => {
3687
+ useEffect12(() => {
3054
3688
  mountedRef.current = true;
3055
3689
  const detachPageHide = flushOnPageHide(flushAutosave);
3056
3690
  return () => {
@@ -3059,20 +3693,20 @@ function useManuscriptDraft(options = {}) {
3059
3693
  detachPageHide();
3060
3694
  };
3061
3695
  }, [flushAutosave]);
3062
- const commit = useCallback8((nextChapters, nextSelected) => {
3696
+ const commit = useCallback10((nextChapters, nextSelected) => {
3063
3697
  chaptersRef.current = nextChapters;
3064
3698
  selectedRef.current = nextSelected;
3065
3699
  setChaptersState(nextChapters);
3066
3700
  setSelectedState(nextSelected);
3067
3701
  }, []);
3068
- const setSelected = useCallback8((index) => {
3702
+ const setSelected = useCallback10((index) => {
3069
3703
  const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
3070
3704
  selectedRef.current = next;
3071
3705
  setSelectedState(next);
3072
3706
  }, []);
3073
- const setChapters = useCallback8(
3707
+ const setChapters = useCallback10(
3074
3708
  (next) => {
3075
- const normalized = next.length ? next : [defaultChapter([], titleForRef.current, bodyForRef.current)];
3709
+ const normalized = next.length ? next : [defaultChapter2([], titleForRef.current, bodyForRef.current)];
3076
3710
  const prev = selectedRef.current;
3077
3711
  const selectedId = chaptersRef.current[prev]?.id;
3078
3712
  const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
@@ -3083,17 +3717,17 @@ function useManuscriptDraft(options = {}) {
3083
3717
  },
3084
3718
  [commit]
3085
3719
  );
3086
- const patchChapter = useCallback8((index, patch) => {
3720
+ const patchChapter = useCallback10((index, patch) => {
3087
3721
  const next = chaptersRef.current.map(
3088
3722
  (chapter, i) => i === index ? { ...chapter, ...patch } : chapter
3089
3723
  );
3090
3724
  chaptersRef.current = next;
3091
3725
  setChaptersState(next);
3092
3726
  }, []);
3093
- const addChapter = useCallback8(
3727
+ const addChapter = useCallback10(
3094
3728
  (chapter = {}) => {
3095
3729
  const current = chaptersRef.current;
3096
- const generated = defaultChapter(current, titleForRef.current, bodyForRef.current);
3730
+ const generated = defaultChapter2(current, titleForRef.current, bodyForRef.current);
3097
3731
  const next = [
3098
3732
  ...current,
3099
3733
  {
@@ -3106,7 +3740,7 @@ function useManuscriptDraft(options = {}) {
3106
3740
  },
3107
3741
  [commit]
3108
3742
  );
3109
- const removeChapter = useCallback8(
3743
+ const removeChapter = useCallback10(
3110
3744
  (index) => {
3111
3745
  const current = chaptersRef.current;
3112
3746
  if (current.length <= 1) return;
@@ -3120,7 +3754,7 @@ function useManuscriptDraft(options = {}) {
3120
3754
  },
3121
3755
  [commit]
3122
3756
  );
3123
- const reorderChapters = useCallback8(
3757
+ const reorderChapters = useCallback10(
3124
3758
  (from, to) => {
3125
3759
  const current = chaptersRef.current;
3126
3760
  if (from < 0 || from >= current.length) return;
@@ -3153,14 +3787,6 @@ function useManuscriptDraft(options = {}) {
3153
3787
 
3154
3788
  // src/MejiroManuscriptEditor.tsx
3155
3789
  import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
3156
- function downloadEpub(buffer, title) {
3157
- const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
3158
- const a = document.createElement("a");
3159
- a.href = url;
3160
- a.download = `${title || "book"}.epub`;
3161
- a.click();
3162
- URL.revokeObjectURL(url);
3163
- }
3164
3790
  function coverExtension(mediaType) {
3165
3791
  switch (mediaType) {
3166
3792
  case "image/gif":
@@ -3181,7 +3807,7 @@ function coverAssetHref(file) {
3181
3807
  const filename = file.name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3182
3808
  return `OPS/Images/${filename && !/^\.+$/u.test(filename) ? filename : `cover${coverExtension(file.type)}`}`;
3183
3809
  }
3184
- function defaultChapter2(messages) {
3810
+ function defaultChapter3(messages) {
3185
3811
  return {
3186
3812
  id: `chapter-${Date.now()}-0`,
3187
3813
  title: format(messages.manuscriptDefaultChapterTitle, { n: 1 }),
@@ -3207,41 +3833,41 @@ function MejiroManuscriptEditor({
3207
3833
  panelSide = "right"
3208
3834
  }) {
3209
3835
  const messages = useI18n();
3210
- const coverInputRef = useRef14(null);
3211
- const bodyTextareaRef = useRef14(null);
3836
+ const coverInputRef = useRef17(null);
3837
+ const bodyTextareaRef = useRef17(null);
3212
3838
  const titleControlled = onTitleChange !== void 0;
3213
3839
  const authorControlled = onAuthorChange !== void 0;
3214
3840
  const coverControlled = onCoverChange !== void 0;
3215
- const [titleState, setTitleState] = useState12(titleProp ?? messages.manuscriptDefaultTitle);
3216
- const [authorState, setAuthorState] = useState12(authorProp ?? "");
3217
- 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);
3218
3844
  const title = titleControlled ? titleProp ?? "" : titleState;
3219
3845
  const author = authorControlled ? authorProp ?? "" : authorState;
3220
3846
  const cover = coverControlled ? coverProp ?? null : coverState;
3221
- useEffect11(() => {
3847
+ useEffect13(() => {
3222
3848
  if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
3223
3849
  }, [titleControlled, titleProp]);
3224
- useEffect11(() => {
3850
+ useEffect13(() => {
3225
3851
  if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
3226
3852
  }, [authorControlled, authorProp]);
3227
- useEffect11(() => {
3853
+ useEffect13(() => {
3228
3854
  if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
3229
3855
  }, [coverControlled, coverProp]);
3230
- const setTitle = useCallback9(
3856
+ const setTitle = useCallback11(
3231
3857
  (next) => {
3232
3858
  if (!titleControlled) setTitleState(next);
3233
3859
  onTitleChange?.(next);
3234
3860
  },
3235
3861
  [titleControlled, onTitleChange]
3236
3862
  );
3237
- const setAuthor = useCallback9(
3863
+ const setAuthor = useCallback11(
3238
3864
  (next) => {
3239
3865
  if (!authorControlled) setAuthorState(next);
3240
3866
  onAuthorChange?.(next);
3241
3867
  },
3242
3868
  [authorControlled, onAuthorChange]
3243
3869
  );
3244
- const setCover = useCallback9(
3870
+ const setCover = useCallback11(
3245
3871
  (next) => {
3246
3872
  if (!coverControlled) setCoverState(next);
3247
3873
  onCoverChange?.(next);
@@ -3249,7 +3875,7 @@ function MejiroManuscriptEditor({
3249
3875
  [coverControlled, onCoverChange]
3250
3876
  );
3251
3877
  const draft = useManuscriptDraft({
3252
- initialChapters: initialChapters?.length ? initialChapters : [defaultChapter2(messages)],
3878
+ initialChapters: initialChapters?.length ? initialChapters : [defaultChapter3(messages)],
3253
3879
  onAutosave,
3254
3880
  autosavePayload: (savedChapters) => ({ title, author, cover, chapters: savedChapters }),
3255
3881
  autosaveKey: `${title}\0${author}\0${cover?.name ?? ""}\0${cover?.size ?? 0}\0${cover?.lastModified ?? 0}`,
@@ -3266,8 +3892,16 @@ function MejiroManuscriptEditor({
3266
3892
  reorderChapters
3267
3893
  } = draft;
3268
3894
  const current = chapters[selected] ?? chapters[0];
3269
- const [draggingIndex, setDraggingIndex] = useState12(null);
3270
- 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), []);
3271
3905
  function wrapSelection(open, close) {
3272
3906
  const el = bodyTextareaRef.current;
3273
3907
  if (!el) return;
@@ -3285,37 +3919,31 @@ function MejiroManuscriptEditor({
3285
3919
  bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
3286
3920
  });
3287
3921
  }
3288
- const [identifier] = useState12(() => `urn:uuid:${crypto.randomUUID()}`);
3289
- const exportEpub = useCallback9(async () => {
3290
- await withErrorReporting(
3291
- async () => {
3292
- const project = EpubProject.fromManuscript({
3293
- metadata: { title, author: author || void 0, identifier },
3294
- dialect,
3295
- chapters: chapters.map((chapter) => ({
3296
- id: chapter.id,
3297
- title: chapter.title || messages.untitled,
3298
- body: chapter.body
3299
- }))
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()
3300
3939
  });
3301
- if (cover) {
3302
- project.setCover({
3303
- href: coverAssetHref(cover),
3304
- mediaType: cover.type || void 0,
3305
- data: await cover.arrayBuffer()
3306
- });
3307
- }
3308
- const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
3309
- setExportError(null);
3310
- onExport?.(buffer);
3311
- downloadEpub(buffer, title);
3312
- },
3313
- (error) => {
3314
- setExportError(error);
3315
- onError?.(error);
3316
3940
  }
3317
- );
3318
- }, [
3941
+ const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
3942
+ clearError();
3943
+ onExport?.(buffer);
3944
+ downloadEpub(buffer, `${title || "book"}.epub`);
3945
+ }, reportError);
3946
+ }, [
3319
3947
  assetResolver,
3320
3948
  author,
3321
3949
  chapters,
@@ -3323,11 +3951,12 @@ function MejiroManuscriptEditor({
3323
3951
  dialect,
3324
3952
  identifier,
3325
3953
  messages.untitled,
3326
- onError,
3327
3954
  onExport,
3955
+ clearError,
3956
+ reportError,
3328
3957
  title
3329
3958
  ]);
3330
- const previewManuscript = useMemo7(
3959
+ const previewManuscript = useMemo9(
3331
3960
  () => chapters.map((chapter) => ({
3332
3961
  id: chapter.id,
3333
3962
  title: chapter.title || messages.untitled,
@@ -3352,6 +3981,7 @@ function MejiroManuscriptEditor({
3352
3981
  fonts,
3353
3982
  chapter: selected,
3354
3983
  onChapterChange: setSelected,
3984
+ onError: reportError,
3355
3985
  enableImageOverlay: false
3356
3986
  }
3357
3987
  ) }),
@@ -3375,7 +4005,11 @@ function MejiroManuscriptEditor({
3375
4005
  type: "file",
3376
4006
  accept: "image/*",
3377
4007
  hidden: true,
3378
- 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
+ }
3379
4013
  }
3380
4014
  )
3381
4015
  ] }),
@@ -3394,6 +4028,7 @@ function MejiroManuscriptEditor({
3394
4028
  onDragStart: (event) => {
3395
4029
  setDraggingIndex(index);
3396
4030
  event.dataTransfer.effectAllowed = "move";
4031
+ event.dataTransfer.setData("text/plain", chapter.id);
3397
4032
  },
3398
4033
  onDragEnd: () => setDraggingIndex(null),
3399
4034
  onDragOver: (event) => {
@@ -3441,7 +4076,7 @@ function MejiroManuscriptEditor({
3441
4076
  onChange: (event) => patchChapter(selected, { title: event.target.value })
3442
4077
  }
3443
4078
  ),
3444
- /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
4079
+ dialect === "mejiro" && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
3445
4080
  /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u300A\u300A", "\u300B\u300B"), children: messages.manuscriptEmphasisDots }),
3446
4081
  /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u3014", "\u3015"), children: messages.manuscriptTcy }),
3447
4082
  /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("*", "*"), children: messages.manuscriptEm }),
@@ -3501,7 +4136,7 @@ function MejiroShelf({
3501
4136
  }
3502
4137
 
3503
4138
  // src/MejiroToc.tsx
3504
- import { useMemo as useMemo8, useState as useState13 } from "react";
4139
+ import { useMemo as useMemo10, useState as useState14 } from "react";
3505
4140
  import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3506
4141
  function buildEntries(epub, messages) {
3507
4142
  return epub.chapters.map((ch, i) => {
@@ -3524,9 +4159,9 @@ function MejiroToc({
3524
4159
  const messages = useI18n();
3525
4160
  const resolvedTitle = title ?? messages.tocTitle;
3526
4161
  const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
3527
- const [query, setQuery] = useState13("");
3528
- const entries = useMemo8(() => buildEntries(epub, messages), [epub, messages]);
3529
- const filtered = useMemo8(() => {
4162
+ const [query, setQuery] = useState14("");
4163
+ const entries = useMemo10(() => buildEntries(epub, messages), [epub, messages]);
4164
+ const filtered = useMemo10(() => {
3530
4165
  if (!query) return entries;
3531
4166
  const needle = query.toLowerCase();
3532
4167
  return entries.filter(
@@ -3576,34 +4211,41 @@ import {
3576
4211
  serializeAnnotations,
3577
4212
  sortAnnotations
3578
4213
  } from "@libraz/mejiro";
3579
- import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef15, useState as useState14 } from "react";
4214
+ import { useCallback as useCallback12, useEffect as useEffect14, useRef as useRef18, useState as useState15 } from "react";
3580
4215
  function parseSorted(raw) {
3581
4216
  return sortAnnotations(parseAnnotations(raw));
3582
4217
  }
3583
4218
  function useAnnotations(options) {
3584
4219
  const { key, throttleMs = 250 } = options;
3585
4220
  const storage = options.storage ?? resolveDefaultStorage();
3586
- const [annotations, setAnnotations] = useState14(
3587
- () => readStorage(storage, key, parseSorted, [])
3588
- );
3589
- const storageRef = useRef15(storage);
4221
+ const storageRef = useRef18(storage);
3590
4222
  storageRef.current = storage;
3591
- const annotationsRef = useRef15(annotations);
3592
- const applyNext = useCallback10((next) => {
3593
- annotationsRef.current = next;
3594
- setAnnotations(next);
3595
- }, []);
3596
- const pendingRef = useRef15(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);
3597
4242
  if (!pendingRef.current) pendingRef.current = createPendingWrite();
3598
4243
  const pending = pendingRef.current;
3599
- useEffect12(() => flushOnPageHide(pending.flush), [pending]);
3600
- useEffect12(() => {
3601
- applyNext(readStorage(storageRef.current, key, parseSorted, []));
3602
- return pending.flush;
3603
- }, [key, applyNext, pending]);
3604
- const onChangeRef = useRef15(options.onChange);
4244
+ useEffect14(() => flushOnPageHide(pending.flush), [pending]);
4245
+ useEffect14(() => pending.flush, [key, pending]);
4246
+ const onChangeRef = useRef18(options.onChange);
3605
4247
  onChangeRef.current = options.onChange;
3606
- const commit = useCallback10(
4248
+ const commit = useCallback12(
3607
4249
  (next) => {
3608
4250
  const currentStorage = storageRef.current;
3609
4251
  if (currentStorage) {
@@ -3615,7 +4257,7 @@ function useAnnotations(options) {
3615
4257
  },
3616
4258
  [key, throttleMs, pending]
3617
4259
  );
3618
- const add = useCallback10(
4260
+ const add = useCallback12(
3619
4261
  (input) => {
3620
4262
  const annotation = {
3621
4263
  ...input,
@@ -3629,7 +4271,7 @@ function useAnnotations(options) {
3629
4271
  },
3630
4272
  [commit, applyNext]
3631
4273
  );
3632
- const remove = useCallback10(
4274
+ const remove = useCallback12(
3633
4275
  (id) => {
3634
4276
  const current = annotationsRef.current;
3635
4277
  const next = current.filter((annotation) => annotation.id !== id);
@@ -3639,7 +4281,7 @@ function useAnnotations(options) {
3639
4281
  },
3640
4282
  [commit, applyNext]
3641
4283
  );
3642
- const update = useCallback10(
4284
+ const update = useCallback12(
3643
4285
  (id, patch) => {
3644
4286
  let changed = false;
3645
4287
  const next = annotationsRef.current.map((annotation) => {
@@ -3654,7 +4296,8 @@ function useAnnotations(options) {
3654
4296
  },
3655
4297
  [commit, applyNext]
3656
4298
  );
3657
- const clear = useCallback10(() => {
4299
+ const clear = useCallback12(() => {
4300
+ if (annotationsRef.current.length === 0) return;
3658
4301
  applyNext([]);
3659
4302
  pending.cancel();
3660
4303
  removeStorage(storageRef.current, key);
@@ -3669,39 +4312,40 @@ import {
3669
4312
  cloneEditableEpubBook as cloneEditableEpubBook2,
3670
4313
  EditableEpub as EditableEpub2
3671
4314
  } from "@libraz/mejiro/epub";
3672
- import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef16, useState as useState15 } from "react";
4315
+ import { useCallback as useCallback13, useEffect as useEffect15, useMemo as useMemo11, useRef as useRef19, useState as useState16 } from "react";
3673
4316
  function useEditableEpub(options = {}) {
3674
- const [editor, setEditor] = useState15(null);
3675
- const [loading, setLoading] = useState15(false);
3676
- const [exporting, setExporting] = useState15(false);
3677
- const [error, setError] = useState15(null);
3678
- const [revision, setRevision] = useState15(0);
3679
- 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({
3680
4323
  chapter: 0,
3681
4324
  paragraph: 0
3682
4325
  });
3683
- const requestIdRef = useRef16(0);
3684
- const onLoadRef = useRef16(options.onLoad);
3685
- const onErrorRef = useRef16(options.onError);
3686
- const onExportRef = useRef16(options.onExport);
3687
- const limitsRef = useRef16(options.limits);
3688
- const assetResolverRef = useRef16(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);
3689
4333
  onLoadRef.current = options.onLoad;
3690
4334
  onErrorRef.current = options.onError;
3691
4335
  onExportRef.current = options.onExport;
3692
4336
  limitsRef.current = options.limits;
3693
4337
  assetResolverRef.current = options.assetResolver;
3694
- const editorRef = useRef16(editor);
4338
+ const editorRef = useRef19(editor);
3695
4339
  editorRef.current = editor;
3696
- const selectionRef = useRef16(selection);
4340
+ const selectionRef = useRef19(selection);
3697
4341
  selectionRef.current = selection;
3698
- const loadBufferWithRequest = useCallback11(
3699
- async (buffer, requestId) => {
4342
+ const loadBufferWithRequest = useCallback13(
4343
+ async (buffer, currentRequest) => {
3700
4344
  setLoading(true);
3701
4345
  setError(null);
3702
4346
  try {
3703
4347
  const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
3704
- if (requestId !== requestIdRef.current) return null;
4348
+ if (!request.isCurrent(currentRequest)) return null;
3705
4349
  const firstParagraph = { chapter: 0, paragraph: 0 };
3706
4350
  editorRef.current = next;
3707
4351
  selectionRef.current = firstParagraph;
@@ -3711,34 +4355,37 @@ function useEditableEpub(options = {}) {
3711
4355
  onLoadRef.current?.(next);
3712
4356
  return next;
3713
4357
  } catch (err) {
3714
- if (requestId === requestIdRef.current) {
4358
+ if (request.isCurrent(currentRequest)) {
3715
4359
  const nextError = toError(err);
3716
4360
  setError(nextError);
3717
4361
  onErrorRef.current?.(nextError);
3718
4362
  }
3719
4363
  return null;
3720
4364
  } finally {
3721
- if (requestId === requestIdRef.current) setLoading(false);
4365
+ if (request.isCurrent(currentRequest)) setLoading(false);
3722
4366
  }
3723
4367
  },
3724
- []
4368
+ [request]
3725
4369
  );
3726
- const loadBuffer = useCallback11(
4370
+ const loadBuffer = useCallback13(
3727
4371
  async (buffer) => {
3728
- const requestId = ++requestIdRef.current;
3729
- return loadBufferWithRequest(buffer, requestId);
4372
+ const currentRequest = request.begin();
4373
+ return loadBufferWithRequest(buffer, currentRequest);
3730
4374
  },
3731
- [loadBufferWithRequest]
4375
+ [loadBufferWithRequest, request]
3732
4376
  );
3733
- const loadFile = useCallback11(
4377
+ const loadFile = useCallback13(
3734
4378
  async (file) => {
3735
- const requestId = ++requestIdRef.current;
4379
+ const currentRequest = request.begin();
3736
4380
  setLoading(true);
3737
4381
  setError(null);
3738
4382
  try {
3739
- return await loadBufferWithRequest(await readEpubFile(file, limitsRef.current), requestId);
4383
+ return await loadBufferWithRequest(
4384
+ await readEpubFile(file, limitsRef.current),
4385
+ currentRequest
4386
+ );
3740
4387
  } catch (err) {
3741
- if (requestId === requestIdRef.current) {
4388
+ if (request.isCurrent(currentRequest)) {
3742
4389
  const nextError = toError(err);
3743
4390
  setError(nextError);
3744
4391
  onErrorRef.current?.(nextError);
@@ -3747,17 +4394,17 @@ function useEditableEpub(options = {}) {
3747
4394
  return null;
3748
4395
  }
3749
4396
  },
3750
- [loadBufferWithRequest]
4397
+ [loadBufferWithRequest, request]
3751
4398
  );
3752
- const loadUrl = useCallback11(
4399
+ const loadUrl = useCallback13(
3753
4400
  async (url) => {
3754
- const requestId = ++requestIdRef.current;
4401
+ const currentRequest = request.begin();
3755
4402
  setLoading(true);
3756
4403
  setError(null);
3757
4404
  try {
3758
- return await loadBufferWithRequest(await fetchEpubBuffer(url), requestId);
4405
+ return await loadBufferWithRequest(await fetchEpubBuffer(url), currentRequest);
3759
4406
  } catch (err) {
3760
- if (requestId === requestIdRef.current) {
4407
+ if (request.isCurrent(currentRequest)) {
3761
4408
  const nextError = toError(err);
3762
4409
  setError(nextError);
3763
4410
  onErrorRef.current?.(nextError);
@@ -3766,24 +4413,32 @@ function useEditableEpub(options = {}) {
3766
4413
  return null;
3767
4414
  }
3768
4415
  },
3769
- [loadBufferWithRequest]
4416
+ [loadBufferWithRequest, request]
3770
4417
  );
3771
- useEffect13(() => {
3772
- if (options.defaultUrl) void loadUrl(options.defaultUrl);
3773
- }, [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]);
3774
4429
  const book = editor?.book ?? null;
3775
4430
  const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
3776
- const previewBook = useMemo9(() => {
4431
+ const previewBook = useMemo11(() => {
3777
4432
  void revision;
3778
- return book ? cloneEditableEpubBook2(book) : null;
3779
- }, [book, revision]);
4433
+ return book && editor ? markBookIdentity(cloneEditableEpubBook2(book), editor) : null;
4434
+ }, [book, editor, revision]);
3780
4435
  const history = editor?.history ?? null;
3781
- const setSelection = useCallback11((nextSelection) => {
4436
+ const setSelection = useCallback13((nextSelection) => {
3782
4437
  const next = clampEditableEpubSelection(editorRef.current?.book ?? null, nextSelection);
3783
4438
  selectionRef.current = next;
3784
4439
  setSelectionState(next);
3785
4440
  }, []);
3786
- const updateParagraph = useCallback11(
4441
+ const updateParagraph = useCallback13(
3787
4442
  (text, inlineAnnotations) => {
3788
4443
  const current = editorRef.current;
3789
4444
  if (!current) return;
@@ -3793,37 +4448,38 @@ function useEditableEpub(options = {}) {
3793
4448
  },
3794
4449
  []
3795
4450
  );
3796
- const setInlineAnnotations = useCallback11((inlineAnnotations) => {
4451
+ const setInlineAnnotations = useCallback13((inlineAnnotations) => {
3797
4452
  const current = editorRef.current;
3798
4453
  if (!current) return;
3799
4454
  const { chapter, paragraph } = selectionRef.current;
3800
4455
  current.setInlineAnnotations(chapter, paragraph, inlineAnnotations);
3801
4456
  setRevision((value) => value + 1);
3802
4457
  }, []);
3803
- const addImage = useCallback11((image) => {
4458
+ const addImage = useCallback13((image) => {
3804
4459
  const current = editorRef.current;
3805
4460
  if (!current) return;
3806
4461
  current.addImage(selectionRef.current.chapter, image);
3807
4462
  setRevision((value) => value + 1);
3808
4463
  }, []);
3809
- const undo = useCallback11(() => {
4464
+ const undo = useCallback13(() => {
3810
4465
  const current = editorRef.current;
3811
4466
  if (!current) return false;
3812
4467
  const changed = current.undo();
3813
4468
  if (changed) setRevision((value) => value + 1);
3814
4469
  return changed;
3815
4470
  }, []);
3816
- const redo = useCallback11(() => {
4471
+ const redo = useCallback13(() => {
3817
4472
  const current = editorRef.current;
3818
4473
  if (!current) return false;
3819
4474
  const changed = current.redo();
3820
4475
  if (changed) setRevision((value) => value + 1);
3821
4476
  return changed;
3822
4477
  }, []);
3823
- const exportEpub = useCallback11(
4478
+ const exportEpub = useCallback13(
3824
4479
  async (exportOptions) => {
3825
4480
  const current = editorRef.current;
3826
4481
  if (!current) return null;
4482
+ exportsInFlightRef.current++;
3827
4483
  setExporting(true);
3828
4484
  try {
3829
4485
  const buffer = await current.export(
@@ -3832,7 +4488,7 @@ function useEditableEpub(options = {}) {
3832
4488
  onExportRef.current?.(buffer);
3833
4489
  return buffer;
3834
4490
  } finally {
3835
- setExporting(false);
4491
+ setExporting(--exportsInFlightRef.current > 0);
3836
4492
  }
3837
4493
  },
3838
4494
  []
@@ -3865,258 +4521,12 @@ function withAssetResolver(options, resolver) {
3865
4521
  return { ...options, assetResolver: resolver };
3866
4522
  }
3867
4523
 
3868
- // src/useEpubProject.ts
3869
- import {
3870
- EpubProject as EpubProject2,
3871
- parseEpub as parseEpub2
3872
- } from "@libraz/mejiro/epub";
3873
- import { useCallback as useCallback12, useEffect as useEffect14, useMemo as useMemo10, useRef as useRef17, useState as useState16 } from "react";
3874
- function useEpubProject(options = {}) {
3875
- const defaultTitle = options.defaultChapterTitle;
3876
- const defaultBody = options.defaultChapterBody;
3877
- const [metadata, setMetadataState] = useState16(
3878
- () => initialMetadata(options.metadata)
3879
- );
3880
- const [chapters, setChaptersState] = useState16(
3881
- () => options.chapters?.length ? options.chapters : [defaultChapter3([], defaultTitle, defaultBody)]
3882
- );
3883
- const [selectedChapter, setSelectedChapterState] = useState16(0);
3884
- const [cover, setCoverState] = useState16(options.cover ?? null);
3885
- const [assets, setAssetsState] = useState16(options.assets ?? []);
3886
- const [previewBook, setPreviewBook] = useState16(null);
3887
- const [previewError, setPreviewError] = useState16(null);
3888
- const [previewing, setPreviewing] = useState16(false);
3889
- const previewRequestIdRef = useRef17(0);
3890
- const onPreviewRef = useRef17(options.onPreview);
3891
- const onExportRef = useRef17(options.onExport);
3892
- onPreviewRef.current = options.onPreview;
3893
- onExportRef.current = options.onExport;
3894
- const assetResolverRef = useRef17(options.assetResolver);
3895
- assetResolverRef.current = options.assetResolver;
3896
- const metadataRef = useRef17(metadata);
3897
- metadataRef.current = metadata;
3898
- const chaptersRef = useRef17(chapters);
3899
- chaptersRef.current = chapters;
3900
- const selectedRef = useRef17(selectedChapter);
3901
- selectedRef.current = selectedChapter;
3902
- const coverRef = useRef17(cover);
3903
- coverRef.current = cover;
3904
- const assetsRef = useRef17(assets);
3905
- assetsRef.current = assets;
3906
- const buildProject = useCallback12(() => {
3907
- const currentCover = coverRef.current;
3908
- const project = EpubProject2.fromManuscript({
3909
- metadata: metadataRef.current,
3910
- includeTitlePage: false,
3911
- includeTitleInFirstChapter: true,
3912
- chapters: chaptersRef.current.map((chapter) => ({
3913
- id: chapter.id,
3914
- title: chapter.title || "Untitled",
3915
- body: chapter.body
3916
- })),
3917
- ...currentCover ? { cover: currentCover } : {}
3918
- });
3919
- for (const asset of assetsRef.current) project.addAsset(asset);
3920
- return project;
3921
- }, []);
3922
- const exportOptions = useCallback12((signal) => {
3923
- const resolver = assetResolverRef.current;
3924
- return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
3925
- }, []);
3926
- useEffect14(() => {
3927
- const requestId = ++previewRequestIdRef.current;
3928
- const controller = new AbortController();
3929
- let cancelled = false;
3930
- setPreviewing(true);
3931
- const timer = setTimeout(() => {
3932
- void (async () => {
3933
- try {
3934
- const book = await parseEpub2(
3935
- await buildProject().export(exportOptions(controller.signal))
3936
- );
3937
- if (cancelled || requestId !== previewRequestIdRef.current) return;
3938
- setPreviewBook(book);
3939
- setPreviewError(null);
3940
- onPreviewRef.current?.(book);
3941
- } catch (err) {
3942
- if (!cancelled && requestId === previewRequestIdRef.current) {
3943
- setPreviewError(toError(err));
3944
- }
3945
- } finally {
3946
- if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
3947
- }
3948
- })();
3949
- }, options.debounceMs ?? 250);
3950
- return () => {
3951
- cancelled = true;
3952
- previewRequestIdRef.current++;
3953
- clearTimeout(timer);
3954
- controller.abort();
3955
- };
3956
- }, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
3957
- const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
3958
- const setMetadata = useCallback12((patch) => {
3959
- const next = { ...metadataRef.current, ...patch };
3960
- metadataRef.current = next;
3961
- setMetadataState(next);
3962
- }, []);
3963
- const commit = useCallback12((nextChapters, nextSelected) => {
3964
- chaptersRef.current = nextChapters;
3965
- selectedRef.current = nextSelected;
3966
- setChaptersState(nextChapters);
3967
- setSelectedChapterState(nextSelected);
3968
- }, []);
3969
- const setChapters = useCallback12(
3970
- (next) => {
3971
- const normalized = next.length ? next : [defaultChapter3([], defaultTitle, defaultBody)];
3972
- const prev = selectedRef.current;
3973
- const selectedId = chaptersRef.current[prev]?.id;
3974
- const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
3975
- commit(
3976
- normalized,
3977
- nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
3978
- );
3979
- },
3980
- [commit, defaultBody, defaultTitle]
3981
- );
3982
- const setCover = useCallback12((asset) => {
3983
- coverRef.current = asset;
3984
- setCoverState(asset);
3985
- }, []);
3986
- const setAssets = useCallback12((next) => {
3987
- assetsRef.current = next;
3988
- setAssetsState(next);
3989
- }, []);
3990
- const setSelectedChapter = useCallback12((index) => {
3991
- const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
3992
- selectedRef.current = next;
3993
- setSelectedChapterState(next);
3994
- }, []);
3995
- const patchChapter = useCallback12((index, patch) => {
3996
- const next = chaptersRef.current.map(
3997
- (chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
3998
- );
3999
- chaptersRef.current = next;
4000
- setChaptersState(next);
4001
- }, []);
4002
- const addChapter = useCallback12(
4003
- (chapter = {}) => {
4004
- const current = chaptersRef.current;
4005
- const generated = defaultChapter3(current, defaultTitle, defaultBody);
4006
- const next = [
4007
- ...current,
4008
- {
4009
- id: chapter.id ?? generated.id,
4010
- title: chapter.title ?? generated.title,
4011
- body: chapter.body ?? generated.body
4012
- }
4013
- ];
4014
- commit(next, next.length - 1);
4015
- },
4016
- [commit, defaultBody, defaultTitle]
4017
- );
4018
- const removeChapter = useCallback12(
4019
- (index = selectedRef.current) => {
4020
- const current = chaptersRef.current;
4021
- if (current.length <= 1) return;
4022
- const next = current.filter((_, chapterIndex) => chapterIndex !== index);
4023
- const prev = selectedRef.current;
4024
- let nextSelected;
4025
- if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
4026
- else if (index < prev) nextSelected = prev - 1;
4027
- else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
4028
- commit(next, nextSelected);
4029
- },
4030
- [commit]
4031
- );
4032
- const reorderChapters = useCallback12(
4033
- (from, to) => {
4034
- const current = chaptersRef.current;
4035
- if (from < 0 || from >= current.length) return;
4036
- const next = [...current];
4037
- const [moved] = next.splice(from, 1);
4038
- const target = Math.max(0, Math.min(next.length, to));
4039
- next.splice(target, 0, moved);
4040
- const prev = selectedRef.current;
4041
- let nextSelected = prev;
4042
- if (prev === from) nextSelected = target;
4043
- else if (from < prev && target >= prev) nextSelected = prev - 1;
4044
- else if (from > prev && target <= prev) nextSelected = prev + 1;
4045
- commit(next, nextSelected);
4046
- },
4047
- [commit]
4048
- );
4049
- const exportEpub = useCallback12(async () => {
4050
- const buffer = await buildProject().export(exportOptions());
4051
- onExportRef.current?.(buffer);
4052
- return buffer;
4053
- }, [buildProject, exportOptions]);
4054
- return useMemo10(
4055
- () => ({
4056
- metadata,
4057
- chapters,
4058
- selectedChapter,
4059
- currentChapter,
4060
- cover,
4061
- assets,
4062
- previewBook,
4063
- previewError,
4064
- previewing,
4065
- setMetadata,
4066
- setChapters,
4067
- setSelectedChapter,
4068
- setCover,
4069
- setAssets,
4070
- patchChapter,
4071
- addChapter,
4072
- removeChapter,
4073
- reorderChapters,
4074
- buildProject,
4075
- exportEpub
4076
- }),
4077
- [
4078
- addChapter,
4079
- assets,
4080
- buildProject,
4081
- chapters,
4082
- cover,
4083
- currentChapter,
4084
- exportEpub,
4085
- metadata,
4086
- patchChapter,
4087
- previewBook,
4088
- previewError,
4089
- previewing,
4090
- reorderChapters,
4091
- removeChapter,
4092
- selectedChapter,
4093
- setAssets,
4094
- setChapters,
4095
- setCover,
4096
- setMetadata,
4097
- setSelectedChapter
4098
- ]
4099
- );
4100
- }
4101
- function initialMetadata(patch = {}) {
4102
- const metadata = mergeDefined(
4103
- { title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
4104
- patch
4105
- );
4106
- if (!metadata.identifier?.trim()) metadata.identifier = `urn:uuid:${crypto.randomUUID()}`;
4107
- return metadata;
4108
- }
4109
- 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" : "") {
4110
- const index = existing.length;
4111
- return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
4112
- }
4113
-
4114
4524
  // src/useImageOverlay.ts
4115
4525
  import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointerPress as isPrimaryPointerPress3 } from "@libraz/mejiro/browser";
4116
4526
  import {
4117
- useCallback as useCallback13,
4118
- useEffect as useEffect15,
4119
- useRef as useRef18,
4527
+ useCallback as useCallback14,
4528
+ useEffect as useEffect16,
4529
+ useRef as useRef20,
4120
4530
  useState as useState17
4121
4531
  } from "react";
4122
4532
  function useImageOverlay(layout, spreadIdx, onUpdate, options) {
@@ -4126,34 +4536,33 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4126
4536
  const defY = options?.defaultY ?? 100;
4127
4537
  const margin = options?.margin;
4128
4538
  const [imageRect, setImageRect] = useState17(null);
4129
- const rectRef = useRef18(imageRect);
4539
+ const rectRef = useRef20(imageRect);
4130
4540
  rectRef.current = imageRect;
4131
- const layoutRef = useRef18(layout);
4541
+ const layoutRef = useRef20(layout);
4132
4542
  layoutRef.current = layout;
4133
- const spreadRef = useRef18(spreadIdx);
4543
+ const spreadRef = useRef20(spreadIdx);
4134
4544
  spreadRef.current = spreadIdx;
4135
- const onUpdateRef = useRef18(onUpdate);
4545
+ const onUpdateRef = useRef20(onUpdate);
4136
4546
  onUpdateRef.current = onUpdate;
4137
- const activeDragCleanupsRef = useRef18(/* @__PURE__ */ new Set());
4138
- useEffect15(
4547
+ const marginRef = useRef20(margin);
4548
+ marginRef.current = margin;
4549
+ const activeDragCleanupsRef = useRef20(/* @__PURE__ */ new Set());
4550
+ useEffect16(
4139
4551
  () => () => {
4140
4552
  for (const cleanup of activeDragCleanupsRef.current) cleanup();
4141
4553
  activeDragCleanupsRef.current.clear();
4142
4554
  },
4143
4555
  []
4144
4556
  );
4145
- const syncToLayout = useCallback13(
4146
- (rect) => {
4147
- const lo = layoutRef.current;
4148
- if (!lo) return;
4149
- const images = rect ? [{ x: rect.x, y: rect.y, w: rect.w, h: rect.h, margin }] : void 0;
4150
- const spread = lo.syncImages(spreadRef.current, images);
4151
- onUpdateRef.current(spread);
4152
- },
4153
- [margin]
4154
- );
4155
- const syncedRef = useRef18({ layout, spreadIdx, margin });
4156
- useEffect15(() => {
4557
+ const syncToLayout = useCallback14((rect) => {
4558
+ const lo = layoutRef.current;
4559
+ if (!lo) return;
4560
+ const images = rect ? [{ x: rect.x, y: rect.y, w: rect.w, h: rect.h, margin: marginRef.current }] : void 0;
4561
+ const spread = lo.syncImages(spreadRef.current, images);
4562
+ onUpdateRef.current(spread);
4563
+ }, []);
4564
+ const syncedRef = useRef20({ layout, spreadIdx, margin });
4565
+ useEffect16(() => {
4157
4566
  const prev = syncedRef.current;
4158
4567
  syncedRef.current = { layout, spreadIdx, margin };
4159
4568
  if (prev.layout === layout && prev.spreadIdx === spreadIdx && prev.margin === margin) return;
@@ -4163,24 +4572,29 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4163
4572
  }
4164
4573
  syncToLayout(rectRef.current);
4165
4574
  }, [layout, spreadIdx, margin, syncToLayout]);
4166
- const toggleImage = useCallback13(() => {
4575
+ const toggleImage = useCallback14(() => {
4167
4576
  if (rectRef.current) {
4577
+ for (const cancel of [...activeDragCleanupsRef.current]) cancel();
4578
+ rectRef.current = null;
4168
4579
  setImageRect(null);
4169
4580
  syncToLayout(null);
4170
4581
  } else {
4171
4582
  const r = { x: defX, y: defY, w: defW, h: defH };
4583
+ rectRef.current = r;
4172
4584
  setImageRect(r);
4173
4585
  syncToLayout(r);
4174
4586
  }
4175
4587
  }, [defX, defY, defW, defH, syncToLayout]);
4176
- const applyRect = useCallback13(
4588
+ const applyRect = useCallback14(
4177
4589
  (rect) => {
4590
+ if (rectRef.current === null) return;
4591
+ rectRef.current = rect;
4178
4592
  setImageRect(rect);
4179
4593
  syncToLayout(rect);
4180
4594
  },
4181
4595
  [syncToLayout]
4182
4596
  );
4183
- const onOverlayPointerDown = useCallback13(
4597
+ const onOverlayPointerDown = useCallback14(
4184
4598
  (e) => {
4185
4599
  if (!isPrimaryPointerPress3(e)) return;
4186
4600
  e.preventDefault();
@@ -4202,7 +4616,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4202
4616
  },
4203
4617
  [applyRect]
4204
4618
  );
4205
- const onResizePointerDown = useCallback13(
4619
+ const onResizePointerDown = useCallback14(
4206
4620
  (e) => {
4207
4621
  if (!isPrimaryPointerPress3(e)) return;
4208
4622
  e.preventDefault();
@@ -4235,7 +4649,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4235
4649
  }
4236
4650
 
4237
4651
  // src/useLibrary.ts
4238
- import { useCallback as useCallback14, useEffect as useEffect16, useMemo as useMemo11, useState as useState18 } from "react";
4652
+ import { useCallback as useCallback15, useEffect as useEffect17, useMemo as useMemo12, useState as useState18 } from "react";
4239
4653
  function resolveCurrentId(volumes, currentId, preferredId) {
4240
4654
  if (volumes.length === 0) return null;
4241
4655
  if (preferredId != null && volumes.some((v) => v.id === preferredId)) return preferredId;
@@ -4249,21 +4663,21 @@ function useLibrary(options) {
4249
4663
  );
4250
4664
  const [navigated, setNavigated] = useState18(false);
4251
4665
  const resolvedId = resolveCurrentId(volumes, currentId, navigated ? void 0 : initialVolumeId);
4252
- useEffect16(() => {
4666
+ useEffect17(() => {
4253
4667
  if (resolvedId === currentId) return;
4254
4668
  setCurrentId(resolvedId);
4255
4669
  const volume = volumes.find((v) => v.id === resolvedId);
4256
4670
  if (volume) onChange?.(volume);
4257
4671
  }, [currentId, resolvedId, volumes, onChange]);
4258
- const index = useMemo11(
4672
+ const index = useMemo12(
4259
4673
  () => resolvedId == null ? -1 : volumes.findIndex((v) => v.id === resolvedId),
4260
4674
  [resolvedId, volumes]
4261
4675
  );
4262
- const current = useMemo11(
4676
+ const current = useMemo12(
4263
4677
  () => index >= 0 && index < volumes.length ? volumes[index] : null,
4264
4678
  [index, volumes]
4265
4679
  );
4266
- const activate = useCallback14(
4680
+ const activate = useCallback15(
4267
4681
  (i) => {
4268
4682
  const v = volumes[i];
4269
4683
  if (!v) return;
@@ -4273,17 +4687,17 @@ function useLibrary(options) {
4273
4687
  },
4274
4688
  [volumes, onChange]
4275
4689
  );
4276
- const next = useCallback14(() => {
4690
+ const next = useCallback15(() => {
4277
4691
  if (index < 0) return;
4278
4692
  const ni = Math.min(volumes.length - 1, index + 1);
4279
4693
  if (ni !== index) activate(ni);
4280
4694
  }, [index, activate, volumes.length]);
4281
- const prev = useCallback14(() => {
4695
+ const prev = useCallback15(() => {
4282
4696
  if (index < 0) return;
4283
4697
  const ni = Math.max(0, index - 1);
4284
4698
  if (ni !== index) activate(ni);
4285
4699
  }, [index, activate]);
4286
- const goTo = useCallback14(
4700
+ const goTo = useCallback15(
4287
4701
  (id) => {
4288
4702
  const ni = volumes.findIndex((v) => v.id === id);
4289
4703
  if (ni < 0) return;
@@ -4295,122 +4709,32 @@ function useLibrary(options) {
4295
4709
  }
4296
4710
 
4297
4711
  // src/useManuscriptLayout.ts
4298
- import {
4299
- useCallback as useCallback15,
4300
- useEffect as useEffect17,
4301
- useLayoutEffect as useLayoutEffect5,
4302
- useRef as useRef19,
4303
- useState as useState19
4304
- } 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
+ }
4305
4716
  function useManuscriptLayout(book, chapter, surface, options = {}) {
4306
4717
  const dialect = options.dialect ?? "mejiro";
4307
- const enableResize = options.enableResize ?? true;
4308
- const resizeDebounce = options.resizeDebounce ?? 120;
4309
- const [layout, setLayout] = useState19(null);
4310
- const [pageWidth, setPageWidth] = useState19(0);
4311
- const [pageHeight, setPageHeight] = useState19(0);
4312
- const [contentHeight, setContentHeight] = useState19(0);
4313
- const [elapsedMs, setElapsedMs] = useState19(0);
4314
- const requestIdRef = useRef19(0);
4315
- const layoutRef = useRef19(null);
4316
- layoutRef.current = layout;
4317
- const pendingRestore = useRef19(null);
4318
- const optionsRef = useRef19(options);
4319
- optionsRef.current = options;
4320
- const recompute = useCallback15(
4321
- async (opts = {}) => {
4322
- const blank = opts.blank ?? true;
4323
- const requestId = ++requestIdRef.current;
4324
- if (!(chapter && surface.current)) {
4325
- setLayout(null);
4326
- setPageWidth(0);
4327
- setPageHeight(0);
4328
- setContentHeight(0);
4329
- setElapsedMs(0);
4330
- return;
4331
- }
4332
- const captured = !blank && layoutRef.current ? optionsRef.current.capturePosition?.(layoutRef.current) ?? null : null;
4333
- pendingRestore.current = captured;
4334
- if (blank) setLayout(null);
4335
- const dims = book.computePageSize(surface.current);
4336
- setPageWidth(dims.pageWidth);
4337
- setPageHeight(dims.pageHeight);
4338
- setContentHeight(dims.contentHeight);
4339
- const t0 = performance.now();
4340
- const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
4341
- if (requestId !== requestIdRef.current) return;
4342
- const next = layouts.values().next().value ?? null;
4343
- layoutRef.current = next;
4344
- setLayout(next);
4345
- setElapsedMs(performance.now() - t0);
4346
- },
4347
- [book, chapter, dialect, surface]
4348
- );
4349
- useLayoutEffect5(() => {
4350
- requestIdRef.current++;
4351
- pendingRestore.current = null;
4352
- setLayout(null);
4353
- setPageWidth(0);
4354
- setPageHeight(0);
4355
- setContentHeight(0);
4356
- setElapsedMs(0);
4357
- }, [recompute]);
4358
- useEffect17(() => {
4359
- void recompute();
4360
- return () => {
4361
- requestIdRef.current++;
4362
- };
4363
- }, [recompute]);
4364
- useLayoutEffect5(() => {
4365
- const anchor = pendingRestore.current;
4366
- const restore = optionsRef.current.restorePosition;
4367
- if (!(layout && anchor && restore)) return;
4368
- pendingRestore.current = null;
4369
- restore(layout, anchor);
4370
- }, [layout]);
4371
- useEffect17(() => {
4372
- if (!enableResize) return;
4373
- const el = surface.current;
4374
- if (!el) return;
4375
- let timer = null;
4376
- let observed = false;
4377
- const clear = () => {
4378
- if (timer) {
4379
- clearTimeout(timer);
4380
- timer = null;
4718
+ const signature = chapterSignature(chapter);
4719
+ const chapterRef = useRef21(chapter);
4720
+ chapterRef.current = chapter;
4721
+ const source = useMemo13(() => {
4722
+ if (signature === null) return null;
4723
+ return {
4724
+ measure: (el) => book.computePageSize(el),
4725
+ layout: async () => {
4726
+ const current = chapterRef.current;
4727
+ if (!current) return null;
4728
+ const layouts = await book.layoutManuscript({ chapters: [current], dialect });
4729
+ return layouts.values().next().value ?? null;
4381
4730
  }
4382
4731
  };
4383
- const scheduleReflow = (immediate) => {
4384
- clear();
4385
- if (immediate) {
4386
- void recompute({ blank: false });
4387
- return;
4388
- }
4389
- timer = setTimeout(() => {
4390
- timer = null;
4391
- void recompute({ blank: false });
4392
- }, resizeDebounce);
4393
- };
4394
- if (typeof ResizeObserver !== "undefined") {
4395
- const observer = new ResizeObserver(() => {
4396
- const immediate = !observed;
4397
- observed = true;
4398
- scheduleReflow(immediate);
4399
- });
4400
- observer.observe(el);
4401
- return () => {
4402
- observer.disconnect();
4403
- clear();
4404
- };
4405
- }
4406
- const onWindowResize = () => scheduleReflow(false);
4407
- window.addEventListener("resize", onWindowResize);
4408
- return () => {
4409
- window.removeEventListener("resize", onWindowResize);
4410
- clear();
4411
- };
4412
- }, [surface, enableResize, resizeDebounce, recompute]);
4413
- return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
4732
+ }, [book, signature, dialect]);
4733
+ return useSurfaceLayout(source, surface, {
4734
+ ...options,
4735
+ enableResize: options.enableResize ?? true,
4736
+ resizeDebounce: options.resizeDebounce ?? 120
4737
+ });
4414
4738
  }
4415
4739
 
4416
4740
  // src/useReadingPosition.ts
@@ -4418,28 +4742,30 @@ import {
4418
4742
  parseReadingPosition,
4419
4743
  serializeReadingPosition
4420
4744
  } from "@libraz/mejiro";
4421
- import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef20, useState as useState20 } from "react";
4745
+ import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef22, useState as useState19 } from "react";
4422
4746
  function useReadingPosition(options) {
4423
4747
  const { key, throttleMs = 250 } = options;
4424
4748
  const storage = options.storage ?? resolveDefaultStorage();
4425
- const [position, setPosition] = useState20(
4426
- () => readStorage(storage, key, parseReadingPosition, null)
4427
- );
4428
- const storageRef = useRef20(storage);
4749
+ const storageRef = useRef22(storage);
4429
4750
  storageRef.current = storage;
4430
- 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);
4431
4760
  if (!pendingRef.current) pendingRef.current = createPendingWrite();
4432
4761
  const pending = pendingRef.current;
4433
4762
  useEffect18(() => flushOnPageHide(pending.flush), [pending]);
4434
- useEffect18(() => {
4435
- setPosition(readStorage(storageRef.current, key, parseReadingPosition, null));
4436
- return pending.flush;
4437
- }, [key, pending]);
4438
- const onChangeRef = useRef20(options.onChange);
4763
+ useEffect18(() => pending.flush, [key, pending]);
4764
+ const onChangeRef = useRef22(options.onChange);
4439
4765
  onChangeRef.current = options.onChange;
4440
4766
  const save = useCallback16(
4441
4767
  (next) => {
4442
- setPosition(next);
4768
+ setHydrated({ key, value: next });
4443
4769
  const currentStorage = storageRef.current;
4444
4770
  if (currentStorage) {
4445
4771
  pending.schedule(() => {
@@ -4451,7 +4777,7 @@ function useReadingPosition(options) {
4451
4777
  [key, throttleMs, pending]
4452
4778
  );
4453
4779
  const clear = useCallback16(() => {
4454
- setPosition(null);
4780
+ setHydrated({ key, value: null });
4455
4781
  pending.cancel();
4456
4782
  removeStorage(storageRef.current, key);
4457
4783
  onChangeRef.current?.(null);