@libraz/mejiro-react 0.8.0 → 0.9.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
@@ -114,6 +114,11 @@ function MejiroDropZone({
114
114
  className: rootClass,
115
115
  style,
116
116
  onClick: () => inputRef.current?.click(),
117
+ onKeyDown: (e) => {
118
+ if (e.key !== "Enter" && e.key !== " ") return;
119
+ e.preventDefault();
120
+ inputRef.current?.click();
121
+ },
117
122
  onDragOver: (e) => {
118
123
  e.preventDefault();
119
124
  setDragover(true);
@@ -150,8 +155,12 @@ function MejiroDropZone({
150
155
  }
151
156
 
152
157
  // src/MejiroEditor.tsx
153
- import { EditableEpub } from "@libraz/mejiro/epub";
154
- import { useCallback as useCallback7, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef10, useState as useState8 } from "react";
158
+ import {
159
+ cloneEditableEpubBook,
160
+ EditableEpub,
161
+ exportEditableEpub
162
+ } from "@libraz/mejiro/epub";
163
+ import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef10, useState as useState8 } from "react";
155
164
 
156
165
  // src/MejiroReader.tsx
157
166
  import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
@@ -160,7 +169,7 @@ import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
160
169
  import {
161
170
  forwardRef,
162
171
  useCallback as useCallback6,
163
- useEffect as useEffect6,
172
+ useEffect as useEffect7,
164
173
  useImperativeHandle,
165
174
  useLayoutEffect as useLayoutEffect4,
166
175
  useMemo as useMemo4,
@@ -190,6 +199,9 @@ import {
190
199
  // src/MejiroPageView.tsx
191
200
  import { normalizeFontFamily } from "@libraz/mejiro/browser";
192
201
 
202
+ // src/MejiroPage.tsx
203
+ import { paragraphClassName } from "@libraz/mejiro/render";
204
+
193
205
  // src/renderInlineNode.tsx
194
206
  import {
195
207
  segmentToInlineNode
@@ -228,12 +240,10 @@ function renderLine(line, lineIndex) {
228
240
  function MejiroPage({ page, className, style }) {
229
241
  const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
230
242
  return /* @__PURE__ */ jsx5("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
231
- let paraClass = "mejiro-paragraph";
232
- if (paragraph.headingLevel != null) {
233
- paraClass += ` mejiro-paragraph--h${paragraph.headingLevel}`;
234
- } else if (paragraph.isHeading) {
235
- paraClass += " mejiro-paragraph--heading";
236
- }
243
+ const paraClass = paragraphClassName(
244
+ paragraph.kind ?? (paragraph.isHeading ? "heading" : void 0),
245
+ paragraph.headingLevel
246
+ );
237
247
  return (
238
248
  // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
239
249
  /* @__PURE__ */ jsx5("div", { className: paraClass, children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
@@ -289,6 +299,7 @@ function MejiroScrollView({
289
299
  fontFamily,
290
300
  fontSize,
291
301
  lineSpacing,
302
+ slotMode,
292
303
  onVisiblePageChange,
293
304
  scrollToPage,
294
305
  pageGap = 24
@@ -303,7 +314,6 @@ function MejiroScrollView({
303
314
  return Array.from({ length: total }, (_, i) => layout.getPage(i));
304
315
  }, [layout]);
305
316
  const pageCount = pages.length;
306
- const useSlot = true;
307
317
  const contentStyle = { height: contentHeight };
308
318
  if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
309
319
  if (fontSize != null) contentStyle.fontSize = fontSize;
@@ -383,7 +393,7 @@ function MejiroScrollView({
383
393
  MejiroPageView,
384
394
  {
385
395
  result,
386
- slotMode: useSlot,
396
+ slotMode,
387
397
  fontFamily,
388
398
  lineSpacing,
389
399
  className: "mejiro-reader-page-content",
@@ -441,7 +451,14 @@ function MejiroSettingsPanel({
441
451
  type: "button",
442
452
  className: "mejiro-reader-btn mejiro-reader-btn--icon",
443
453
  "aria-label": messages.settingsSizeDown,
444
- onClick: () => patch({ fontSize: Math.max(minFontSize, settings.fontSize - 1) }),
454
+ onClick: () => patch({
455
+ fontSize: clampNumber(
456
+ settings.fontSize - 1,
457
+ minFontSize,
458
+ maxFontSize,
459
+ minFontSize
460
+ )
461
+ }),
445
462
  children: "A\u2212"
446
463
  }
447
464
  ),
@@ -469,7 +486,14 @@ function MejiroSettingsPanel({
469
486
  type: "button",
470
487
  className: "mejiro-reader-btn mejiro-reader-btn--icon",
471
488
  "aria-label": messages.settingsSizeUp,
472
- onClick: () => patch({ fontSize: Math.min(maxFontSize, settings.fontSize + 1) }),
489
+ onClick: () => patch({
490
+ fontSize: clampNumber(
491
+ settings.fontSize + 1,
492
+ minFontSize,
493
+ maxFontSize,
494
+ minFontSize
495
+ )
496
+ }),
473
497
  children: "A+"
474
498
  }
475
499
  )
@@ -627,6 +651,7 @@ function MejiroSelectionLayer({
627
651
  width: r.width,
628
652
  height: r.height,
629
653
  ...posStyle,
654
+ ...r.color != null ? { backgroundColor: r.color } : null,
630
655
  ...rectStyle
631
656
  }
632
657
  },
@@ -657,6 +682,7 @@ function MejiroSpread({
657
682
  rightHeader = {},
658
683
  leftHeader = {},
659
684
  images = [],
685
+ slotMode,
660
686
  renderPageHeader,
661
687
  indicator,
662
688
  onPrev,
@@ -665,6 +691,7 @@ function MejiroSpread({
665
691
  onImageResizePointerDown,
666
692
  onImageClose,
667
693
  anchorAtCoord,
694
+ spreadIdx,
668
695
  selectionRects,
669
696
  onSelectionChange,
670
697
  singlePage = false,
@@ -673,7 +700,6 @@ function MejiroSpread({
673
700
  }) {
674
701
  const messages = useI18n();
675
702
  const hasImages = images.length > 0;
676
- const useSlot = true;
677
703
  const selectionStartRef = useRef3(null);
678
704
  const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
679
705
  const gestureRef = useRef3(null);
@@ -747,6 +773,7 @@ function MejiroSpread({
747
773
  if (fontFamily) contentStyle.fontFamily = normalizeFontFamily4(fontFamily);
748
774
  if (fontSize != null) contentStyle.fontSize = fontSize;
749
775
  if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
776
+ const visibleRects = selectionRects && spreadIdx != null ? selectionRects.filter((r) => r.spreadIdx === spreadIdx) : selectionRects;
750
777
  const renderPage = (side) => {
751
778
  const isRight = side === "right";
752
779
  const result = isRight ? spread.right : spread.left;
@@ -770,7 +797,7 @@ function MejiroSpread({
770
797
  MejiroPageView,
771
798
  {
772
799
  result,
773
- slotMode: useSlot,
800
+ slotMode,
774
801
  fontFamily,
775
802
  lineSpacing,
776
803
  className: "mejiro-reader-page-content",
@@ -778,7 +805,7 @@ function MejiroSpread({
778
805
  },
779
806
  pageKey
780
807
  ),
781
- selectionRects && selectionRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: selectionRects, side })
808
+ visibleRects && visibleRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: visibleRects, side })
782
809
  ] }) }),
783
810
  isRight && images.map((item) => /* @__PURE__ */ jsx11(
784
811
  MejiroImageOverlay,
@@ -855,7 +882,10 @@ function MejiroStats({
855
882
  readingTimeLocale = "ja"
856
883
  }) {
857
884
  if (!chapter) return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats" });
858
- const totalChars = chapter.paragraphs.reduce((s, p) => s + p.text.length, 0);
885
+ const totalChars = chapter.paragraphs.reduce(
886
+ (s, p) => p.headingLevel != null ? s : s + [...p.text].length,
887
+ 0
888
+ );
859
889
  const totalRuby = chapter.paragraphs.reduce(
860
890
  (s, p) => s + p.inlineAnnotations.filter((a) => a.kind === "ruby").length,
861
891
  0
@@ -1008,12 +1038,14 @@ function useEpub(options = {}) {
1008
1038
  fetchOptionsRef.current = options.fetchOptions;
1009
1039
  const fetchEpubRef = useRef5(options.fetchEpub);
1010
1040
  fetchEpubRef.current = options.fetchEpub;
1041
+ const limitsRef = useRef5(options.limits);
1042
+ limitsRef.current = options.limits;
1011
1043
  const loadBuffer = useCallback2(async (buffer) => {
1012
1044
  const requestId = ++requestIdRef.current;
1013
1045
  setLoading(true);
1014
1046
  setError(null);
1015
1047
  try {
1016
- const book = await parseEpub(buffer);
1048
+ const book = await parseEpub(buffer, { limits: limitsRef.current });
1017
1049
  if (requestId !== requestIdRef.current) return null;
1018
1050
  setEpub(book);
1019
1051
  onLoadRef.current?.(book);
@@ -1034,7 +1066,7 @@ function useEpub(options = {}) {
1034
1066
  setLoading(true);
1035
1067
  setError(null);
1036
1068
  try {
1037
- const book = await parseEpub(await file.arrayBuffer());
1069
+ const book = await parseEpub(await file.arrayBuffer(), { limits: limitsRef.current });
1038
1070
  if (requestId !== requestIdRef.current) return null;
1039
1071
  setEpub(book);
1040
1072
  onLoadRef.current?.(book);
@@ -1064,7 +1096,7 @@ function useEpub(options = {}) {
1064
1096
  if (!res.ok) return null;
1065
1097
  buffer = await res.arrayBuffer();
1066
1098
  }
1067
- const book = await parseEpub(buffer);
1099
+ const book = await parseEpub(buffer, { limits: limitsRef.current });
1068
1100
  if (requestId !== requestIdRef.current) return null;
1069
1101
  setEpub(book);
1070
1102
  onLoadRef.current?.(book);
@@ -1095,23 +1127,79 @@ function useEpub(options = {}) {
1095
1127
 
1096
1128
  // src/useMejiroBook.ts
1097
1129
  import { MejiroBook } from "@libraz/mejiro/book";
1098
- import { useCallback as useCallback3, useRef as useRef6, useState as useState4 } from "react";
1099
- function useMejiroBook(initial) {
1130
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef6, useState as useState4 } from "react";
1131
+ function useMejiroBook(initial, options = {}) {
1100
1132
  const bookRef = useRef6(null);
1101
1133
  if (!bookRef.current) bookRef.current = new MejiroBook(initial);
1102
- const [options, setLocal] = useState4(initial);
1103
- const setOptions = useCallback3(async (next) => {
1104
- setLocal((prev) => ({ ...prev, ...next }));
1105
- await bookRef.current?.setOptions(next);
1134
+ const [snapshot, setLocal] = useState4(initial);
1135
+ const optionsRef = useRef6(options);
1136
+ optionsRef.current = options;
1137
+ const pendingRef = useRef6(null);
1138
+ const flush = useCallback3(() => {
1139
+ const pending = pendingRef.current;
1140
+ if (!pending) return;
1141
+ pendingRef.current = null;
1142
+ if (pending.timer) clearTimeout(pending.timer);
1143
+ const { waiters } = pending;
1144
+ const applied = bookRef.current?.setOptions(pending.patch) ?? Promise.resolve();
1145
+ void applied.then(
1146
+ () => {
1147
+ for (const waiter of waiters) waiter.resolve();
1148
+ },
1149
+ (err) => {
1150
+ const error = err instanceof Error ? err : new Error(String(err));
1151
+ const handler = optionsRef.current.onError;
1152
+ if (handler) {
1153
+ handler(error);
1154
+ for (const waiter of waiters) waiter.resolve();
1155
+ } else {
1156
+ for (const waiter of waiters) waiter.reject(error);
1157
+ }
1158
+ }
1159
+ );
1106
1160
  }, []);
1107
- return { book: bookRef.current, options, setOptions };
1161
+ const setOptions = useCallback3(
1162
+ (next) => {
1163
+ setLocal((prev) => ({ ...prev, ...next }));
1164
+ const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
1165
+ pendingRef.current = pending;
1166
+ Object.assign(pending.patch, next);
1167
+ const settled = new Promise((resolve, reject) => {
1168
+ pending.waiters.push({ resolve, reject });
1169
+ });
1170
+ if (pending.timer) clearTimeout(pending.timer);
1171
+ const debounceMs = optionsRef.current.debounceMs ?? 0;
1172
+ if (debounceMs > 0) {
1173
+ pending.timer = setTimeout(() => {
1174
+ pending.timer = null;
1175
+ flush();
1176
+ }, debounceMs);
1177
+ } else {
1178
+ pending.timer = null;
1179
+ flush();
1180
+ }
1181
+ return settled;
1182
+ },
1183
+ [flush]
1184
+ );
1185
+ useEffect4(
1186
+ () => () => {
1187
+ const pending = pendingRef.current;
1188
+ if (!pending) return;
1189
+ pendingRef.current = null;
1190
+ if (pending.timer) clearTimeout(pending.timer);
1191
+ for (const waiter of pending.waiters) waiter.resolve();
1192
+ },
1193
+ []
1194
+ );
1195
+ return { book: bookRef.current, options: snapshot, setOptions };
1108
1196
  }
1109
1197
 
1110
1198
  // src/useMultiImageOverlay.ts
1111
- import { moveImageOverlayRect, resizeImageOverlayRect } from "@libraz/mejiro";
1199
+ import { createOverlayDragSession } from "@libraz/mejiro/browser";
1112
1200
  import {
1113
1201
  useCallback as useCallback4,
1114
- useEffect as useEffect4,
1202
+ useEffect as useEffect5,
1115
1203
  useMemo as useMemo3,
1116
1204
  useRef as useRef7,
1117
1205
  useState as useState5
@@ -1133,7 +1221,7 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
1133
1221
  const onUpdateRef = useRef7(options.onUpdate);
1134
1222
  onUpdateRef.current = options.onUpdate;
1135
1223
  const activeDragCleanupsRef = useRef7(/* @__PURE__ */ new Set());
1136
- useEffect4(
1224
+ useEffect5(
1137
1225
  () => () => {
1138
1226
  for (const cleanup of activeDragCleanupsRef.current) cleanup();
1139
1227
  activeDragCleanupsRef.current.clear();
@@ -1247,32 +1335,19 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
1247
1335
  e.preventDefault();
1248
1336
  const found = findById(id);
1249
1337
  if (!found) return;
1250
- const startX = e.clientX;
1251
- const startY = e.clientY;
1252
- const start = { ...found.item.rect };
1253
1338
  const target = e.currentTarget;
1254
- target.setPointerCapture(e.nativeEvent.pointerId);
1255
- target.classList.add("is-dragging");
1256
- let rafId = 0;
1257
- const onMove = (me) => {
1258
- const dx = me.clientX - startX;
1259
- const dy = me.clientY - startY;
1260
- const r = moveImageOverlayRect(start, dx, dy);
1261
- cancelAnimationFrame(rafId);
1262
- rafId = requestAnimationFrame(() => updateImage(id, r));
1263
- };
1264
- let cleanup;
1265
- const onUp = () => cleanup();
1266
- cleanup = () => {
1267
- cancelAnimationFrame(rafId);
1268
- target.classList.remove("is-dragging");
1269
- document.removeEventListener("pointermove", onMove);
1270
- document.removeEventListener("pointerup", onUp);
1271
- activeDragCleanupsRef.current.delete(cleanup);
1272
- };
1273
- document.addEventListener("pointermove", onMove);
1274
- document.addEventListener("pointerup", onUp);
1275
- activeDragCleanupsRef.current.add(cleanup);
1339
+ createOverlayDragSession({
1340
+ mode: "move",
1341
+ rect: found.item.rect,
1342
+ startX: e.clientX,
1343
+ startY: e.clientY,
1344
+ pointerId: e.nativeEvent.pointerId,
1345
+ captureElement: target,
1346
+ activeElement: target,
1347
+ dragClass: "is-dragging",
1348
+ registry: activeDragCleanupsRef.current,
1349
+ onChange: (rect) => updateImage(id, rect)
1350
+ });
1276
1351
  },
1277
1352
  [findById, updateImage]
1278
1353
  );
@@ -1282,36 +1357,23 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
1282
1357
  e.stopPropagation();
1283
1358
  const found = findById(id);
1284
1359
  if (!found) return;
1285
- const startX = e.clientX;
1286
- const startY = e.clientY;
1287
- const start = { ...found.item.rect };
1288
1360
  const target = e.currentTarget;
1289
- target.setPointerCapture(e.nativeEvent.pointerId);
1290
- target.parentElement?.classList.add("is-dragging");
1291
- let rafId = 0;
1292
- const onMove = (me) => {
1293
- const dx = me.clientX - startX;
1294
- const dy = me.clientY - startY;
1295
- const r = resizeImageOverlayRect(start, dx, dy);
1296
- cancelAnimationFrame(rafId);
1297
- rafId = requestAnimationFrame(() => updateImage(id, r));
1298
- };
1299
- let cleanup;
1300
- const onUp = () => cleanup();
1301
- cleanup = () => {
1302
- cancelAnimationFrame(rafId);
1303
- target.parentElement?.classList.remove("is-dragging");
1304
- document.removeEventListener("pointermove", onMove);
1305
- document.removeEventListener("pointerup", onUp);
1306
- activeDragCleanupsRef.current.delete(cleanup);
1307
- };
1308
- document.addEventListener("pointermove", onMove);
1309
- document.addEventListener("pointerup", onUp);
1310
- activeDragCleanupsRef.current.add(cleanup);
1361
+ createOverlayDragSession({
1362
+ mode: "resize",
1363
+ rect: found.item.rect,
1364
+ startX: e.clientX,
1365
+ startY: e.clientY,
1366
+ pointerId: e.nativeEvent.pointerId,
1367
+ captureElement: target,
1368
+ activeElement: target.parentElement,
1369
+ dragClass: "is-dragging",
1370
+ registry: activeDragCleanupsRef.current,
1371
+ onChange: (rect) => updateImage(id, rect)
1372
+ });
1311
1373
  },
1312
1374
  [findById, updateImage]
1313
1375
  );
1314
- useEffect4(() => {
1376
+ useEffect5(() => {
1315
1377
  if (!layout) return;
1316
1378
  for (const si of imagesRef.current.keys()) syncSpread(si);
1317
1379
  }, [layout, syncSpread]);
@@ -1331,7 +1393,16 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
1331
1393
  }
1332
1394
 
1333
1395
  // src/useSpread.ts
1334
- import { useCallback as useCallback5, useEffect as useEffect5, useLayoutEffect as useLayoutEffect3, useRef as useRef8, useState as useState6 } from "react";
1396
+ import { useCallback as useCallback5, useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef8, useState as useState6 } from "react";
1397
+ function ignoreNavigationKey(e) {
1398
+ if (e.defaultPrevented) return true;
1399
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
1400
+ const target = e.target;
1401
+ if (!target) return false;
1402
+ if (target.isContentEditable) return true;
1403
+ const tag = target.tagName;
1404
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
1405
+ }
1335
1406
  function useSpread(layout, options = {}) {
1336
1407
  const enableKeyboard = options.enableKeyboard ?? true;
1337
1408
  const turnDuration = options.turnDuration ?? 180;
@@ -1370,7 +1441,7 @@ function useSpread(layout, options = {}) {
1370
1441
  }
1371
1442
  setSpread(layout.getSpread(0));
1372
1443
  }, [layout]);
1373
- useEffect5(() => {
1444
+ useEffect6(() => {
1374
1445
  if (!layout) return;
1375
1446
  setSpread(layout.getSpread(spreadIdx));
1376
1447
  const last = lastEmittedRef.current;
@@ -1417,17 +1488,18 @@ function useSpread(layout, options = {}) {
1417
1488
  }, []);
1418
1489
  const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
1419
1490
  const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
1420
- useEffect5(() => {
1491
+ useEffect6(() => {
1421
1492
  if (!enableKeyboard) return;
1422
1493
  const onKey = (e) => {
1423
1494
  if (!layoutRef.current) return;
1495
+ if (ignoreNavigationKey(e)) return;
1424
1496
  if (e.key === "ArrowLeft") next();
1425
1497
  else if (e.key === "ArrowRight") prev();
1426
1498
  };
1427
1499
  window.addEventListener("keydown", onKey);
1428
1500
  return () => window.removeEventListener("keydown", onKey);
1429
1501
  }, [enableKeyboard, next, prev]);
1430
- useEffect5(
1502
+ useEffect6(
1431
1503
  () => () => {
1432
1504
  if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
1433
1505
  },
@@ -1449,6 +1521,7 @@ function useSpread(layout, options = {}) {
1449
1521
 
1450
1522
  // src/MejiroReader.tsx
1451
1523
  import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
1524
+ var OPTIONS_DEBOUNCE_MS = 60;
1452
1525
  function MejiroReaderInner(props, ref) {
1453
1526
  const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
1454
1527
  const dialectProp = "dialect" in props ? props.dialect : void 0;
@@ -1467,6 +1540,7 @@ function MejiroReaderInner(props, ref) {
1467
1540
  enableSurfaceTap = true,
1468
1541
  fallback,
1469
1542
  fetchOptions,
1543
+ limits,
1470
1544
  fetchEpub: fetchEpubFn,
1471
1545
  locale,
1472
1546
  messages,
@@ -1502,7 +1576,7 @@ function MejiroReaderInner(props, ref) {
1502
1576
  const chapter = chapterProp ?? chapterState;
1503
1577
  const [chromeHidden, setChromeHidden] = useState7(false);
1504
1578
  const [autoSingle, setAutoSingle] = useState7(false);
1505
- useEffect6(() => {
1579
+ useEffect7(() => {
1506
1580
  if (!settingsOpen) return;
1507
1581
  const onKey = (e2) => {
1508
1582
  if (e2.key === "Escape") setSettingsOpen(false);
@@ -1510,7 +1584,7 @@ function MejiroReaderInner(props, ref) {
1510
1584
  window.addEventListener("keydown", onKey);
1511
1585
  return () => window.removeEventListener("keydown", onKey);
1512
1586
  }, [settingsOpen]);
1513
- useEffect6(() => {
1587
+ useEffect7(() => {
1514
1588
  if (spreadMode !== "auto") return;
1515
1589
  const surface = surfaceRef.current;
1516
1590
  if (!surface) return;
@@ -1547,15 +1621,26 @@ function MejiroReaderInner(props, ref) {
1547
1621
  () => ({ ...DEFAULT_BOOK_OPTIONS, ...optionsProp ?? {} }),
1548
1622
  [optionsProp]
1549
1623
  );
1550
- const { book, options: bookOptions, setOptions } = useMejiroBook(resolvedOptions);
1551
- const didSyncOptionsPropRef = useRef9(false);
1552
- useEffect6(() => {
1553
- if (!didSyncOptionsPropRef.current) {
1554
- didSyncOptionsPropRef.current = true;
1555
- return;
1556
- }
1557
- void setOptions(resolvedOptions);
1558
- }, [resolvedOptions, setOptions]);
1624
+ const onErrorRef = useRef9(onError);
1625
+ onErrorRef.current = onError;
1626
+ const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
1627
+ const {
1628
+ book,
1629
+ options: bookOptions,
1630
+ setOptions
1631
+ } = useMejiroBook(resolvedOptions, {
1632
+ debounceMs: OPTIONS_DEBOUNCE_MS,
1633
+ onError: reportError
1634
+ });
1635
+ const optionsPropKey = JSON.stringify(resolvedOptions);
1636
+ const syncedOptionsPropKeyRef = useRef9(optionsPropKey);
1637
+ const resolvedOptionsRef = useRef9(resolvedOptions);
1638
+ resolvedOptionsRef.current = resolvedOptions;
1639
+ useEffect7(() => {
1640
+ if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
1641
+ syncedOptionsPropKeyRef.current = optionsPropKey;
1642
+ void setOptions(resolvedOptionsRef.current);
1643
+ }, [optionsPropKey, setOptions]);
1559
1644
  const synthesizedEpub = useMemo4(() => {
1560
1645
  if (manuscriptProp === void 0) return null;
1561
1646
  return manuscriptToEpubBook(manuscriptProp, { dialect: dialectProp });
@@ -1565,6 +1650,7 @@ function MejiroReaderInner(props, ref) {
1565
1650
  // book (or a synthesized manuscript book) is supplied.
1566
1651
  defaultUrl: epubProp !== void 0 || manuscriptProp !== void 0 ? void 0 : epubUrl,
1567
1652
  fetchOptions,
1653
+ limits,
1568
1654
  fetchEpub: fetchEpubFn,
1569
1655
  onLoad: (b) => {
1570
1656
  if (chapterProp == null) setChapterState(0);
@@ -1574,9 +1660,7 @@ function MejiroReaderInner(props, ref) {
1574
1660
  const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
1575
1661
  const isManuscriptSource = manuscriptProp !== void 0;
1576
1662
  const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
1577
- const onErrorRef = useRef9(onError);
1578
- onErrorRef.current = onError;
1579
- useEffect6(() => {
1663
+ useEffect7(() => {
1580
1664
  if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
1581
1665
  }, [epubCtx.error]);
1582
1666
  const pageGeometryRef = useRef9(resolvedGeometry);
@@ -1622,20 +1706,24 @@ function MejiroReaderInner(props, ref) {
1622
1706
  bookOptionsRef.current = bookOptions;
1623
1707
  const recomputeRef = useRef9(layoutCtx.recompute);
1624
1708
  recomputeRef.current = layoutCtx.recompute;
1625
- useEffect6(() => {
1709
+ useEffect7(() => {
1626
1710
  if (optionsKeyRef.current === optionsKey) return;
1627
1711
  optionsKeyRef.current = optionsKey;
1628
1712
  const timer = setTimeout(() => {
1629
1713
  void (async () => {
1630
- await setOptions({ ...bookOptionsRef.current });
1631
- await recomputeRef.current({ blank: false });
1714
+ try {
1715
+ await setOptions({ ...bookOptionsRef.current });
1716
+ await recomputeRef.current({ blank: false });
1717
+ } catch (err) {
1718
+ reportError(err instanceof Error ? err : new Error(String(err)));
1719
+ }
1632
1720
  })();
1633
- }, 60);
1721
+ }, OPTIONS_DEBOUNCE_MS);
1634
1722
  return () => clearTimeout(timer);
1635
- }, [optionsKey, setOptions]);
1723
+ }, [optionsKey, setOptions, reportError]);
1636
1724
  const geometryKey = JSON.stringify(resolvedGeometry ?? null);
1637
1725
  const geometryKeyRef = useRef9(geometryKey);
1638
- useEffect6(() => {
1726
+ useEffect7(() => {
1639
1727
  if (geometryKeyRef.current === geometryKey) return;
1640
1728
  geometryKeyRef.current = geometryKey;
1641
1729
  void recomputeRef.current({ blank: false });
@@ -1652,7 +1740,7 @@ function MejiroReaderInner(props, ref) {
1652
1740
  start: annotation.start,
1653
1741
  end: annotation.end
1654
1742
  });
1655
- for (const rect of rects) result.push(rect);
1743
+ for (const rect of rects) result.push({ ...rect, color: annotation.color });
1656
1744
  }
1657
1745
  return result;
1658
1746
  }, [annotations, layoutCtx.layout, chapter]);
@@ -1660,7 +1748,7 @@ function MejiroReaderInner(props, ref) {
1660
1748
  clearImagesRef.current = imageCtx.clearImages;
1661
1749
  const onLoadRef = useRef9(onLoad);
1662
1750
  onLoadRef.current = onLoad;
1663
- useEffect6(() => {
1751
+ useEffect7(() => {
1664
1752
  if (!controlled) return;
1665
1753
  if (chapterIsUncontrolled) setChapterState(0);
1666
1754
  clearImagesRef.current();
@@ -1669,10 +1757,18 @@ function MejiroReaderInner(props, ref) {
1669
1757
  }, [controlled, e, book, chapterIsUncontrolled]);
1670
1758
  const spreadGoToRef = useRef9(spreadCtx.goTo);
1671
1759
  spreadGoToRef.current = spreadCtx.goTo;
1672
- useEffect6(() => {
1673
- if (spreadIdxProp == null) return;
1674
- spreadGoToRef.current(spreadIdxProp);
1675
- }, [spreadIdxProp]);
1760
+ const appliedSpreadIdxPropRef = useRef9(spreadIdxProp);
1761
+ useEffect7(() => {
1762
+ if (spreadIdxProp == null) {
1763
+ appliedSpreadIdxPropRef.current = spreadIdxProp;
1764
+ return;
1765
+ }
1766
+ const propChanged = appliedSpreadIdxPropRef.current !== spreadIdxProp;
1767
+ appliedSpreadIdxPropRef.current = spreadIdxProp;
1768
+ if (spreadCtx.spreadIdx === spreadIdxProp) return;
1769
+ if (propChanged) spreadGoToRef.current(spreadIdxProp);
1770
+ else setSpreadRef.current(spreadIdxProp);
1771
+ }, [spreadIdxProp, spreadCtx.spreadIdx]);
1676
1772
  useLayoutEffect4(() => {
1677
1773
  const next = spreadIdxPropRef.current;
1678
1774
  if (next == null || !layoutCtx.layout) return;
@@ -1711,10 +1807,10 @@ function MejiroReaderInner(props, ref) {
1711
1807
  spreadGoToRef.current(loc.spreadIdx);
1712
1808
  pending.resolve();
1713
1809
  }, [chapter, layout]);
1714
- useEffect6(() => {
1810
+ useEffect7(() => {
1715
1811
  tryApplyPendingAnchor();
1716
1812
  }, [tryApplyPendingAnchor]);
1717
- useEffect6(
1813
+ useEffect7(
1718
1814
  () => () => {
1719
1815
  pendingAnchorRef.current?.resolve();
1720
1816
  pendingAnchorRef.current = null;
@@ -1727,7 +1823,7 @@ function MejiroReaderInner(props, ref) {
1727
1823
  onPageReadRef.current = onPageRead;
1728
1824
  const onChapterCompletedRef = useRef9(onChapterCompleted);
1729
1825
  onChapterCompletedRef.current = onChapterCompleted;
1730
- useEffect6(() => {
1826
+ useEffect7(() => {
1731
1827
  const now = typeof performance !== "undefined" ? performance.now() : Date.now();
1732
1828
  if (didMountRef.current && dwellRef.current) {
1733
1829
  onPageReadRef.current?.(dwellRef.current.anchor, now - dwellRef.current.ts);
@@ -1749,7 +1845,7 @@ function MejiroReaderInner(props, ref) {
1749
1845
  }
1750
1846
  }, [chapter, spreadCtx.spreadIdx, spreadCtx.totalSpreads, layout, emit]);
1751
1847
  const prevTurningRef = useRef9(false);
1752
- useEffect6(() => {
1848
+ useEffect7(() => {
1753
1849
  if (spreadCtx.turning && !prevTurningRef.current) {
1754
1850
  emit("turnStart", { from: spreadCtx.spreadIdx });
1755
1851
  } else if (!spreadCtx.turning && prevTurningRef.current) {
@@ -1998,6 +2094,7 @@ function MejiroReaderInner(props, ref) {
1998
2094
  onImagePointerDown: imageCtx.onOverlayPointerDown,
1999
2095
  onImageResizePointerDown: imageCtx.onResizePointerDown,
2000
2096
  onImageClose: imageCtx.removeImage,
2097
+ spreadIdx: spreadCtx.spreadIdx,
2001
2098
  selectionRects: annotationRects.length ? annotationRects : void 0
2002
2099
  },
2003
2100
  `${chapter}-${spreadCtx.spreadIdx}-${layoutCtx.pageWidth}x${layoutCtx.pageHeight}`
@@ -2030,28 +2127,6 @@ MejiroReader.displayName = "MejiroReader";
2030
2127
 
2031
2128
  // src/MejiroEditor.tsx
2032
2129
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
2033
- function cloneBook(book) {
2034
- return {
2035
- ...book,
2036
- chapters: book.chapters.map((chapter) => ({
2037
- ...chapter,
2038
- blocks: chapter.blocks.map(
2039
- (block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
2040
- ),
2041
- imageAssets: new Map(chapter.imageAssets),
2042
- originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
2043
- paragraphs: chapter.paragraphs.map((paragraph) => ({
2044
- ...paragraph,
2045
- inlineAnnotations: [...paragraph.inlineAnnotations]
2046
- })),
2047
- paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
2048
- })),
2049
- packageData: {
2050
- ...book.packageData,
2051
- files: new Map(book.packageData.files)
2052
- }
2053
- };
2054
- }
2055
2130
  function utf16ToCodepoint(text, utf16Offset) {
2056
2131
  let cp = 0;
2057
2132
  let i = 0;
@@ -2063,15 +2138,29 @@ function utf16ToCodepoint(text, utf16Offset) {
2063
2138
  }
2064
2139
  return cp;
2065
2140
  }
2066
- function applyWatermark(editor, watermark) {
2067
- editor.transaction(() => {
2068
- for (let i = 0; i < editor.chapters.length; i++) {
2069
- editor.insertParagraph(i, 0, {
2070
- text: `[mejiro-watermark] ${watermark.text}`,
2071
- inlineAnnotations: []
2072
- });
2073
- }
2074
- });
2141
+ var WATERMARK_PREFIX = "[mejiro-watermark]";
2142
+ var WATERMARK_BLOCK_ID = "mejiro-watermark";
2143
+ function watermarkedBook(book, watermark) {
2144
+ const copy = cloneEditableEpubBook(book);
2145
+ const text = `${WATERMARK_PREFIX} ${watermark.text}`;
2146
+ for (const chapter of copy.chapters) {
2147
+ chapter.blocks.unshift({
2148
+ kind: "paragraph",
2149
+ id: watermarkBlockId(chapter),
2150
+ text,
2151
+ inlineAnnotations: []
2152
+ });
2153
+ chapter.paragraphs.unshift({ text, inlineAnnotations: [] });
2154
+ chapter.isDirty = true;
2155
+ }
2156
+ return copy;
2157
+ }
2158
+ function watermarkBlockId(chapter) {
2159
+ const used = new Set(chapter.blocks.map((block) => block.id));
2160
+ let id = WATERMARK_BLOCK_ID;
2161
+ let suffix = 2;
2162
+ while (used.has(id)) id = `${WATERMARK_BLOCK_ID}-${suffix++}`;
2163
+ return id;
2075
2164
  }
2076
2165
  function MejiroEditor({
2077
2166
  epubUrl,
@@ -2083,6 +2172,7 @@ function MejiroEditor({
2083
2172
  onBeforeExport,
2084
2173
  exportPolicy,
2085
2174
  assetResolver,
2175
+ limits,
2086
2176
  onLoad,
2087
2177
  onExport,
2088
2178
  onError
@@ -2102,25 +2192,27 @@ function MejiroEditor({
2102
2192
  const loadRequestIdRef = useRef10(0);
2103
2193
  const onLoadRef = useRef10(onLoad);
2104
2194
  const onErrorRef = useRef10(onError);
2195
+ const limitsRef = useRef10(limits);
2196
+ limitsRef.current = limits;
2105
2197
  const book = editor?.book ?? null;
2106
2198
  const chapter = book?.chapters[selection.chapter] ?? null;
2107
2199
  const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
2108
2200
  const textDirty = paragraph ? text !== paragraph.text : false;
2109
2201
  const previewBook = useMemo5(() => {
2110
2202
  void revision;
2111
- return book ? cloneBook(book) : null;
2203
+ return book ? cloneEditableEpubBook(book) : null;
2112
2204
  }, [book, revision]);
2113
- useEffect7(() => {
2205
+ useEffect8(() => {
2114
2206
  onLoadRef.current = onLoad;
2115
2207
  }, [onLoad]);
2116
- useEffect7(() => {
2208
+ useEffect8(() => {
2117
2209
  onErrorRef.current = onError;
2118
2210
  }, [onError]);
2119
2211
  const loadBufferForRequest = useCallback7(async (buffer, requestId) => {
2120
2212
  setLoading(true);
2121
2213
  setError(null);
2122
2214
  try {
2123
- const next = await EditableEpub.load(buffer);
2215
+ const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
2124
2216
  if (requestId !== loadRequestIdRef.current) return;
2125
2217
  setEditor(next);
2126
2218
  setSelection({ chapter: 0, paragraph: 0 });
@@ -2151,7 +2243,7 @@ function MejiroEditor({
2151
2243
  }
2152
2244
  }
2153
2245
  }
2154
- useEffect7(() => {
2246
+ useEffect8(() => {
2155
2247
  if (!epubUrl) return;
2156
2248
  let cancelled = false;
2157
2249
  const requestId = ++loadRequestIdRef.current;
@@ -2178,13 +2270,15 @@ function MejiroEditor({
2178
2270
  cancelled = true;
2179
2271
  };
2180
2272
  }, [epubUrl, loadBufferForRequest]);
2181
- useEffect7(() => {
2273
+ useEffect8(() => {
2182
2274
  setText(paragraph?.text ?? "");
2183
2275
  setRubyStart(0);
2184
2276
  setRubyEnd(Math.min(1, [...paragraph?.text ?? ""].length));
2185
2277
  setRubyText("");
2186
2278
  }, [paragraph]);
2187
2279
  function selectParagraph(chapterIndex, paragraphIndex) {
2280
+ if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
2281
+ if (textDirty) applyText();
2188
2282
  setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
2189
2283
  }
2190
2284
  function captureRubyRange() {
@@ -2240,8 +2334,9 @@ function MejiroEditor({
2240
2334
  }
2241
2335
  async function exportEpub() {
2242
2336
  if (!editor) return;
2243
- if (exportPolicy?.watermark) applyWatermark(editor, exportPolicy.watermark);
2244
- let buffer = await editor.export(assetResolver ? { assetResolver } : void 0);
2337
+ const watermark = exportPolicy?.watermark;
2338
+ const source = watermark ? watermarkedBook(editor.book, watermark) : editor.book;
2339
+ let buffer = await exportEditableEpub(source, assetResolver ? { assetResolver } : void 0);
2245
2340
  if (exportPolicy?.encrypt) buffer = await exportPolicy.encrypt(buffer);
2246
2341
  const decision = await onBeforeExport?.(buffer);
2247
2342
  onExport?.(buffer);
@@ -2377,7 +2472,7 @@ function paragraphBlockId(chapter, paragraphIndex) {
2377
2472
 
2378
2473
  // src/MejiroManuscriptEditor.tsx
2379
2474
  import { EpubProject } from "@libraz/mejiro/epub";
2380
- import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef13, useState as useState11 } from "react";
2475
+ import { useCallback as useCallback9, useEffect as useEffect10, useRef as useRef13, useState as useState11 } from "react";
2381
2476
 
2382
2477
  // src/MejiroNotationHighlighter.tsx
2383
2478
  import { tokenizeManuscriptSource } from "@libraz/mejiro";
@@ -2495,7 +2590,7 @@ function buildSegments(text, dialect) {
2495
2590
  }
2496
2591
 
2497
2592
  // src/useManuscriptDraft.ts
2498
- import { useCallback as useCallback8, useEffect as useEffect8, useRef as useRef12, useState as useState10 } from "react";
2593
+ import { useCallback as useCallback8, useEffect as useEffect9, useRef as useRef12, useState as useState10 } from "react";
2499
2594
  var DEFAULT_DELAY = 800;
2500
2595
  function defaultChapter(index, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
2501
2596
  return { id: `chapter-${Date.now()}-${index}`, title: titleFor(index), body: bodyFor(index) };
@@ -2524,33 +2619,45 @@ function useManuscriptDraft(options = {}) {
2524
2619
  payloadRef.current = autosavePayload;
2525
2620
  const chaptersRef = useRef12(chapters);
2526
2621
  chaptersRef.current = chapters;
2622
+ const selectedRef = useRef12(selected);
2623
+ selectedRef.current = selected;
2527
2624
  const didMountRef = useRef12(false);
2528
2625
  const dirtyRef = useRef12(false);
2529
2626
  const mountedRef = useRef12(true);
2627
+ const revisionRef = useRef12(0);
2628
+ const inFlightRevisionRef = useRef12(-1);
2530
2629
  const flushAutosave = useCallback8(() => {
2531
2630
  const callback = saveRef.current;
2532
- if (!(callback && dirtyRef.current)) return;
2533
- dirtyRef.current = false;
2631
+ if (!(callback && dirtyRef.current) || inFlightRevisionRef.current === revisionRef.current) {
2632
+ return;
2633
+ }
2634
+ const revision = revisionRef.current;
2635
+ inFlightRevisionRef.current = revision;
2534
2636
  const payload = payloadRef.current ? payloadRef.current(chaptersRef.current) : chaptersRef.current;
2535
- void Promise.resolve(callback(payload)).catch((err) => {
2637
+ void Promise.resolve(callback(payload)).then(() => {
2638
+ if (revisionRef.current === revision) dirtyRef.current = false;
2639
+ }).catch((err) => {
2536
2640
  if (!mountedRef.current) return;
2537
2641
  setAutosaveError(err instanceof Error ? err : new Error(String(err)));
2642
+ }).finally(() => {
2643
+ if (inFlightRevisionRef.current === revision) inFlightRevisionRef.current = -1;
2538
2644
  });
2539
2645
  }, []);
2540
- useEffect8(() => {
2646
+ useEffect9(() => {
2541
2647
  if (!didMountRef.current) {
2542
2648
  didMountRef.current = true;
2543
2649
  return void 0;
2544
2650
  }
2545
2651
  if (!saveRef.current) return void 0;
2546
2652
  dirtyRef.current = true;
2653
+ revisionRef.current += 1;
2547
2654
  setAutosaveError(null);
2548
2655
  const timer = setTimeout(() => {
2549
2656
  flushAutosave();
2550
2657
  }, autosaveDelay);
2551
2658
  return () => clearTimeout(timer);
2552
2659
  }, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
2553
- useEffect8(() => {
2660
+ useEffect9(() => {
2554
2661
  mountedRef.current = true;
2555
2662
  const handleBeforeUnload = () => flushAutosave();
2556
2663
  window.addEventListener("beforeunload", handleBeforeUnload);
@@ -2560,31 +2667,40 @@ function useManuscriptDraft(options = {}) {
2560
2667
  window.removeEventListener("beforeunload", handleBeforeUnload);
2561
2668
  };
2562
2669
  }, [flushAutosave]);
2563
- const setSelected = useCallback8(
2564
- (index) => {
2565
- setSelectedState(() => Math.max(0, Math.min(index, chapters.length - 1)));
2566
- },
2567
- [chapters.length]
2568
- );
2670
+ const commit = useCallback8((nextChapters, nextSelected) => {
2671
+ chaptersRef.current = nextChapters;
2672
+ selectedRef.current = nextSelected;
2673
+ setChaptersState(nextChapters);
2674
+ setSelectedState(nextSelected);
2675
+ }, []);
2676
+ const setSelected = useCallback8((index) => {
2677
+ const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
2678
+ selectedRef.current = next;
2679
+ setSelectedState(next);
2680
+ }, []);
2569
2681
  const setChapters = useCallback8(
2570
2682
  (next) => {
2571
2683
  const normalized = next.length ? next : [defaultChapter(0, titleForRef.current, bodyForRef.current)];
2572
- setChaptersState(normalized);
2573
- setSelectedState((current) => {
2574
- const selectedId = chapters[current]?.id;
2575
- const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
2576
- return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
2577
- });
2684
+ const prev = selectedRef.current;
2685
+ const selectedId = chaptersRef.current[prev]?.id;
2686
+ const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
2687
+ commit(
2688
+ normalized,
2689
+ nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
2690
+ );
2578
2691
  },
2579
- [chapters]
2692
+ [commit]
2580
2693
  );
2581
2694
  const patchChapter = useCallback8((index, patch) => {
2582
- setChaptersState(
2583
- (current) => current.map((chapter, i) => i === index ? { ...chapter, ...patch } : chapter)
2695
+ const next = chaptersRef.current.map(
2696
+ (chapter, i) => i === index ? { ...chapter, ...patch } : chapter
2584
2697
  );
2698
+ chaptersRef.current = next;
2699
+ setChaptersState(next);
2585
2700
  }, []);
2586
- const addChapter = useCallback8((chapter = {}) => {
2587
- setChaptersState((current) => {
2701
+ const addChapter = useCallback8(
2702
+ (chapter = {}) => {
2703
+ const current = chaptersRef.current;
2588
2704
  const generated = defaultChapter(current.length, titleForRef.current, bodyForRef.current);
2589
2705
  const next = [
2590
2706
  ...current,
@@ -2594,38 +2710,41 @@ function useManuscriptDraft(options = {}) {
2594
2710
  body: chapter.body ?? generated.body
2595
2711
  }
2596
2712
  ];
2597
- setSelectedState(next.length - 1);
2598
- return next;
2599
- });
2600
- }, []);
2601
- const removeChapter = useCallback8((index) => {
2602
- setChaptersState((current) => {
2603
- if (current.length <= 1) return current;
2713
+ commit(next, next.length - 1);
2714
+ },
2715
+ [commit]
2716
+ );
2717
+ const removeChapter = useCallback8(
2718
+ (index) => {
2719
+ const current = chaptersRef.current;
2720
+ if (current.length <= 1) return;
2604
2721
  const next = current.filter((_, i) => i !== index);
2605
- setSelectedState((prev) => {
2606
- if (prev === index) return Math.max(0, Math.min(index, next.length - 1));
2607
- if (index < prev) return prev - 1;
2608
- return Math.max(0, Math.min(prev, next.length - 1));
2609
- });
2610
- return next;
2611
- });
2612
- }, []);
2613
- const reorderChapters = useCallback8((from, to) => {
2614
- setChaptersState((current) => {
2615
- if (from < 0 || from >= current.length) return current;
2722
+ const prev = selectedRef.current;
2723
+ let nextSelected;
2724
+ if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
2725
+ else if (index < prev) nextSelected = prev - 1;
2726
+ else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
2727
+ commit(next, nextSelected);
2728
+ },
2729
+ [commit]
2730
+ );
2731
+ const reorderChapters = useCallback8(
2732
+ (from, to) => {
2733
+ const current = chaptersRef.current;
2734
+ if (from < 0 || from >= current.length) return;
2616
2735
  const next = [...current];
2617
2736
  const [moved] = next.splice(from, 1);
2618
2737
  const target = Math.max(0, Math.min(next.length, to));
2619
2738
  next.splice(target, 0, moved);
2620
- setSelectedState((prev) => {
2621
- if (prev === from) return target;
2622
- if (from < prev && target >= prev) return prev - 1;
2623
- if (from > prev && target <= prev) return prev + 1;
2624
- return prev;
2625
- });
2626
- return next;
2627
- });
2628
- }, []);
2739
+ const prev = selectedRef.current;
2740
+ let nextSelected = prev;
2741
+ if (prev === from) nextSelected = target;
2742
+ else if (from < prev && target >= prev) nextSelected = prev - 1;
2743
+ else if (from > prev && target <= prev) nextSelected = prev + 1;
2744
+ commit(next, nextSelected);
2745
+ },
2746
+ [commit]
2747
+ );
2629
2748
  return {
2630
2749
  chapters,
2631
2750
  selected,
@@ -2686,11 +2805,14 @@ function MejiroManuscriptEditor({
2686
2805
  cover: coverProp,
2687
2806
  onCoverChange,
2688
2807
  chapters: initialChapters,
2808
+ dialect = "mejiro",
2689
2809
  onAutosave,
2690
2810
  autosaveDelay,
2691
2811
  previewProps,
2692
2812
  assetResolver,
2693
- onExport
2813
+ onExport,
2814
+ onError,
2815
+ panelSide = "right"
2694
2816
  }) {
2695
2817
  const messages = useI18n();
2696
2818
  const coverInputRef = useRef13(null);
@@ -2704,13 +2826,13 @@ function MejiroManuscriptEditor({
2704
2826
  const title = titleControlled ? titleProp ?? "" : titleState;
2705
2827
  const author = authorControlled ? authorProp ?? "" : authorState;
2706
2828
  const cover = coverControlled ? coverProp ?? null : coverState;
2707
- useEffect9(() => {
2829
+ useEffect10(() => {
2708
2830
  if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
2709
2831
  }, [titleControlled, titleProp]);
2710
- useEffect9(() => {
2832
+ useEffect10(() => {
2711
2833
  if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
2712
2834
  }, [authorControlled, authorProp]);
2713
- useEffect9(() => {
2835
+ useEffect10(() => {
2714
2836
  if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
2715
2837
  }, [coverControlled, coverProp]);
2716
2838
  const setTitle = useCallback9(
@@ -2753,6 +2875,7 @@ function MejiroManuscriptEditor({
2753
2875
  } = draft;
2754
2876
  const current = chapters[selected] ?? chapters[0];
2755
2877
  const [draggingIndex, setDraggingIndex] = useState11(null);
2878
+ const [exportError, setExportError] = useState11(null);
2756
2879
  function wrapSelection(open, close) {
2757
2880
  const el = bodyTextareaRef.current;
2758
2881
  if (!el) return;
@@ -2771,26 +2894,44 @@ function MejiroManuscriptEditor({
2771
2894
  });
2772
2895
  }
2773
2896
  const exportEpub = useCallback9(async () => {
2774
- const project = EpubProject.fromManuscript({
2775
- metadata: { title, author: author || void 0 },
2776
- chapters: chapters.map((chapter) => ({
2777
- id: chapter.id,
2778
- title: chapter.title || messages.untitled,
2779
- body: chapter.body
2780
- }))
2781
- });
2782
- if (cover) {
2783
- project.setCover({
2784
- href: coverAssetHref(cover),
2785
- mediaType: cover.type || void 0,
2786
- data: await cover.arrayBuffer()
2897
+ try {
2898
+ const project = EpubProject.fromManuscript({
2899
+ metadata: { title, author: author || void 0 },
2900
+ dialect,
2901
+ chapters: chapters.map((chapter) => ({
2902
+ id: chapter.id,
2903
+ title: chapter.title || messages.untitled,
2904
+ body: chapter.body
2905
+ }))
2787
2906
  });
2907
+ if (cover) {
2908
+ project.setCover({
2909
+ href: coverAssetHref(cover),
2910
+ mediaType: cover.type || void 0,
2911
+ data: await cover.arrayBuffer()
2912
+ });
2913
+ }
2914
+ const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
2915
+ setExportError(null);
2916
+ onExport?.(buffer);
2917
+ downloadEpub(buffer, title);
2918
+ } catch (cause) {
2919
+ const error = cause instanceof Error ? cause : new Error(String(cause));
2920
+ setExportError(error);
2921
+ onError?.(error);
2788
2922
  }
2789
- const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
2790
- onExport?.(buffer);
2791
- downloadEpub(buffer, title);
2792
- }, [assetResolver, author, chapters, cover, messages.untitled, onExport, title]);
2793
- return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor mejiro-manuscript-editor", children: [
2923
+ }, [
2924
+ assetResolver,
2925
+ author,
2926
+ chapters,
2927
+ cover,
2928
+ dialect,
2929
+ messages.untitled,
2930
+ onError,
2931
+ onExport,
2932
+ title
2933
+ ]);
2934
+ return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor mejiro-manuscript-editor", "data-panel-side": panelSide, children: [
2794
2935
  /* @__PURE__ */ jsx16("main", { className: "mejiro-editor-preview", children: /* @__PURE__ */ jsx16(
2795
2936
  MejiroReader,
2796
2937
  {
@@ -2803,6 +2944,7 @@ function MejiroManuscriptEditor({
2803
2944
  title: chapter.title || messages.untitled,
2804
2945
  body: chapter.body
2805
2946
  })),
2947
+ dialect,
2806
2948
  fonts,
2807
2949
  chapter: selected,
2808
2950
  onChapterChange: setSelected,
@@ -2816,6 +2958,7 @@ function MejiroManuscriptEditor({
2816
2958
  /* @__PURE__ */ jsx16("small", { children: messages.manuscriptRubyHint })
2817
2959
  ] }),
2818
2960
  draft.autosaveError && /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-error", children: draft.autosaveError.message }),
2961
+ exportError && /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-error", children: exportError.message }),
2819
2962
  /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2820
2963
  /* @__PURE__ */ jsx16("span", { className: "mejiro-editor-label", children: messages.manuscriptMetadata }),
2821
2964
  /* @__PURE__ */ jsx16("input", { value: title, onChange: (event) => setTitle(event.target.value) }),
@@ -2905,6 +3048,7 @@ function MejiroManuscriptEditor({
2905
3048
  {
2906
3049
  ref: bodyTextareaRef,
2907
3050
  className: "mejiro-editor-manuscript",
3051
+ dialect,
2908
3052
  value: current.body,
2909
3053
  onChange: (body) => patchChapter(selected, { body })
2910
3054
  }
@@ -3025,7 +3169,7 @@ import {
3025
3169
  serializeAnnotations,
3026
3170
  sortAnnotations
3027
3171
  } from "@libraz/mejiro";
3028
- import { useCallback as useCallback10, useEffect as useEffect10, useRef as useRef14, useState as useState13 } from "react";
3172
+ import { useCallback as useCallback10, useEffect as useEffect11, useRef as useRef14, useState as useState13 } from "react";
3029
3173
  function resolveDefaultStorage() {
3030
3174
  if (typeof globalThis === "undefined") return null;
3031
3175
  const w = globalThis.localStorage;
@@ -3042,40 +3186,63 @@ function useAnnotations(options) {
3042
3186
  return [];
3043
3187
  }
3044
3188
  });
3045
- useEffect10(() => {
3046
- if (!storage) {
3047
- setAnnotations([]);
3048
- return;
3189
+ const storageRef = useRef14(storage);
3190
+ storageRef.current = storage;
3191
+ const annotationsRef = useRef14(annotations);
3192
+ const applyNext = useCallback10((next) => {
3193
+ annotationsRef.current = next;
3194
+ setAnnotations(next);
3195
+ }, []);
3196
+ const timerRef = useRef14(null);
3197
+ const pendingWriteRef = useRef14(null);
3198
+ const flushPending = useCallback10(() => {
3199
+ if (timerRef.current) {
3200
+ clearTimeout(timerRef.current);
3201
+ timerRef.current = null;
3049
3202
  }
3203
+ const write = pendingWriteRef.current;
3204
+ pendingWriteRef.current = null;
3205
+ if (!write) return;
3050
3206
  try {
3051
- setAnnotations(sortAnnotations(parseAnnotations(storage.getItem(key))));
3207
+ write();
3052
3208
  } catch {
3053
- setAnnotations([]);
3054
3209
  }
3055
- }, [storage, key]);
3056
- const timerRef = useRef14(null);
3057
- useEffect10(
3058
- () => () => {
3059
- if (timerRef.current) clearTimeout(timerRef.current);
3060
- },
3061
- []
3062
- );
3210
+ }, []);
3211
+ const cancelPending = useCallback10(() => {
3212
+ if (timerRef.current) {
3213
+ clearTimeout(timerRef.current);
3214
+ timerRef.current = null;
3215
+ }
3216
+ pendingWriteRef.current = null;
3217
+ }, []);
3218
+ useEffect11(() => {
3219
+ const currentStorage = storageRef.current;
3220
+ if (currentStorage) {
3221
+ try {
3222
+ applyNext(sortAnnotations(parseAnnotations(currentStorage.getItem(key))));
3223
+ } catch {
3224
+ applyNext([]);
3225
+ }
3226
+ } else {
3227
+ applyNext([]);
3228
+ }
3229
+ return flushPending;
3230
+ }, [key, applyNext, flushPending]);
3063
3231
  const onChangeRef = useRef14(options.onChange);
3064
3232
  onChangeRef.current = options.onChange;
3065
3233
  const commit = useCallback10(
3066
3234
  (next) => {
3067
- if (storage) {
3068
- if (timerRef.current) clearTimeout(timerRef.current);
3069
- timerRef.current = setTimeout(() => {
3070
- try {
3071
- storage.setItem(key, serializeAnnotations(next));
3072
- } catch {
3073
- }
3074
- }, throttleMs);
3235
+ const currentStorage = storageRef.current;
3236
+ if (currentStorage) {
3237
+ cancelPending();
3238
+ pendingWriteRef.current = () => {
3239
+ currentStorage.setItem(key, serializeAnnotations(next));
3240
+ };
3241
+ timerRef.current = setTimeout(flushPending, throttleMs);
3075
3242
  }
3076
3243
  onChangeRef.current?.(next);
3077
3244
  },
3078
- [storage, key, throttleMs]
3245
+ [key, throttleMs, cancelPending, flushPending]
3079
3246
  );
3080
3247
  const add = useCallback10(
3081
3248
  (input) => {
@@ -3084,61 +3251,60 @@ function useAnnotations(options) {
3084
3251
  id: input.id ?? createAnnotationId(),
3085
3252
  createdAt: input.createdAt ?? Date.now()
3086
3253
  };
3087
- setAnnotations((current) => {
3088
- const next = sortAnnotations([...current, annotation]);
3089
- commit(next);
3090
- return next;
3091
- });
3254
+ const next = sortAnnotations([...annotationsRef.current, annotation]);
3255
+ applyNext(next);
3256
+ commit(next);
3092
3257
  return annotation;
3093
3258
  },
3094
- [commit]
3259
+ [commit, applyNext]
3095
3260
  );
3096
3261
  const remove = useCallback10(
3097
3262
  (id) => {
3098
- setAnnotations((current) => {
3099
- const next = current.filter((annotation) => annotation.id !== id);
3100
- if (next.length !== current.length) commit(next);
3101
- return next;
3102
- });
3263
+ const current = annotationsRef.current;
3264
+ const next = current.filter((annotation) => annotation.id !== id);
3265
+ if (next.length === current.length) return;
3266
+ applyNext(next);
3267
+ commit(next);
3103
3268
  },
3104
- [commit]
3269
+ [commit, applyNext]
3105
3270
  );
3106
3271
  const update = useCallback10(
3107
3272
  (id, patch) => {
3108
- setAnnotations((current) => {
3109
- let changed = false;
3110
- const next = current.map((annotation) => {
3111
- if (annotation.id !== id) return annotation;
3112
- changed = true;
3113
- return { ...annotation, ...patch, id };
3114
- });
3115
- if (!changed) return current;
3116
- const sorted = sortAnnotations(next);
3117
- commit(sorted);
3118
- return sorted;
3273
+ let changed = false;
3274
+ const next = annotationsRef.current.map((annotation) => {
3275
+ if (annotation.id !== id) return annotation;
3276
+ changed = true;
3277
+ return { ...annotation, ...patch, id };
3119
3278
  });
3279
+ if (!changed) return;
3280
+ const sorted = sortAnnotations(next);
3281
+ applyNext(sorted);
3282
+ commit(sorted);
3120
3283
  },
3121
- [commit]
3284
+ [commit, applyNext]
3122
3285
  );
3123
3286
  const clear = useCallback10(() => {
3124
- setAnnotations([]);
3125
- if (storage) {
3126
- if (timerRef.current) clearTimeout(timerRef.current);
3287
+ applyNext([]);
3288
+ const currentStorage = storageRef.current;
3289
+ if (currentStorage) {
3290
+ cancelPending();
3127
3291
  try {
3128
- storage.removeItem(key);
3292
+ currentStorage.removeItem(key);
3129
3293
  } catch {
3130
3294
  }
3131
3295
  }
3132
3296
  onChangeRef.current?.([]);
3133
- }, [storage, key]);
3297
+ }, [key, cancelPending, applyNext]);
3134
3298
  return { annotations, add, remove, update, clear };
3135
3299
  }
3136
3300
 
3137
3301
  // src/useEditableEpub.ts
3138
3302
  import {
3303
+ clampEditableEpubSelection,
3304
+ cloneEditableEpubBook as cloneEditableEpubBook2,
3139
3305
  EditableEpub as EditableEpub2
3140
3306
  } from "@libraz/mejiro/epub";
3141
- import { useCallback as useCallback11, useEffect as useEffect11, useMemo as useMemo8, useRef as useRef15, useState as useState14 } from "react";
3307
+ import { useCallback as useCallback11, useEffect as useEffect12, useMemo as useMemo8, useRef as useRef15, useState as useState14 } from "react";
3142
3308
  function useEditableEpub(options = {}) {
3143
3309
  const [editor, setEditor] = useState14(null);
3144
3310
  const [loading, setLoading] = useState14(false);
@@ -3153,15 +3319,17 @@ function useEditableEpub(options = {}) {
3153
3319
  const onLoadRef = useRef15(options.onLoad);
3154
3320
  const onErrorRef = useRef15(options.onError);
3155
3321
  const onExportRef = useRef15(options.onExport);
3322
+ const limitsRef = useRef15(options.limits);
3156
3323
  onLoadRef.current = options.onLoad;
3157
3324
  onErrorRef.current = options.onError;
3158
3325
  onExportRef.current = options.onExport;
3326
+ limitsRef.current = options.limits;
3159
3327
  const loadBufferWithRequest = useCallback11(
3160
3328
  async (buffer, requestId) => {
3161
3329
  setLoading(true);
3162
3330
  setError(null);
3163
3331
  try {
3164
- const next = await EditableEpub2.load(buffer);
3332
+ const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
3165
3333
  if (requestId !== requestIdRef.current) return null;
3166
3334
  setEditor(next);
3167
3335
  setSelectionState({ chapter: 0, paragraph: 0 });
@@ -3228,19 +3396,19 @@ function useEditableEpub(options = {}) {
3228
3396
  },
3229
3397
  [loadBufferWithRequest]
3230
3398
  );
3231
- useEffect11(() => {
3399
+ useEffect12(() => {
3232
3400
  if (options.defaultUrl) void loadUrl(options.defaultUrl);
3233
3401
  }, [options.defaultUrl, loadUrl]);
3234
3402
  const book = editor?.book ?? null;
3235
3403
  const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
3236
3404
  const previewBook = useMemo8(() => {
3237
3405
  void revision;
3238
- return book ? cloneBook2(book) : null;
3406
+ return book ? cloneEditableEpubBook2(book) : null;
3239
3407
  }, [book, revision]);
3240
3408
  const history = editor?.history ?? null;
3241
3409
  const setSelection = useCallback11(
3242
3410
  (nextSelection) => {
3243
- setSelectionState(clampSelection(book, nextSelection));
3411
+ setSelectionState(clampEditableEpubSelection(book, nextSelection));
3244
3412
  },
3245
3413
  [book]
3246
3414
  );
@@ -3320,46 +3488,13 @@ function useEditableEpub(options = {}) {
3320
3488
  exportEpub
3321
3489
  };
3322
3490
  }
3323
- function cloneBook2(book) {
3324
- return {
3325
- ...book,
3326
- chapters: book.chapters.map((chapter) => ({
3327
- ...chapter,
3328
- blocks: chapter.blocks.map(
3329
- (block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
3330
- ),
3331
- imageAssets: new Map(chapter.imageAssets),
3332
- originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
3333
- paragraphs: chapter.paragraphs.map((paragraph) => ({
3334
- ...paragraph,
3335
- inlineAnnotations: [...paragraph.inlineAnnotations]
3336
- })),
3337
- paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
3338
- })),
3339
- packageData: {
3340
- ...book.packageData,
3341
- files: new Map(book.packageData.files)
3342
- }
3343
- };
3344
- }
3345
- function clampSelection(book, selection) {
3346
- if (!book?.chapters.length) return { chapter: 0, paragraph: 0 };
3347
- const chapter = clampInteger(selection.chapter, 0, book.chapters.length - 1);
3348
- const paragraphCount = book.chapters[chapter]?.paragraphs.length ?? 0;
3349
- const paragraph = paragraphCount ? clampInteger(selection.paragraph, 0, paragraphCount - 1) : 0;
3350
- return { chapter, paragraph };
3351
- }
3352
- function clampInteger(value, min, max) {
3353
- if (!Number.isFinite(value)) return min;
3354
- return Math.min(max, Math.max(min, Math.trunc(value)));
3355
- }
3356
3491
 
3357
3492
  // src/useEpubProject.ts
3358
3493
  import {
3359
3494
  EpubProject as EpubProject2,
3360
3495
  parseEpub as parseEpub2
3361
3496
  } from "@libraz/mejiro/epub";
3362
- import { useCallback as useCallback12, useEffect as useEffect12, useMemo as useMemo9, useRef as useRef16, useState as useState15 } from "react";
3497
+ import { useCallback as useCallback12, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef16, useState as useState15 } from "react";
3363
3498
  function useEpubProject(options = {}) {
3364
3499
  const defaultTitle = options.defaultChapterTitle;
3365
3500
  const defaultBody = options.defaultChapterBody;
@@ -3372,6 +3507,8 @@ function useEpubProject(options = {}) {
3372
3507
  options.chapters?.length ? options.chapters : [defaultChapter3(0, defaultTitle, defaultBody)]
3373
3508
  );
3374
3509
  const [selectedChapter, setSelectedChapterState] = useState15(0);
3510
+ const [cover, setCoverState] = useState15(options.cover ?? null);
3511
+ const [assets, setAssetsState] = useState15(options.assets ?? []);
3375
3512
  const [previewBook, setPreviewBook] = useState15(null);
3376
3513
  const [previewError, setPreviewError] = useState15(null);
3377
3514
  const [previewing, setPreviewing] = useState15(false);
@@ -3380,8 +3517,8 @@ function useEpubProject(options = {}) {
3380
3517
  const onExportRef = useRef16(options.onExport);
3381
3518
  onPreviewRef.current = options.onPreview;
3382
3519
  onExportRef.current = options.onExport;
3383
- const buildProject = useCallback12(
3384
- () => EpubProject2.fromManuscript({
3520
+ const buildProject = useCallback12(() => {
3521
+ const project = EpubProject2.fromManuscript({
3385
3522
  metadata,
3386
3523
  includeTitlePage: false,
3387
3524
  includeTitleInFirstChapter: true,
@@ -3389,13 +3526,15 @@ function useEpubProject(options = {}) {
3389
3526
  id: chapter.id,
3390
3527
  title: chapter.title || "Untitled",
3391
3528
  body: chapter.body
3392
- }))
3393
- }),
3394
- [chapters, metadata]
3395
- );
3529
+ })),
3530
+ ...cover ? { cover } : {}
3531
+ });
3532
+ for (const asset of assets) project.addAsset(asset);
3533
+ return project;
3534
+ }, [assets, chapters, cover, metadata]);
3396
3535
  const assetResolverRef = useRef16(options.assetResolver);
3397
3536
  assetResolverRef.current = options.assetResolver;
3398
- useEffect12(() => {
3537
+ useEffect13(() => {
3399
3538
  const requestId = ++previewRequestIdRef.current;
3400
3539
  let cancelled = false;
3401
3540
  setPreviewing(true);
@@ -3441,6 +3580,12 @@ function useEpubProject(options = {}) {
3441
3580
  },
3442
3581
  [chapters, defaultBody, defaultTitle]
3443
3582
  );
3583
+ const setCover = useCallback12((asset) => {
3584
+ setCoverState(asset);
3585
+ }, []);
3586
+ const setAssets = useCallback12((next) => {
3587
+ setAssetsState(next);
3588
+ }, []);
3444
3589
  const setSelectedChapter = useCallback12(
3445
3590
  (index) => {
3446
3591
  setSelectedChapterState(Math.max(0, Math.min(index, chapters.length - 1)));
@@ -3515,12 +3660,16 @@ function useEpubProject(options = {}) {
3515
3660
  chapters,
3516
3661
  selectedChapter,
3517
3662
  currentChapter,
3663
+ cover,
3664
+ assets,
3518
3665
  previewBook,
3519
3666
  previewError,
3520
3667
  previewing,
3521
3668
  setMetadata,
3522
3669
  setChapters,
3523
3670
  setSelectedChapter,
3671
+ setCover,
3672
+ setAssets,
3524
3673
  patchChapter,
3525
3674
  addChapter,
3526
3675
  removeChapter,
@@ -3530,8 +3679,10 @@ function useEpubProject(options = {}) {
3530
3679
  }),
3531
3680
  [
3532
3681
  addChapter,
3682
+ assets,
3533
3683
  buildProject,
3534
3684
  chapters,
3685
+ cover,
3535
3686
  currentChapter,
3536
3687
  exportEpub,
3537
3688
  metadata,
@@ -3542,7 +3693,9 @@ function useEpubProject(options = {}) {
3542
3693
  reorderChapters,
3543
3694
  removeChapter,
3544
3695
  selectedChapter,
3696
+ setAssets,
3545
3697
  setChapters,
3698
+ setCover,
3546
3699
  setMetadata,
3547
3700
  setSelectedChapter
3548
3701
  ]
@@ -3557,10 +3710,10 @@ function defaultChapter3(index, titleFor = (i) => i === 0 ? "\u7B2C\u4E00\u8A71"
3557
3710
  }
3558
3711
 
3559
3712
  // src/useImageOverlay.ts
3560
- import { moveImageOverlayRect as moveImageOverlayRect2, resizeImageOverlayRect as resizeImageOverlayRect2 } from "@libraz/mejiro";
3713
+ import { createOverlayDragSession as createOverlayDragSession2 } from "@libraz/mejiro/browser";
3561
3714
  import {
3562
3715
  useCallback as useCallback13,
3563
- useEffect as useEffect13,
3716
+ useEffect as useEffect14,
3564
3717
  useRef as useRef17,
3565
3718
  useState as useState16
3566
3719
  } from "react";
@@ -3580,7 +3733,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3580
3733
  const onUpdateRef = useRef17(onUpdate);
3581
3734
  onUpdateRef.current = onUpdate;
3582
3735
  const activeDragCleanupsRef = useRef17(/* @__PURE__ */ new Set());
3583
- useEffect13(
3736
+ useEffect14(
3584
3737
  () => () => {
3585
3738
  for (const cleanup of activeDragCleanupsRef.current) cleanup();
3586
3739
  activeDragCleanupsRef.current.clear();
@@ -3597,6 +3750,20 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3597
3750
  },
3598
3751
  [margin]
3599
3752
  );
3753
+ const syncedRef = useRef17({
3754
+ layout,
3755
+ spreadIdx
3756
+ });
3757
+ useEffect14(() => {
3758
+ const prev = syncedRef.current;
3759
+ syncedRef.current = { layout, spreadIdx };
3760
+ if (prev.layout === layout && prev.spreadIdx === spreadIdx) return;
3761
+ if (rectRef.current === null) return;
3762
+ if (prev.layout && prev.spreadIdx !== spreadIdx) {
3763
+ prev.layout.syncImages(prev.spreadIdx, void 0);
3764
+ }
3765
+ syncToLayout(rectRef.current);
3766
+ }, [layout, spreadIdx, syncToLayout]);
3600
3767
  const toggleImage = useCallback13(() => {
3601
3768
  if (rectRef.current) {
3602
3769
  setImageRect(null);
@@ -3607,42 +3774,33 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3607
3774
  syncToLayout(r);
3608
3775
  }
3609
3776
  }, [defX, defY, defW, defH, syncToLayout]);
3777
+ const applyRect = useCallback13(
3778
+ (rect) => {
3779
+ setImageRect(rect);
3780
+ syncToLayout(rect);
3781
+ },
3782
+ [syncToLayout]
3783
+ );
3610
3784
  const onOverlayPointerDown = useCallback13(
3611
3785
  (e) => {
3612
3786
  e.preventDefault();
3613
3787
  const current = rectRef.current;
3614
3788
  if (!current) return;
3615
- const startX = e.clientX;
3616
- const startY = e.clientY;
3617
- const start = { ...current };
3618
3789
  const target = e.currentTarget;
3619
- target.setPointerCapture(e.nativeEvent.pointerId);
3620
- target.classList.add("dragging");
3621
- let rafId = 0;
3622
- const onMove = (me) => {
3623
- const dx = me.clientX - startX;
3624
- const dy = me.clientY - startY;
3625
- const r = moveImageOverlayRect2(start, dx, dy);
3626
- cancelAnimationFrame(rafId);
3627
- rafId = requestAnimationFrame(() => {
3628
- setImageRect(r);
3629
- syncToLayout(r);
3630
- });
3631
- };
3632
- let cleanup;
3633
- const onUp = () => cleanup();
3634
- cleanup = () => {
3635
- cancelAnimationFrame(rafId);
3636
- target.classList.remove("dragging");
3637
- document.removeEventListener("pointermove", onMove);
3638
- document.removeEventListener("pointerup", onUp);
3639
- activeDragCleanupsRef.current.delete(cleanup);
3640
- };
3641
- document.addEventListener("pointermove", onMove);
3642
- document.addEventListener("pointerup", onUp);
3643
- activeDragCleanupsRef.current.add(cleanup);
3790
+ createOverlayDragSession2({
3791
+ mode: "move",
3792
+ rect: current,
3793
+ startX: e.clientX,
3794
+ startY: e.clientY,
3795
+ pointerId: e.nativeEvent.pointerId,
3796
+ captureElement: target,
3797
+ activeElement: target,
3798
+ dragClass: "dragging",
3799
+ registry: activeDragCleanupsRef.current,
3800
+ onChange: applyRect
3801
+ });
3644
3802
  },
3645
- [syncToLayout]
3803
+ [applyRect]
3646
3804
  );
3647
3805
  const onResizePointerDown = useCallback13(
3648
3806
  (e) => {
@@ -3650,37 +3808,21 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3650
3808
  e.stopPropagation();
3651
3809
  const current = rectRef.current;
3652
3810
  if (!current) return;
3653
- const startX = e.clientX;
3654
- const startY = e.clientY;
3655
- const start = { ...current };
3656
3811
  const target = e.currentTarget;
3657
- target.setPointerCapture(e.nativeEvent.pointerId);
3658
- target.parentElement?.classList.add("dragging");
3659
- let rafId = 0;
3660
- const onMove = (me) => {
3661
- const dx = me.clientX - startX;
3662
- const dy = me.clientY - startY;
3663
- const r = resizeImageOverlayRect2(start, dx, dy);
3664
- cancelAnimationFrame(rafId);
3665
- rafId = requestAnimationFrame(() => {
3666
- setImageRect(r);
3667
- syncToLayout(r);
3668
- });
3669
- };
3670
- let cleanup;
3671
- const onUp = () => cleanup();
3672
- cleanup = () => {
3673
- cancelAnimationFrame(rafId);
3674
- target.parentElement?.classList.remove("dragging");
3675
- document.removeEventListener("pointermove", onMove);
3676
- document.removeEventListener("pointerup", onUp);
3677
- activeDragCleanupsRef.current.delete(cleanup);
3678
- };
3679
- document.addEventListener("pointermove", onMove);
3680
- document.addEventListener("pointerup", onUp);
3681
- activeDragCleanupsRef.current.add(cleanup);
3812
+ createOverlayDragSession2({
3813
+ mode: "resize",
3814
+ rect: current,
3815
+ startX: e.clientX,
3816
+ startY: e.clientY,
3817
+ pointerId: e.nativeEvent.pointerId,
3818
+ captureElement: target,
3819
+ activeElement: target.parentElement,
3820
+ dragClass: "dragging",
3821
+ registry: activeDragCleanupsRef.current,
3822
+ onChange: applyRect
3823
+ });
3682
3824
  },
3683
- [syncToLayout]
3825
+ [applyRect]
3684
3826
  );
3685
3827
  return {
3686
3828
  imageRect,
@@ -3692,7 +3834,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3692
3834
  }
3693
3835
 
3694
3836
  // src/useLibrary.ts
3695
- import { useCallback as useCallback14, useEffect as useEffect14, useMemo as useMemo10, useState as useState17 } from "react";
3837
+ import { useCallback as useCallback14, useEffect as useEffect15, useMemo as useMemo10, useState as useState17 } from "react";
3696
3838
  function useLibrary(options) {
3697
3839
  const { volumes, initialVolumeId, onChange } = options;
3698
3840
  const initialIndex = initialVolumeId != null ? Math.max(
@@ -3702,7 +3844,7 @@ function useLibrary(options) {
3702
3844
  const [currentId, setCurrentId] = useState17(
3703
3845
  volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
3704
3846
  );
3705
- useEffect14(() => {
3847
+ useEffect15(() => {
3706
3848
  if (volumes.length === 0) {
3707
3849
  if (currentId !== null) setCurrentId(null);
3708
3850
  return;
@@ -3760,7 +3902,13 @@ function useLibrary(options) {
3760
3902
  }
3761
3903
 
3762
3904
  // src/useManuscriptLayout.ts
3763
- import { useCallback as useCallback15, useEffect as useEffect15, useLayoutEffect as useLayoutEffect5, useRef as useRef18, useState as useState18 } from "react";
3905
+ import {
3906
+ useCallback as useCallback15,
3907
+ useEffect as useEffect16,
3908
+ useLayoutEffect as useLayoutEffect5,
3909
+ useRef as useRef18,
3910
+ useState as useState18
3911
+ } from "react";
3764
3912
  function useManuscriptLayout(book, chapter, surface, options = {}) {
3765
3913
  const dialect = options.dialect ?? "mejiro";
3766
3914
  const enableResize = options.enableResize ?? true;
@@ -3771,43 +3919,56 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
3771
3919
  const [contentHeight, setContentHeight] = useState18(0);
3772
3920
  const [elapsedMs, setElapsedMs] = useState18(0);
3773
3921
  const requestIdRef = useRef18(0);
3774
- const recompute = useCallback15(async () => {
3775
- const requestId = ++requestIdRef.current;
3776
- if (!(chapter && surface.current)) {
3777
- setLayout(null);
3778
- setPageWidth(0);
3779
- setPageHeight(0);
3780
- setContentHeight(0);
3781
- setElapsedMs(0);
3782
- return;
3783
- }
3784
- setLayout(null);
3785
- const dims = book.computePageSize(surface.current);
3786
- setPageWidth(dims.pageWidth);
3787
- setPageHeight(dims.pageHeight);
3788
- setContentHeight(dims.contentHeight);
3789
- const t0 = performance.now();
3790
- const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
3791
- if (requestId !== requestIdRef.current) return;
3792
- const next = layouts.values().next().value ?? null;
3793
- setLayout(next);
3794
- setElapsedMs(performance.now() - t0);
3795
- }, [book, chapter, dialect, surface]);
3922
+ const layoutRef = useRef18(null);
3923
+ layoutRef.current = layout;
3924
+ const pendingRestore = useRef18(null);
3925
+ const optionsRef = useRef18(options);
3926
+ optionsRef.current = options;
3927
+ const recompute = useCallback15(
3928
+ async (opts = {}) => {
3929
+ const blank = opts.blank ?? true;
3930
+ const requestId = ++requestIdRef.current;
3931
+ if (!(chapter && surface.current)) {
3932
+ setLayout(null);
3933
+ setPageWidth(0);
3934
+ setPageHeight(0);
3935
+ setContentHeight(0);
3936
+ setElapsedMs(0);
3937
+ return;
3938
+ }
3939
+ const captured = !blank && layoutRef.current ? optionsRef.current.capturePosition?.(layoutRef.current) ?? null : null;
3940
+ pendingRestore.current = captured;
3941
+ if (blank) setLayout(null);
3942
+ const dims = book.computePageSize(surface.current);
3943
+ setPageWidth(dims.pageWidth);
3944
+ setPageHeight(dims.pageHeight);
3945
+ setContentHeight(dims.contentHeight);
3946
+ const t0 = performance.now();
3947
+ const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
3948
+ if (requestId !== requestIdRef.current) return;
3949
+ const next = layouts.values().next().value ?? null;
3950
+ layoutRef.current = next;
3951
+ setLayout(next);
3952
+ setElapsedMs(performance.now() - t0);
3953
+ },
3954
+ [book, chapter, dialect, surface]
3955
+ );
3796
3956
  useLayoutEffect5(() => {
3797
3957
  requestIdRef.current++;
3958
+ pendingRestore.current = null;
3798
3959
  setLayout(null);
3799
3960
  setPageWidth(0);
3800
3961
  setPageHeight(0);
3801
3962
  setContentHeight(0);
3802
3963
  setElapsedMs(0);
3803
3964
  }, [recompute]);
3804
- useEffect15(() => {
3965
+ useEffect16(() => {
3805
3966
  void recompute();
3806
3967
  return () => {
3807
3968
  requestIdRef.current++;
3808
3969
  };
3809
3970
  }, [recompute]);
3810
- useEffect15(() => {
3971
+ useEffect16(() => {
3811
3972
  if (!enableResize) return;
3812
3973
  const el = surface.current;
3813
3974
  if (!el) return;
@@ -3822,12 +3983,12 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
3822
3983
  const scheduleReflow = (immediate) => {
3823
3984
  clear();
3824
3985
  if (immediate) {
3825
- void recompute();
3986
+ void recompute({ blank: false });
3826
3987
  return;
3827
3988
  }
3828
3989
  timer = setTimeout(() => {
3829
3990
  timer = null;
3830
- void recompute();
3991
+ void recompute({ blank: false });
3831
3992
  }, resizeDebounce);
3832
3993
  };
3833
3994
  if (typeof ResizeObserver !== "undefined") {
@@ -3849,7 +4010,7 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
3849
4010
  clear();
3850
4011
  };
3851
4012
  }, [surface, enableResize, resizeDebounce, recompute]);
3852
- return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
4013
+ return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
3853
4014
  }
3854
4015
 
3855
4016
  // src/useReadingPosition.ts
@@ -3857,7 +4018,7 @@ import {
3857
4018
  parseReadingPosition,
3858
4019
  serializeReadingPosition
3859
4020
  } from "@libraz/mejiro";
3860
- import { useCallback as useCallback16, useEffect as useEffect16, useRef as useRef19, useState as useState19 } from "react";
4021
+ import { useCallback as useCallback16, useEffect as useEffect17, useRef as useRef19, useState as useState19 } from "react";
3861
4022
  function resolveDefaultStorage2() {
3862
4023
  if (typeof globalThis === "undefined") return null;
3863
4024
  const w = globalThis.localStorage;
@@ -3876,25 +4037,41 @@ function useReadingPosition(options) {
3876
4037
  });
3877
4038
  const storageRef = useRef19(storage);
3878
4039
  storageRef.current = storage;
3879
- useEffect16(() => {
3880
- const currentStorage = storageRef.current;
3881
- if (!currentStorage) {
3882
- setPosition(null);
3883
- return;
4040
+ const timerRef = useRef19(null);
4041
+ const pendingWriteRef = useRef19(null);
4042
+ const flushPending = useCallback16(() => {
4043
+ if (timerRef.current) {
4044
+ clearTimeout(timerRef.current);
4045
+ timerRef.current = null;
3884
4046
  }
4047
+ const write = pendingWriteRef.current;
4048
+ pendingWriteRef.current = null;
4049
+ if (!write) return;
3885
4050
  try {
3886
- setPosition(parseReadingPosition(currentStorage.getItem(key)));
4051
+ write();
3887
4052
  } catch {
4053
+ }
4054
+ }, []);
4055
+ const cancelPending = useCallback16(() => {
4056
+ if (timerRef.current) {
4057
+ clearTimeout(timerRef.current);
4058
+ timerRef.current = null;
4059
+ }
4060
+ pendingWriteRef.current = null;
4061
+ }, []);
4062
+ useEffect17(() => {
4063
+ const currentStorage = storageRef.current;
4064
+ if (currentStorage) {
4065
+ try {
4066
+ setPosition(parseReadingPosition(currentStorage.getItem(key)));
4067
+ } catch {
4068
+ setPosition(null);
4069
+ }
4070
+ } else {
3888
4071
  setPosition(null);
3889
4072
  }
3890
- }, [key]);
3891
- const timerRef = useRef19(null);
3892
- useEffect16(
3893
- () => () => {
3894
- if (timerRef.current) clearTimeout(timerRef.current);
3895
- },
3896
- []
3897
- );
4073
+ return flushPending;
4074
+ }, [key, flushPending]);
3898
4075
  const onChangeRef = useRef19(options.onChange);
3899
4076
  onChangeRef.current = options.onChange;
3900
4077
  const save = useCallback16(
@@ -3902,21 +4079,19 @@ function useReadingPosition(options) {
3902
4079
  setPosition(next);
3903
4080
  const currentStorage = storageRef.current;
3904
4081
  if (currentStorage) {
3905
- if (timerRef.current) clearTimeout(timerRef.current);
3906
- timerRef.current = setTimeout(() => {
3907
- try {
3908
- currentStorage.setItem(key, serializeReadingPosition(next));
3909
- } catch {
3910
- }
3911
- }, throttleMs);
4082
+ cancelPending();
4083
+ pendingWriteRef.current = () => {
4084
+ currentStorage.setItem(key, serializeReadingPosition(next));
4085
+ };
4086
+ timerRef.current = setTimeout(flushPending, throttleMs);
3912
4087
  }
3913
4088
  onChangeRef.current?.(next);
3914
4089
  },
3915
- [key, throttleMs]
4090
+ [key, throttleMs, cancelPending, flushPending]
3916
4091
  );
3917
4092
  const clear = useCallback16(() => {
3918
4093
  setPosition(null);
3919
- if (timerRef.current) clearTimeout(timerRef.current);
4094
+ cancelPending();
3920
4095
  const currentStorage = storageRef.current;
3921
4096
  if (currentStorage) {
3922
4097
  try {
@@ -3925,7 +4100,7 @@ function useReadingPosition(options) {
3925
4100
  }
3926
4101
  }
3927
4102
  onChangeRef.current?.(null);
3928
- }, [key]);
4103
+ }, [key, cancelPending]);
3929
4104
  return { position, save, clear };
3930
4105
  }
3931
4106