@libraz/mejiro-react 0.9.1 → 0.9.2

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,7 @@ 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 useCallback7, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef12, useState as useState9 } from "react";
177
180
 
178
181
  // src/errors.ts
179
182
  import { assertEpubInputSize } from "@libraz/mejiro/epub";
@@ -208,8 +211,8 @@ import {
208
211
  useEffect as useEffect8,
209
212
  useImperativeHandle,
210
213
  useLayoutEffect as useLayoutEffect4,
211
- useMemo as useMemo4,
212
- useRef as useRef10,
214
+ useMemo as useMemo5,
215
+ useRef as useRef11,
213
216
  useState as useState8
214
217
  } from "react";
215
218
 
@@ -324,10 +327,7 @@ function paragraphScaleStyle(scale) {
324
327
  function MejiroPage({ page, className, style }) {
325
328
  const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
326
329
  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
- );
330
+ const paraClass = paragraphClassName(paragraph.kind, paragraph.headingLevel);
331
331
  return (
332
332
  // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
333
333
  /* @__PURE__ */ jsx6("div", { className: paraClass, style: paragraphScaleStyle(paragraph.scale), children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
@@ -468,6 +468,8 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
468
468
  spreadIdxRef.current = spreadIdx;
469
469
  const onUpdateRef = useRef2(options.onUpdate);
470
470
  onUpdateRef.current = options.onUpdate;
471
+ const marginRef = useRef2(margin);
472
+ marginRef.current = margin;
471
473
  const activeDragCleanupsRef = useRef2(/* @__PURE__ */ new Set());
472
474
  useEffect(
473
475
  () => () => {
@@ -476,15 +478,12 @@ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
476
478
  },
477
479
  []
478
480
  );
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
- );
481
+ const syncSpreads = useCallback((spreads) => {
482
+ const lo = layoutRef.current;
483
+ if (!lo) return;
484
+ applyImages(lo, imagesRef.current, spreads, marginRef.current);
485
+ for (const si of spreads) onUpdateRef.current?.(lo.getSpread(si));
486
+ }, []);
488
487
  const appliedRef = useRef2({
489
488
  layout: null,
490
489
  margin
@@ -1061,11 +1060,13 @@ function MejiroSpread({
1061
1060
  const selectionRef = useRef5(null);
1062
1061
  const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
1063
1062
  const gestureRef = useRef5(null);
1063
+ const swipedRef = useRef5(false);
1064
1064
  const gestureEnabled = onSwipe != null || onSurfaceTap != null;
1065
1065
  const SWIPE_THRESHOLD = 40;
1066
1066
  const TAP_MOVE_THRESHOLD = 8;
1067
1067
  function handleGesturePointerDown(e) {
1068
1068
  if (!gestureEnabled) return;
1069
+ swipedRef.current = false;
1069
1070
  if (!isPrimaryPointerPress2(e)) return;
1070
1071
  if (e.target?.closest(OVERLAY_SELECTOR)) return;
1071
1072
  gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
@@ -1080,6 +1081,7 @@ function MejiroSpread({
1080
1081
  const ax = Math.abs(dx);
1081
1082
  const ay = Math.abs(dy);
1082
1083
  if (ax >= SWIPE_THRESHOLD && ax >= ay * 1.4) {
1084
+ swipedRef.current = true;
1083
1085
  onSwipe?.(dx < 0 ? "next" : "prev");
1084
1086
  return;
1085
1087
  }
@@ -1089,6 +1091,12 @@ function MejiroSpread({
1089
1091
  onSurfaceTap?.();
1090
1092
  }
1091
1093
  }
1094
+ function suppressSwipeClick(e) {
1095
+ if (!swipedRef.current) return;
1096
+ swipedRef.current = false;
1097
+ e.preventDefault();
1098
+ e.stopPropagation();
1099
+ }
1092
1100
  function resolvePointer(e) {
1093
1101
  if (!anchorAtCoord) return null;
1094
1102
  const content = contentAtPoint(e.currentTarget, e.clientX, e.clientY);
@@ -1207,6 +1215,7 @@ function MejiroSpread({
1207
1215
  onPointerUp: useCombined ? combinedPointerUp : void 0,
1208
1216
  onPointerCancel: useCombined ? combinedPointerCancel : void 0,
1209
1217
  onLostPointerCapture: selectionEnabled ? endSelection : void 0,
1218
+ onClickCapture: gestureEnabled ? suppressSwipeClick : void 0,
1210
1219
  children: [
1211
1220
  renderPage(singlePage ? singleSide : "right"),
1212
1221
  !singlePage && renderPage("left"),
@@ -1366,6 +1375,9 @@ function createDraftChangeTracker(chapters, key) {
1366
1375
  }
1367
1376
 
1368
1377
  // src/useChapterLayout.ts
1378
+ import { useMemo as useMemo4, useRef as useRef7 } from "react";
1379
+
1380
+ // src/useSurfaceLayout.ts
1369
1381
  import {
1370
1382
  useCallback as useCallback2,
1371
1383
  useEffect as useEffect4,
@@ -1373,18 +1385,18 @@ import {
1373
1385
  useRef as useRef6,
1374
1386
  useState as useState4
1375
1387
  } from "react";
1376
- function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1377
- const enableResize = options.enableResize ?? true;
1378
- const resizeDebounce = options.resizeDebounce ?? 120;
1388
+ function useSurfaceLayout(source, surface, options) {
1389
+ const { enableResize, resizeDebounce } = options;
1379
1390
  const [layout, setLayout] = useState4(null);
1380
1391
  const [pageWidth, setPageWidth] = useState4(0);
1381
1392
  const [pageHeight, setPageHeight] = useState4(0);
1382
1393
  const [contentHeight, setContentHeight] = useState4(0);
1383
1394
  const [elapsedMs, setElapsedMs] = useState4(0);
1384
1395
  const layoutRef = useRef6(null);
1396
+ layoutRef.current = layout;
1385
1397
  const requestIdRef = useRef6(0);
1386
1398
  const pendingRestore = useRef6(null);
1387
- layoutRef.current = layout;
1399
+ const measuredBoxRef = useRef6(null);
1388
1400
  const optionsRef = useRef6(options);
1389
1401
  optionsRef.current = options;
1390
1402
  const recompute = useCallback2(
@@ -1392,8 +1404,9 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1392
1404
  const blank = opts.blank ?? true;
1393
1405
  const o = optionsRef.current;
1394
1406
  const requestId = ++requestIdRef.current;
1395
- const chapter = epub?.chapters[chapterIndex];
1396
- if (!(chapter && surface.current)) {
1407
+ const el = surface.current;
1408
+ if (!(source && el)) {
1409
+ measuredBoxRef.current = null;
1397
1410
  setLayout(null);
1398
1411
  setPageWidth(0);
1399
1412
  setPageHeight(0);
@@ -1401,22 +1414,36 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1401
1414
  setElapsedMs(0);
1402
1415
  return;
1403
1416
  }
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);
1417
+ try {
1418
+ const captured = !blank && layoutRef.current ? o.capturePosition?.(layoutRef.current) ?? null : null;
1419
+ pendingRestore.current = captured;
1420
+ if (blank) setLayout(null);
1421
+ measuredBoxRef.current = { width: el.clientWidth, height: el.clientHeight };
1422
+ const dims = source.measure(el);
1423
+ setPageWidth(dims.pageWidth);
1424
+ setPageHeight(dims.pageHeight);
1425
+ setContentHeight(dims.contentHeight);
1426
+ const t0 = performance.now();
1427
+ const next = await source.layout();
1428
+ if (requestId !== requestIdRef.current) return;
1429
+ layoutRef.current = next;
1430
+ setLayout(next);
1431
+ setElapsedMs(performance.now() - t0);
1432
+ } catch (err) {
1433
+ if (requestId !== requestIdRef.current) return;
1434
+ const handler = optionsRef.current.onError;
1435
+ if (!handler) throw err;
1436
+ handler(toError(err));
1437
+ }
1417
1438
  },
1418
- [book, epub, chapterIndex, surface]
1439
+ [source, surface]
1419
1440
  );
1441
+ const recomputeRef = useRef6(recompute);
1442
+ recomputeRef.current = recompute;
1443
+ const trigger = useCallback2((opts) => {
1444
+ recomputeRef.current(opts).catch(() => {
1445
+ });
1446
+ }, []);
1420
1447
  useLayoutEffect2(() => {
1421
1448
  requestIdRef.current++;
1422
1449
  pendingRestore.current = null;
@@ -1427,7 +1454,8 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1427
1454
  setElapsedMs(0);
1428
1455
  }, [recompute]);
1429
1456
  useEffect4(() => {
1430
- void recompute();
1457
+ recompute().catch(() => {
1458
+ });
1431
1459
  return () => {
1432
1460
  requestIdRef.current++;
1433
1461
  };
@@ -1454,18 +1482,23 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1454
1482
  const scheduleReflow = (immediate) => {
1455
1483
  clear();
1456
1484
  if (immediate) {
1457
- void recompute({ blank: false });
1485
+ trigger({ blank: false });
1458
1486
  return;
1459
1487
  }
1460
1488
  timer = setTimeout(() => {
1461
1489
  timer = null;
1462
- void recompute({ blank: false });
1490
+ trigger({ blank: false });
1463
1491
  }, resizeDebounce);
1464
1492
  };
1465
1493
  if (typeof ResizeObserver !== "undefined") {
1466
1494
  const observer = new ResizeObserver(() => {
1467
1495
  const immediate = !observed;
1468
1496
  observed = true;
1497
+ const box = measuredBoxRef.current;
1498
+ if (box && box.width === el.clientWidth && box.height === el.clientHeight) {
1499
+ clear();
1500
+ return;
1501
+ }
1469
1502
  scheduleReflow(immediate);
1470
1503
  });
1471
1504
  observer.observe(el);
@@ -1480,35 +1513,46 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1480
1513
  window.removeEventListener("resize", onWindowResize);
1481
1514
  clear();
1482
1515
  };
1483
- }, [surface, enableResize, resizeDebounce, recompute]);
1484
- return {
1485
- layout,
1486
- pageWidth,
1487
- pageHeight,
1488
- contentHeight,
1489
- elapsedMs,
1490
- recompute,
1491
- pendingRestore
1492
- };
1516
+ }, [surface, enableResize, resizeDebounce, trigger]);
1517
+ return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
1518
+ }
1519
+
1520
+ // src/useChapterLayout.ts
1521
+ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1522
+ const optionsRef = useRef7(options);
1523
+ optionsRef.current = options;
1524
+ const source = useMemo4(() => {
1525
+ const chapter = epub?.chapters[chapterIndex];
1526
+ if (!chapter) return null;
1527
+ return {
1528
+ measure: (el) => book.computePageSize(el, optionsRef.current.pageGeometry?.()),
1529
+ layout: () => book.layoutChapter(chapter)
1530
+ };
1531
+ }, [book, epub, chapterIndex]);
1532
+ return useSurfaceLayout(source, surface, {
1533
+ ...options,
1534
+ enableResize: options.enableResize ?? true,
1535
+ resizeDebounce: options.resizeDebounce ?? 120
1536
+ });
1493
1537
  }
1494
1538
 
1495
1539
  // src/useEpub.ts
1496
1540
  import { parseEpub } from "@libraz/mejiro/epub";
1497
- import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef7, useState as useState5 } from "react";
1541
+ import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef8, useState as useState5 } from "react";
1498
1542
  function useEpub(options = {}) {
1499
1543
  const [epub, setEpub] = useState5(null);
1500
1544
  const [loading, setLoading] = useState5(false);
1501
1545
  const [error, setError] = useState5(null);
1502
- const requestIdRef = useRef7(0);
1503
- const onLoadRef = useRef7(options.onLoad);
1546
+ const requestIdRef = useRef8(0);
1547
+ const onLoadRef = useRef8(options.onLoad);
1504
1548
  onLoadRef.current = options.onLoad;
1505
- const onErrorRef = useRef7(options.onError);
1549
+ const onErrorRef = useRef8(options.onError);
1506
1550
  onErrorRef.current = options.onError;
1507
- const fetchOptionsRef = useRef7(options.fetchOptions);
1551
+ const fetchOptionsRef = useRef8(options.fetchOptions);
1508
1552
  fetchOptionsRef.current = options.fetchOptions;
1509
- const fetchEpubRef = useRef7(options.fetchEpub);
1553
+ const fetchEpubRef = useRef8(options.fetchEpub);
1510
1554
  fetchEpubRef.current = options.fetchEpub;
1511
- const limitsRef = useRef7(options.limits);
1555
+ const limitsRef = useRef8(options.limits);
1512
1556
  limitsRef.current = options.limits;
1513
1557
  const load = useCallback3(async (read) => {
1514
1558
  const requestId = ++requestIdRef.current;
@@ -1544,8 +1588,21 @@ function useEpub(options = {}) {
1544
1588
  );
1545
1589
  const defaultUrl = options.defaultUrl;
1546
1590
  useEffect5(() => {
1547
- if (defaultUrl) void loadUrl(defaultUrl);
1591
+ if (!defaultUrl) return;
1592
+ void loadUrl(defaultUrl);
1593
+ const requestId = requestIdRef.current;
1594
+ return () => {
1595
+ if (requestIdRef.current !== requestId) return;
1596
+ requestIdRef.current++;
1597
+ setLoading(false);
1598
+ };
1548
1599
  }, [defaultUrl, loadUrl]);
1600
+ useEffect5(
1601
+ () => () => {
1602
+ requestIdRef.current++;
1603
+ },
1604
+ []
1605
+ );
1549
1606
  const replaceEpub = useCallback3((book) => {
1550
1607
  requestIdRef.current++;
1551
1608
  setLoading(false);
@@ -1557,26 +1614,35 @@ function useEpub(options = {}) {
1557
1614
 
1558
1615
  // src/useMejiroBook.ts
1559
1616
  import { MejiroBook } from "@libraz/mejiro/book";
1560
- import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef8, useState as useState6 } from "react";
1617
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef9, useState as useState6 } from "react";
1561
1618
  function useMejiroBook(initial, options = {}) {
1562
- const bookRef = useRef8(null);
1619
+ const bookRef = useRef9(null);
1563
1620
  if (!bookRef.current) bookRef.current = new MejiroBook(initial);
1564
1621
  const [snapshot, setLocal] = useState6(initial);
1565
- const optionsRef = useRef8(options);
1622
+ const optionsRef = useRef9(options);
1566
1623
  optionsRef.current = options;
1567
- const pendingRef = useRef8(null);
1624
+ const pendingRef = useRef9(null);
1625
+ const flushGenerationRef = useRef9(0);
1568
1626
  const flush = useCallback4(() => {
1569
1627
  const pending = pendingRef.current;
1570
1628
  if (!pending) return;
1571
1629
  pendingRef.current = null;
1572
1630
  if (pending.timer) clearTimeout(pending.timer);
1573
1631
  const { waiters } = pending;
1632
+ const generation = ++flushGenerationRef.current;
1574
1633
  const applied = bookRef.current?.setOptions(pending.patch) ?? Promise.resolve();
1634
+ const syncSnapshot = () => {
1635
+ const book = bookRef.current;
1636
+ if (!book || generation !== flushGenerationRef.current) return;
1637
+ setLocal((prev) => ({ ...prev, ...book.getOptions(), ...pendingRef.current?.patch }));
1638
+ };
1575
1639
  void applied.then(
1576
1640
  () => {
1641
+ syncSnapshot();
1577
1642
  for (const waiter of waiters) waiter.resolve();
1578
1643
  },
1579
1644
  (err) => {
1645
+ syncSnapshot();
1580
1646
  const error = toError(err);
1581
1647
  const handler = optionsRef.current.onError;
1582
1648
  if (handler) {
@@ -1627,7 +1693,7 @@ function useMejiroBook(initial, options = {}) {
1627
1693
  }
1628
1694
 
1629
1695
  // src/useSpread.ts
1630
- import { useCallback as useCallback5, useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as useRef9, useState as useState7 } from "react";
1696
+ import { useCallback as useCallback5, useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as useRef10, useState as useState7 } from "react";
1631
1697
  function ignoreNavigationKey(e) {
1632
1698
  if (e.defaultPrevented) return true;
1633
1699
  if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
@@ -1657,7 +1723,7 @@ function useSpread(layout, options = {}) {
1657
1723
  const enableKeyboard = options.enableKeyboard ?? true;
1658
1724
  const turnDuration = options.turnDuration ?? 180;
1659
1725
  const single = options.single ?? false;
1660
- const onChangeRef = useRef9(options.onChange);
1726
+ const onChangeRef = useRef10(options.onChange);
1661
1727
  onChangeRef.current = options.onChange;
1662
1728
  const [spreadIdx, setSpreadIdx] = useState7(0);
1663
1729
  const [indexSingle, setIndexSingle] = useState7(single);
@@ -1667,15 +1733,16 @@ function useSpread(layout, options = {}) {
1667
1733
  }
1668
1734
  const [spread, setSpread] = useState7(null);
1669
1735
  const [turning, setTurning] = useState7(false);
1670
- const layoutRef = useRef9(null);
1736
+ const layoutRef = useRef10(null);
1671
1737
  layoutRef.current = layout;
1672
- const spreadIdxRef = useRef9(0);
1738
+ const spreadIdxRef = useRef10(0);
1673
1739
  spreadIdxRef.current = spreadIdx;
1674
- const singleRef = useRef9(single);
1740
+ const singleRef = useRef10(single);
1675
1741
  singleRef.current = single;
1676
- const turnTimerRef = useRef9(null);
1677
- const layoutGenerationRef = useRef9(0);
1678
- const lastEmittedRef = useRef9(null);
1742
+ const turnTimerRef = useRef10(null);
1743
+ const turnTargetRef = useRef10(null);
1744
+ const layoutGenerationRef = useRef10(0);
1745
+ const lastEmittedRef = useRef10(null);
1679
1746
  const totalPages = spread?.totalPages ?? 0;
1680
1747
  const totalSpreads = navigationCount(totalPages, single);
1681
1748
  const firstPage = firstPageOf(spreadIdx, single);
@@ -1695,6 +1762,7 @@ function useSpread(layout, options = {}) {
1695
1762
  clearTimeout(turnTimerRef.current);
1696
1763
  turnTimerRef.current = null;
1697
1764
  }
1765
+ turnTargetRef.current = null;
1698
1766
  setTurning(false);
1699
1767
  setSpreadIdx(0);
1700
1768
  if (!layout) {
@@ -1716,28 +1784,44 @@ function useSpread(layout, options = {}) {
1716
1784
  }
1717
1785
  lastEmittedRef.current = { layout, spreadIdx };
1718
1786
  }, [layout, spreadIdx, layoutSpreadIdx]);
1787
+ const navigationTarget = useCallback5(() => {
1788
+ const pending = turnTargetRef.current;
1789
+ if (!pending) return spreadIdxRef.current;
1790
+ return indexOfPageIn(firstPageOf(pending.index, pending.single), singleRef.current);
1791
+ }, []);
1719
1792
  const goTo = useCallback5(
1720
1793
  (index) => {
1721
1794
  if (!layoutRef.current) return;
1722
1795
  const targetSingle = singleRef.current;
1723
1796
  const max = navigationCount(layoutRef.current.totalPages, targetSingle) - 1;
1724
1797
  const target = Math.max(0, Math.min(max, index));
1798
+ if (turnTimerRef.current) {
1799
+ if (target === navigationTarget()) return;
1800
+ clearTimeout(turnTimerRef.current);
1801
+ turnTimerRef.current = null;
1802
+ const landed = navigationTarget();
1803
+ turnTargetRef.current = null;
1804
+ spreadIdxRef.current = landed;
1805
+ setSpreadIdx(landed);
1806
+ setTurning(false);
1807
+ }
1725
1808
  if (target === spreadIdxRef.current) return;
1726
1809
  if (turnDuration > 0) {
1727
1810
  const generation = layoutGenerationRef.current;
1728
- if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
1811
+ turnTargetRef.current = { index: target, single: targetSingle };
1729
1812
  setTurning(true);
1730
1813
  turnTimerRef.current = setTimeout(() => {
1731
1814
  if (generation !== layoutGenerationRef.current) return;
1732
- setSpreadIdx(indexOfPageIn(firstPageOf(target, targetSingle), singleRef.current));
1815
+ setSpreadIdx(navigationTarget());
1733
1816
  setTurning(false);
1817
+ turnTargetRef.current = null;
1734
1818
  turnTimerRef.current = null;
1735
1819
  }, turnDuration);
1736
1820
  } else {
1737
1821
  setSpreadIdx(target);
1738
1822
  }
1739
1823
  },
1740
- [turnDuration]
1824
+ [turnDuration, navigationTarget]
1741
1825
  );
1742
1826
  const setSpreadIndex = useCallback5((index) => {
1743
1827
  if (!layoutRef.current) return;
@@ -1745,6 +1829,7 @@ function useSpread(layout, options = {}) {
1745
1829
  clearTimeout(turnTimerRef.current);
1746
1830
  turnTimerRef.current = null;
1747
1831
  }
1832
+ turnTargetRef.current = null;
1748
1833
  setTurning(false);
1749
1834
  const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
1750
1835
  setSpreadIdx(Math.max(0, Math.min(max, index)));
@@ -1757,8 +1842,8 @@ function useSpread(layout, options = {}) {
1757
1842
  },
1758
1843
  [layout, single]
1759
1844
  );
1760
- const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
1761
- const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
1845
+ const next = useCallback5(() => goTo(navigationTarget() + 1), [goTo, navigationTarget]);
1846
+ const prev = useCallback5(() => goTo(navigationTarget() - 1), [goTo, navigationTarget]);
1762
1847
  useEffect7(() => {
1763
1848
  if (!enableKeyboard) return;
1764
1849
  const onKey = (e) => {
@@ -1823,10 +1908,16 @@ function scrollTargetPage(spread, userPage) {
1823
1908
  return userPage != null && spread.indexOfPage(userPage) === spread.spreadIdx ? userPage : spread.firstPage;
1824
1909
  }
1825
1910
  function createLifecycleTracker(sink) {
1911
+ let lastBook = null;
1826
1912
  let last = null;
1827
1913
  let dwell = null;
1828
- return (chapter, spreadIdx, view, controlledSpreadIdx) => {
1914
+ return (book, chapter, spreadIdx, view, controlledSpreadIdx) => {
1829
1915
  if (!view) return;
1916
+ if (book !== lastBook) {
1917
+ lastBook = book;
1918
+ last = null;
1919
+ dwell = null;
1920
+ }
1830
1921
  const totalSpreads = view.total;
1831
1922
  if (controlledSpreadIdx != null) {
1832
1923
  const target = Math.max(0, Math.min(totalSpreads - 1, controlledSpreadIdx));
@@ -1844,6 +1935,49 @@ function createLifecycleTracker(sink) {
1844
1935
  if (spreadIdx === totalSpreads - 1) sink.chapterFinished(chapter);
1845
1936
  };
1846
1937
  }
1938
+ function clampChapter(index, book) {
1939
+ if (!book) return index;
1940
+ return Math.max(0, Math.min(book.chapters.length - 1, index));
1941
+ }
1942
+ function createAnchorResolver(nav) {
1943
+ let pending = null;
1944
+ const settle = () => {
1945
+ const settled = pending;
1946
+ pending = null;
1947
+ settled?.resolve();
1948
+ };
1949
+ const update = (target) => {
1950
+ const current = pending;
1951
+ if (!(current && target.book)) return;
1952
+ const { anchor } = current;
1953
+ if (!(anchor.chapter >= 0 && anchor.chapter < target.book.chapters.length)) {
1954
+ settle();
1955
+ return;
1956
+ }
1957
+ if (anchor.chapter !== target.chapter) {
1958
+ if (current.chapterRequested) return;
1959
+ current.chapterRequested = true;
1960
+ nav.goToChapter(anchor.chapter);
1961
+ return;
1962
+ }
1963
+ if (!target.layout) return;
1964
+ const loc = target.layout.locateAnchor({
1965
+ paragraph: anchor.paragraph,
1966
+ charIndex: anchor.charIndex
1967
+ });
1968
+ if (loc) nav.goToPage(loc.pageIdx);
1969
+ settle();
1970
+ };
1971
+ return {
1972
+ request: (anchor, target) => new Promise((resolve) => {
1973
+ settle();
1974
+ pending = { anchor, resolve, chapterRequested: false };
1975
+ update(target);
1976
+ }),
1977
+ update,
1978
+ dispose: settle
1979
+ };
1980
+ }
1847
1981
  function MejiroReaderInner(props, ref) {
1848
1982
  const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
1849
1983
  const dialectProp = "dialect" in props ? props.dialect : void 0;
@@ -1890,12 +2024,11 @@ function MejiroReaderInner(props, ref) {
1890
2024
  onPageRead,
1891
2025
  onChapterCompleted
1892
2026
  } = props;
1893
- const surfaceRef = useRef10(null);
1894
- const fileRef = useRef10(null);
2027
+ const surfaceRef = useRef11(null);
2028
+ const fileRef = useRef11(null);
1895
2029
  const [settingsOpen, setSettingsOpen] = useState8(false);
1896
2030
  const [chapterState, setChapterState] = useState8(chapterProp ?? 0);
1897
2031
  const chapterIsUncontrolled = chapterProp == null;
1898
- const chapter = chapterProp ?? chapterState;
1899
2032
  const [chromeHidden, setChromeHidden] = useState8(false);
1900
2033
  const [userScroll, setUserScroll] = useState8(
1901
2034
  null
@@ -1924,7 +2057,7 @@ function MejiroReaderInner(props, ref) {
1924
2057
  return () => observer.disconnect();
1925
2058
  }, [spreadMode]);
1926
2059
  const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
1927
- const resolvedGeometry = useMemo4(() => {
2060
+ const resolvedGeometry = useMemo5(() => {
1928
2061
  const columns = effectiveSingle ? 1 : 2;
1929
2062
  const geometry = pageGeometryProp ?? {};
1930
2063
  if (fit !== "width") return mergeDefined({ columns }, geometry);
@@ -1940,29 +2073,38 @@ function MejiroReaderInner(props, ref) {
1940
2073
  geometry
1941
2074
  );
1942
2075
  }, [fit, pageGeometryProp, effectiveSingle]);
1943
- const surfaceAspect = useMemo4(() => {
2076
+ const surfaceAspect = useMemo5(() => {
1944
2077
  const columns = effectiveSingle ? 1 : 2;
1945
2078
  const aspect = resolvedGeometry?.aspect ?? DEFAULT_PAGE_GEOMETRY.aspect;
1946
2079
  return `${columns} / ${aspect}`;
1947
2080
  }, [effectiveSingle, resolvedGeometry]);
1948
- const resolvedOptions = useMemo4(
2081
+ const resolvedOptions = useMemo5(
1949
2082
  () => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
1950
2083
  [optionsProp]
1951
2084
  );
1952
- const onErrorRef = useRef10(onError);
2085
+ const onErrorRef = useRef11(onError);
1953
2086
  onErrorRef.current = onError;
1954
2087
  const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
1955
2088
  const {
1956
2089
  book,
1957
2090
  options: bookOptions,
1958
- setOptions
2091
+ setOptions: applyBookOptions
1959
2092
  } = useMejiroBook(resolvedOptions, {
1960
2093
  debounceMs: OPTIONS_DEBOUNCE_MS,
1961
2094
  onError: reportError
1962
2095
  });
2096
+ const lastApplyRef = useRef11(Promise.resolve());
2097
+ const setOptions = useCallback6(
2098
+ (partial) => {
2099
+ const applied = applyBookOptions(partial);
2100
+ lastApplyRef.current = applied;
2101
+ return applied;
2102
+ },
2103
+ [applyBookOptions]
2104
+ );
1963
2105
  const optionsPropKey = JSON.stringify(resolvedOptions);
1964
- const syncedOptionsPropKeyRef = useRef10(optionsPropKey);
1965
- const resolvedOptionsRef = useRef10(resolvedOptions);
2106
+ const syncedOptionsPropKeyRef = useRef11(optionsPropKey);
2107
+ const resolvedOptionsRef = useRef11(resolvedOptions);
1966
2108
  resolvedOptionsRef.current = resolvedOptions;
1967
2109
  useEffect8(() => {
1968
2110
  if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
@@ -1970,7 +2112,7 @@ function MejiroReaderInner(props, ref) {
1970
2112
  void setOptions(resolvedOptionsRef.current);
1971
2113
  }, [optionsPropKey, setOptions]);
1972
2114
  const manuscriptSourceKey = manuscriptKey(manuscriptProp);
1973
- const synthesizedEpub = useMemo4(() => {
2115
+ const synthesizedEpub = useMemo5(() => {
1974
2116
  if (manuscriptSourceKey === void 0) return null;
1975
2117
  const chapters = JSON.parse(manuscriptSourceKey);
1976
2118
  return manuscriptToEpubBook(chapters, { dialect: dialectProp });
@@ -1991,15 +2133,19 @@ function MejiroReaderInner(props, ref) {
1991
2133
  const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
1992
2134
  const isManuscriptSource = manuscriptProp !== void 0;
1993
2135
  const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
2136
+ const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
2137
+ const bookKey = controlled ? sourceKey : epubCtx.epub;
2138
+ const chapter = clampChapter(chapterProp ?? chapterState, e);
2139
+ const filePicker = enableDropZone && !controlled;
1994
2140
  useEffect8(() => {
1995
2141
  if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
1996
2142
  }, [epubCtx.error]);
1997
- const pageGeometryRef = useRef10(resolvedGeometry);
2143
+ const pageGeometryRef = useRef11(resolvedGeometry);
1998
2144
  pageGeometryRef.current = resolvedGeometry;
1999
- const spreadAnchorRef = useRef10(() => null);
2000
- const spreadIdxPropRef = useRef10(spreadIdxProp);
2145
+ const spreadAnchorRef = useRef11(() => null);
2146
+ const spreadIdxPropRef = useRef11(spreadIdxProp);
2001
2147
  spreadIdxPropRef.current = spreadIdxProp;
2002
- const visibleAnchorRef = useRef10(
2148
+ const visibleAnchorRef = useRef11(
2003
2149
  null
2004
2150
  );
2005
2151
  const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef, {
@@ -2012,10 +2158,13 @@ function MejiroReaderInner(props, ref) {
2012
2158
  const visible = visibleAnchorRef.current;
2013
2159
  if (visible?.layout === layout2) return visible.anchor;
2014
2160
  return spreadAnchorRef.current();
2015
- }
2161
+ },
2162
+ onError: reportError
2016
2163
  });
2017
- const spreadChangedRef = useRef10(void 0);
2164
+ const reportedSpreadIdxRef = useRef11(null);
2165
+ const spreadChangedRef = useRef11(void 0);
2018
2166
  spreadChangedRef.current = (i) => {
2167
+ reportedSpreadIdxRef.current = i;
2019
2168
  onSpreadChange?.(i);
2020
2169
  onSpreadIdxChange?.(i);
2021
2170
  };
@@ -2025,9 +2174,9 @@ function MejiroReaderInner(props, ref) {
2025
2174
  onChange: (i) => spreadChangedRef.current?.(i)
2026
2175
  });
2027
2176
  spreadAnchorRef.current = () => spreadCtx.anchorAt(spreadCtx.spreadIdx);
2028
- const indexOfPageRef = useRef10(spreadCtx.indexOfPage);
2177
+ const indexOfPageRef = useRef11(spreadCtx.indexOfPage);
2029
2178
  indexOfPageRef.current = spreadCtx.indexOfPage;
2030
- const anchorSingleRef = useRef10(effectiveSingle);
2179
+ const anchorSingleRef = useRef11(effectiveSingle);
2031
2180
  useLayoutEffect4(() => {
2032
2181
  const l = layoutCtx.layout;
2033
2182
  const flipped = anchorSingleRef.current !== effectiveSingle;
@@ -2035,7 +2184,7 @@ function MejiroReaderInner(props, ref) {
2035
2184
  if (flipped && l && visibleAnchorRef.current?.layout === l) return;
2036
2185
  visibleAnchorRef.current = l ? { layout: l, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
2037
2186
  }, [layoutCtx.layout, spreadCtx.spreadIdx, spreadCtx.anchorAt, effectiveSingle]);
2038
- const setSpreadRef = useRef10(spreadCtx.setSpread);
2187
+ const setSpreadRef = useRef11(spreadCtx.setSpread);
2039
2188
  setSpreadRef.current = spreadCtx.setSpread;
2040
2189
  useLayoutEffect4(() => {
2041
2190
  const anchor = layoutCtx.pendingRestore.current;
@@ -2046,10 +2195,8 @@ function MejiroReaderInner(props, ref) {
2046
2195
  visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
2047
2196
  }, [layoutCtx.layout]);
2048
2197
  const optionsKey = reflowOptionsKey(bookOptions);
2049
- const optionsKeyRef = useRef10(optionsKey);
2050
- const bookOptionsRef = useRef10(bookOptions);
2051
- bookOptionsRef.current = bookOptions;
2052
- const recomputeRef = useRef10(layoutCtx.recompute);
2198
+ const optionsKeyRef = useRef11(optionsKey);
2199
+ const recomputeRef = useRef11(layoutCtx.recompute);
2053
2200
  recomputeRef.current = layoutCtx.recompute;
2054
2201
  useEffect8(() => {
2055
2202
  if (optionsKeyRef.current === optionsKey) return;
@@ -2057,7 +2204,7 @@ function MejiroReaderInner(props, ref) {
2057
2204
  const timer = setTimeout(() => {
2058
2205
  void (async () => {
2059
2206
  try {
2060
- await setOptions({ ...bookOptionsRef.current });
2207
+ await lastApplyRef.current;
2061
2208
  await recomputeRef.current({ blank: false });
2062
2209
  } catch (err) {
2063
2210
  reportError(toError(err));
@@ -2065,9 +2212,9 @@ function MejiroReaderInner(props, ref) {
2065
2212
  })();
2066
2213
  }, OPTIONS_DEBOUNCE_MS);
2067
2214
  return () => clearTimeout(timer);
2068
- }, [optionsKey, setOptions, reportError]);
2215
+ }, [optionsKey, reportError]);
2069
2216
  const geometryKey = JSON.stringify(resolvedGeometry ?? null);
2070
- const geometryKeyRef = useRef10(geometryKey);
2217
+ const geometryKeyRef = useRef11(geometryKey);
2071
2218
  useEffect8(() => {
2072
2219
  if (geometryKeyRef.current === geometryKey) return;
2073
2220
  geometryKeyRef.current = geometryKey;
@@ -2077,7 +2224,7 @@ function MejiroReaderInner(props, ref) {
2077
2224
  defaultX: IMAGE_DEFAULT_X,
2078
2225
  onUpdate: () => spreadCtx.refresh()
2079
2226
  });
2080
- const annotationRects = useMemo4(() => {
2227
+ const annotationRects = useMemo5(() => {
2081
2228
  if (!(annotations && layoutCtx.layout)) return [];
2082
2229
  const result = [];
2083
2230
  for (const annotation of annotations) {
@@ -2090,13 +2237,12 @@ function MejiroReaderInner(props, ref) {
2090
2237
  }
2091
2238
  return result;
2092
2239
  }, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
2093
- const clearImagesRef = useRef10(imageCtx.clearImages);
2240
+ const clearImagesRef = useRef11(imageCtx.clearImages);
2094
2241
  clearImagesRef.current = imageCtx.clearImages;
2095
- const onLoadRef = useRef10(onLoad);
2242
+ const onLoadRef = useRef11(onLoad);
2096
2243
  onLoadRef.current = onLoad;
2097
- const eRef = useRef10(e);
2244
+ const eRef = useRef11(e);
2098
2245
  eRef.current = e;
2099
- const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
2100
2246
  useEffect8(() => {
2101
2247
  if (!controlled) return;
2102
2248
  if (chapterIsUncontrolled) setChapterState(0);
@@ -2104,16 +2250,19 @@ function MejiroReaderInner(props, ref) {
2104
2250
  book.clearCache();
2105
2251
  if (eRef.current) onLoadRef.current?.(eRef.current);
2106
2252
  }, [controlled, sourceKey, book, chapterIsUncontrolled]);
2107
- const imagesChapterRef = useRef10(chapter);
2253
+ const imagesChapterRef = useRef11(chapter);
2108
2254
  useEffect8(() => {
2109
2255
  if (imagesChapterRef.current === chapter) return;
2110
2256
  imagesChapterRef.current = chapter;
2111
2257
  clearImagesRef.current();
2112
2258
  }, [chapter]);
2113
- const spreadGoToRef = useRef10(spreadCtx.goTo);
2259
+ const spreadGoToRef = useRef11(spreadCtx.goTo);
2114
2260
  spreadGoToRef.current = spreadCtx.goTo;
2115
- const appliedSpreadIdxPropRef = useRef10(spreadIdxProp);
2261
+ const appliedSpreadIdxPropRef = useRef11(spreadIdxProp);
2262
+ const [hostTurn, setHostTurn] = useState8(0);
2116
2263
  useEffect8(() => {
2264
+ const reported = reportedSpreadIdxRef.current;
2265
+ reportedSpreadIdxRef.current = null;
2117
2266
  if (spreadIdxProp == null) {
2118
2267
  appliedSpreadIdxPropRef.current = spreadIdxProp;
2119
2268
  return;
@@ -2122,22 +2271,26 @@ function MejiroReaderInner(props, ref) {
2122
2271
  appliedSpreadIdxPropRef.current = spreadIdxProp;
2123
2272
  if (spreadCtx.spreadIdx === spreadIdxProp) return;
2124
2273
  if (propChanged) spreadGoToRef.current(spreadIdxProp);
2274
+ else if (reported === spreadCtx.spreadIdx) setHostTurn((n) => n + 1);
2125
2275
  else setSpreadRef.current(spreadIdxProp);
2126
- }, [spreadIdxProp, spreadCtx.spreadIdx]);
2276
+ }, [spreadIdxProp, spreadCtx.spreadIdx, hostTurn]);
2127
2277
  useLayoutEffect4(() => {
2128
2278
  const next = spreadIdxPropRef.current;
2129
2279
  if (next == null || !layoutCtx.layout) return;
2130
2280
  setSpreadRef.current(next);
2131
2281
  }, [layoutCtx.layout]);
2132
2282
  const onChapter = useCallback6(
2133
- (i) => {
2283
+ (index) => {
2284
+ const i = clampChapter(index, e);
2134
2285
  if (i === chapter) return;
2135
2286
  if (chapterProp == null) setChapterState(i);
2136
2287
  onChapterChange?.(i);
2137
2288
  },
2138
- [chapter, chapterProp, onChapterChange]
2289
+ [chapter, chapterProp, onChapterChange, e]
2139
2290
  );
2140
- const listenersRef = useRef10(/* @__PURE__ */ new Map());
2291
+ const onChapterRef = useRef11(onChapter);
2292
+ onChapterRef.current = onChapter;
2293
+ const listenersRef = useRef11(/* @__PURE__ */ new Map());
2141
2294
  const emit = useCallback6(
2142
2295
  (event, payload) => {
2143
2296
  const set = listenersRef.current.get(event);
@@ -2146,44 +2299,33 @@ function MejiroReaderInner(props, ref) {
2146
2299
  },
2147
2300
  []
2148
2301
  );
2149
- const pendingAnchorRef = useRef10(null);
2150
- const layoutOwnerRef = useRef10({
2302
+ const layoutOwnerRef = useRef11({
2151
2303
  layout: null,
2152
- chapter
2304
+ chapter,
2305
+ book: bookKey
2153
2306
  });
2154
2307
  if (layoutOwnerRef.current.layout !== layoutCtx.layout) {
2155
- layoutOwnerRef.current = { layout: layoutCtx.layout, chapter };
2308
+ layoutOwnerRef.current = { layout: layoutCtx.layout, chapter, book: bookKey };
2156
2309
  }
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]);
2310
+ const owner = layoutOwnerRef.current;
2311
+ const layout = owner.chapter === chapter && owner.book === bookKey ? layoutCtx.layout : null;
2312
+ const anchorTarget = { book: epubCtx.loading ? null : e, chapter, layout };
2313
+ const anchorTargetRef = useRef11(anchorTarget);
2314
+ anchorTargetRef.current = anchorTarget;
2315
+ const anchorResolverRef = useRef11(null);
2316
+ anchorResolverRef.current ??= createAnchorResolver({
2317
+ goToChapter: (i) => onChapterRef.current(i),
2318
+ goToPage: (pageIdx) => spreadGoToRef.current(indexOfPageRef.current(pageIdx))
2319
+ });
2172
2320
  useEffect8(() => {
2173
- tryApplyPendingAnchor();
2174
- }, [tryApplyPendingAnchor]);
2175
- useEffect8(
2176
- () => () => {
2177
- pendingAnchorRef.current?.resolve();
2178
- pendingAnchorRef.current = null;
2179
- },
2180
- []
2181
- );
2182
- const onPageReadRef = useRef10(onPageRead);
2321
+ anchorResolverRef.current?.update(anchorTargetRef.current);
2322
+ }, [anchorTarget.book, chapter, layout]);
2323
+ useEffect8(() => () => anchorResolverRef.current?.dispose(), []);
2324
+ const onPageReadRef = useRef11(onPageRead);
2183
2325
  onPageReadRef.current = onPageRead;
2184
- const onChapterCompletedRef = useRef10(onChapterCompleted);
2326
+ const onChapterCompletedRef = useRef11(onChapterCompleted);
2185
2327
  onChapterCompletedRef.current = onChapterCompleted;
2186
- const trackLifecycleRef = useRef10(null);
2328
+ const trackLifecycleRef = useRef11(null);
2187
2329
  trackLifecycleRef.current ??= createLifecycleTracker({
2188
2330
  spreadChanged: (ch, spreadIdx) => emit("spreadChanged", { chapter: ch, spreadIdx }),
2189
2331
  chapterFinished: (ch) => {
@@ -2194,8 +2336,9 @@ function MejiroReaderInner(props, ref) {
2194
2336
  });
2195
2337
  useEffect8(() => {
2196
2338
  const view = layout ? { total: spreadCtx.totalSpreads, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
2197
- trackLifecycleRef.current?.(chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
2339
+ trackLifecycleRef.current?.(bookKey, chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
2198
2340
  }, [
2341
+ bookKey,
2199
2342
  chapter,
2200
2343
  spreadCtx.spreadIdx,
2201
2344
  spreadCtx.totalSpreads,
@@ -2203,7 +2346,7 @@ function MejiroReaderInner(props, ref) {
2203
2346
  layout,
2204
2347
  spreadIdxProp
2205
2348
  ]);
2206
- const prevTurningRef = useRef10(false);
2349
+ const prevTurningRef = useRef11(false);
2207
2350
  useEffect8(() => {
2208
2351
  if (spreadCtx.turning && !prevTurningRef.current) {
2209
2352
  emit("turnStart", { from: spreadCtx.spreadIdx });
@@ -2225,12 +2368,7 @@ function MejiroReaderInner(props, ref) {
2225
2368
  totalPages: spreadCtx.totalPages,
2226
2369
  totalSpreads: spreadCtx.totalSpreads
2227
2370
  }),
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
- }),
2371
+ goToAnchor: (anchor) => anchorResolverRef.current?.request(anchor, anchorTargetRef.current) ?? Promise.resolve(),
2234
2372
  getAnchor: () => {
2235
2373
  if (!layout) return null;
2236
2374
  const inCh = spreadCtx.anchorAt(spreadCtx.spreadIdx);
@@ -2269,8 +2407,7 @@ function MejiroReaderInner(props, ref) {
2269
2407
  onChapter,
2270
2408
  chapter,
2271
2409
  layout,
2272
- setOptions,
2273
- tryApplyPendingAnchor
2410
+ setOptions
2274
2411
  ]
2275
2412
  );
2276
2413
  const editable = {
@@ -2293,7 +2430,7 @@ function MejiroReaderInner(props, ref) {
2293
2430
  const layoutReady = e && spreadCtx.spread && layoutCtx.layout && layoutCtx.pageWidth > 0;
2294
2431
  const themeName = typeof theme === "string" ? theme : theme.name;
2295
2432
  const themeOverride = typeof theme === "string" ? void 0 : theme.override;
2296
- const themeStyle = useMemo4(() => {
2433
+ const themeStyle = useMemo5(() => {
2297
2434
  const pad = pageGeometryProp?.padding;
2298
2435
  const styleVars = {};
2299
2436
  if (pad?.x != null) styleVars["--mejiro-page-pad-x"] = `${pad.x}px`;
@@ -2325,7 +2462,7 @@ function MejiroReaderInner(props, ref) {
2325
2462
  fontLabel
2326
2463
  }
2327
2464
  ),
2328
- enableDropZone && /* @__PURE__ */ jsx13(
2465
+ filePicker && /* @__PURE__ */ jsx13(
2329
2466
  "button",
2330
2467
  {
2331
2468
  type: "button",
@@ -2393,7 +2530,7 @@ function MejiroReaderInner(props, ref) {
2393
2530
  /* @__PURE__ */ jsxs8("div", { className: `mejiro-reader-body${showChapterPanel ? " has-chapter-panel" : ""}`, children: [
2394
2531
  showChapterPanel && /* @__PURE__ */ jsx13(MejiroChapterNav, { epub: e, chapter, onChange: onChapter, variant: "panel" }),
2395
2532
  /* @__PURE__ */ jsxs8("div", { ref: surfaceRef, className: "mejiro-reader-surface", children: [
2396
- !(e || epubCtx.loading) && enableDropZone && /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
2533
+ !(e || epubCtx.loading) && filePicker && /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
2397
2534
  epubCtx.loading && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-loading", children: resolvedMessages.loading }),
2398
2535
  !layoutReady && fallback && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-fallback", children: fallback }),
2399
2536
  layoutReady && mode === "scroll" && layoutCtx.layout && /* @__PURE__ */ jsx13(
@@ -2476,6 +2613,7 @@ function MejiroReaderInner(props, ref) {
2476
2613
  hidden: true,
2477
2614
  onChange: (ev) => {
2478
2615
  const file = ev.target.files?.[0];
2616
+ ev.target.value = "";
2479
2617
  if (file) void epubCtx.loadFile(file);
2480
2618
  }
2481
2619
  }
@@ -2493,6 +2631,55 @@ MejiroReader.displayName = "MejiroReader";
2493
2631
 
2494
2632
  // src/MejiroEditor.tsx
2495
2633
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
2634
+ function clampRubyRange(start, end, length) {
2635
+ if (length <= 0) return null;
2636
+ const from = Math.max(0, Math.min(start, length - 1));
2637
+ return { start: from, end: Math.max(from + 1, Math.min(end, length)) };
2638
+ }
2639
+ function commitPendingEdit(editor, chapterIndex, paragraphIndex, text, ruby) {
2640
+ const paragraphs = editor.book.chapters[chapterIndex]?.paragraphs;
2641
+ if (!paragraphs?.[paragraphIndex]) return false;
2642
+ const textChanged = text !== paragraphs[paragraphIndex].text;
2643
+ const rubyText = ruby?.rubyText.trim();
2644
+ const range = ruby ? clampRubyRange(ruby.start, ruby.end, [...text].length) : null;
2645
+ if (!(rubyText && range)) {
2646
+ if (textChanged) editor.updateParagraph(chapterIndex, paragraphIndex, { text });
2647
+ return textChanged;
2648
+ }
2649
+ editor.transaction(() => {
2650
+ if (textChanged) editor.updateParagraph(chapterIndex, paragraphIndex, { text });
2651
+ const committed = editor.book.chapters[chapterIndex].paragraphs[paragraphIndex];
2652
+ const newRuby = {
2653
+ kind: "ruby",
2654
+ startIndex: range.start,
2655
+ endIndex: range.end,
2656
+ rubyText,
2657
+ type: range.end - range.start === 1 ? "mono" : "group"
2658
+ };
2659
+ const inlineAnnotations = [
2660
+ ...committed.inlineAnnotations.filter(
2661
+ (ann) => ann.kind !== "ruby" || ann.endIndex <= range.start || ann.startIndex >= range.end
2662
+ ),
2663
+ newRuby
2664
+ ].sort((a, b) => a.startIndex - b.startIndex);
2665
+ editor.updateParagraph(chapterIndex, paragraphIndex, { inlineAnnotations });
2666
+ });
2667
+ return true;
2668
+ }
2669
+ function downloadEpub(buffer, filename) {
2670
+ const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2671
+ const a = document.createElement("a");
2672
+ a.href = url;
2673
+ a.download = filename;
2674
+ a.hidden = true;
2675
+ document.body.append(a);
2676
+ try {
2677
+ a.click();
2678
+ } finally {
2679
+ a.remove();
2680
+ setTimeout(() => URL.revokeObjectURL(url), 0);
2681
+ }
2682
+ }
2496
2683
  function utf16ToCodepoint(text, utf16Offset) {
2497
2684
  let cp = 0;
2498
2685
  let i = 0;
@@ -2544,9 +2731,9 @@ function MejiroEditor({
2544
2731
  onError
2545
2732
  }) {
2546
2733
  const messages = useI18n();
2547
- const imageInputRef = useRef11(null);
2548
- const textareaRef = useRef11(null);
2549
- const readerRef = useRef11(null);
2734
+ const imageInputRef = useRef12(null);
2735
+ const textareaRef = useRef12(null);
2736
+ const readerRef = useRef12(null);
2550
2737
  const [editor, setEditor] = useState9(null);
2551
2738
  const [loading, setLoading] = useState9(false);
2552
2739
  const [error, setError] = useState9(null);
@@ -2556,16 +2743,17 @@ function MejiroEditor({
2556
2743
  const [rubyStart, setRubyStart] = useState9(0);
2557
2744
  const [rubyEnd, setRubyEnd] = useState9(1);
2558
2745
  const [rubyText, setRubyText] = useState9("");
2559
- const loadRequestIdRef = useRef11(0);
2560
- const onLoadRef = useRef11(onLoad);
2561
- const onErrorRef = useRef11(onError);
2562
- const limitsRef = useRef11(limits);
2746
+ const loadRequestIdRef = useRef12(0);
2747
+ const onLoadRef = useRef12(onLoad);
2748
+ const onErrorRef = useRef12(onError);
2749
+ const limitsRef = useRef12(limits);
2563
2750
  limitsRef.current = limits;
2564
2751
  const book = editor?.book ?? null;
2565
2752
  const chapter = book?.chapters[selection.chapter] ?? null;
2566
2753
  const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
2567
- const textDirty = paragraph ? text !== paragraph.text : false;
2568
- const previewBook = useMemo5(() => {
2754
+ const sceneBreak = chapter ? paragraphBlock(chapter, selection.paragraph)?.paragraphKind === "sceneBreak" : false;
2755
+ const rubyRange = clampRubyRange(rubyStart, rubyEnd, [...text].length);
2756
+ const previewBook = useMemo6(() => {
2569
2757
  void revision;
2570
2758
  return book ? cloneEditableEpubBook(book) : null;
2571
2759
  }, [book, revision]);
@@ -2632,7 +2820,7 @@ function MejiroEditor({
2632
2820
  cancelled = true;
2633
2821
  };
2634
2822
  }, [epubUrl, loadBufferForRequest, reportError]);
2635
- const syncedRef = useRef11(
2823
+ const syncedRef = useRef12(
2636
2824
  null
2637
2825
  );
2638
2826
  useEffect9(() => {
@@ -2659,7 +2847,7 @@ function MejiroEditor({
2659
2847
  }, [previewBook, selection.chapter, selection.paragraph]);
2660
2848
  function selectParagraph(chapterIndex, paragraphIndex) {
2661
2849
  if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
2662
- if (textDirty) applyText();
2850
+ commitEdit();
2663
2851
  setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
2664
2852
  }
2665
2853
  function captureRubyRange() {
@@ -2672,41 +2860,22 @@ function MejiroEditor({
2672
2860
  setRubyStart(start);
2673
2861
  setRubyEnd(Math.max(start + 1, end));
2674
2862
  }
2675
- function applyText() {
2863
+ function commitEdit(ruby) {
2676
2864
  if (!editor) return;
2677
- editor.updateParagraph(selection.chapter, selection.paragraph, { text });
2678
- setRevision((value) => value + 1);
2865
+ if (commitPendingEdit(editor, selection.chapter, selection.paragraph, text, ruby)) {
2866
+ setRevision((value) => value + 1);
2867
+ }
2679
2868
  }
2680
2869
  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
- });
2870
+ if (!(rubyRange && rubyText.trim())) return;
2871
+ commitEdit({ start: rubyStart, end: rubyEnd, rubyText });
2702
2872
  setRubyText("");
2703
- setRevision((value) => value + 1);
2704
2873
  }
2705
2874
  async function addImage(file) {
2706
2875
  await withErrorReporting(async () => {
2707
2876
  if (!(editor && chapter)) return;
2708
2877
  const chapterIndex = selection.chapter;
2709
- const afterBlockId = paragraphBlockId(chapter, selection.paragraph);
2878
+ const afterBlockId = paragraphBlock(chapter, selection.paragraph)?.id;
2710
2879
  const data = await file.arrayBuffer();
2711
2880
  editor.addImage(chapterIndex, {
2712
2881
  filename: file.name,
@@ -2721,6 +2890,7 @@ function MejiroEditor({
2721
2890
  async function exportEpub() {
2722
2891
  await withErrorReporting(async () => {
2723
2892
  if (!editor) return;
2893
+ commitEdit();
2724
2894
  const watermark = exportPolicy?.watermark;
2725
2895
  const source = watermark ? watermarkedBook(editor.book, watermark) : editor.book;
2726
2896
  let buffer = await exportEditableEpub(source, assetResolver ? { assetResolver } : void 0);
@@ -2729,12 +2899,7 @@ function MejiroEditor({
2729
2899
  onExport?.(buffer);
2730
2900
  const allowDownload = exportPolicy ? exportPolicy.allowDownload !== false : decision !== false;
2731
2901
  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);
2902
+ downloadEpub(buffer, `${editor.title || "edited"}.epub`);
2738
2903
  }, reportError);
2739
2904
  }
2740
2905
  return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
@@ -2788,19 +2953,29 @@ function MejiroEditor({
2788
2953
  {
2789
2954
  ref: textareaRef,
2790
2955
  value: text,
2956
+ readOnly: sceneBreak,
2791
2957
  onChange: (event) => setText(event.target.value),
2792
2958
  onSelect: captureRubyRange
2793
2959
  }
2794
2960
  ),
2795
- /* @__PURE__ */ jsx14("button", { type: "button", className: "mejiro-editor-primary", onClick: applyText, children: messages.editorApplyText })
2961
+ /* @__PURE__ */ jsx14(
2962
+ "button",
2963
+ {
2964
+ type: "button",
2965
+ className: "mejiro-editor-primary",
2966
+ disabled: sceneBreak,
2967
+ onClick: () => commitEdit(),
2968
+ children: messages.editorApplyText
2969
+ }
2970
+ )
2796
2971
  ] }),
2797
2972
  enableRuby && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
2798
2973
  /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
2799
2974
  /* @__PURE__ */ jsx14("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
2800
2975
  /* @__PURE__ */ jsx14("p", { className: "mejiro-editor-range", children: format(messages.editorRubyRange, {
2801
- start: rubyStart,
2802
- end: rubyEnd,
2803
- count: Math.max(0, rubyEnd - rubyStart)
2976
+ start: rubyRange?.start ?? 0,
2977
+ end: rubyRange?.end ?? 0,
2978
+ count: rubyRange ? rubyRange.end - rubyRange.start : 0
2804
2979
  }) }),
2805
2980
  /* @__PURE__ */ jsx14(
2806
2981
  "input",
@@ -2815,7 +2990,7 @@ function MejiroEditor({
2815
2990
  {
2816
2991
  type: "button",
2817
2992
  className: "mejiro-editor-primary",
2818
- disabled: textDirty || !rubyText.trim(),
2993
+ disabled: !(rubyRange && rubyText.trim()),
2819
2994
  onClick: applyRuby,
2820
2995
  children: messages.editorApplyRuby
2821
2996
  }
@@ -2833,6 +3008,7 @@ function MejiroEditor({
2833
3008
  hidden: true,
2834
3009
  onChange: (event) => {
2835
3010
  const file = event.target.files?.[0];
3011
+ event.target.value = "";
2836
3012
  if (file) void addImage(file);
2837
3013
  }
2838
3014
  }
@@ -2851,11 +3027,11 @@ function MejiroEditor({
2851
3027
  ] })
2852
3028
  ] });
2853
3029
  }
2854
- function paragraphBlockId(chapter, paragraphIndex) {
3030
+ function paragraphBlock(chapter, paragraphIndex) {
2855
3031
  let current = 0;
2856
3032
  for (const block of chapter.blocks) {
2857
3033
  if (block.kind !== "paragraph") continue;
2858
- if (current === paragraphIndex) return block.id;
3034
+ if (current === paragraphIndex) return block;
2859
3035
  current++;
2860
3036
  }
2861
3037
  return void 0;
@@ -2863,15 +3039,15 @@ function paragraphBlockId(chapter, paragraphIndex) {
2863
3039
 
2864
3040
  // src/MejiroManuscriptEditor.tsx
2865
3041
  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";
3042
+ import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMemo8, useRef as useRef15, useState as useState12 } from "react";
2867
3043
 
2868
3044
  // src/MejiroNotationHighlighter.tsx
2869
3045
  import { tokenizeManuscriptSource } from "@libraz/mejiro";
2870
3046
  import {
2871
3047
  forwardRef as forwardRef2,
2872
3048
  useImperativeHandle as useImperativeHandle2,
2873
- useMemo as useMemo6,
2874
- useRef as useRef12,
3049
+ useMemo as useMemo7,
3050
+ useRef as useRef13,
2875
3051
  useState as useState10
2876
3052
  } from "react";
2877
3053
  import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
@@ -2888,12 +3064,13 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
2888
3064
  onCompositionEnd,
2889
3065
  ...rest
2890
3066
  }, ref) {
2891
- const textareaRef = useRef12(null);
2892
- const overlayRef = useRef12(null);
3067
+ const textareaRef = useRef13(null);
3068
+ const overlayRef = useRef13(null);
2893
3069
  useImperativeHandle2(ref, () => textareaRef.current, []);
2894
3070
  const [isComposing, setIsComposing] = useState10(false);
2895
- const lastSegmentsRef = useRef12(null);
2896
- const segments = useMemo6(() => {
3071
+ const [composingValue, setComposingValue] = useState10("");
3072
+ const lastSegmentsRef = useRef13(null);
3073
+ const segments = useMemo7(() => {
2897
3074
  if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
2898
3075
  const next = buildSegments(value, dialect);
2899
3076
  lastSegmentsRef.current = next;
@@ -2932,14 +3109,19 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
2932
3109
  ref: textareaRef,
2933
3110
  className: `mejiro-notation-textarea${className ? ` ${className}` : ""}`,
2934
3111
  style,
2935
- value,
2936
- onChange: (event) => onChange(event.target.value),
3112
+ value: isComposing ? composingValue : value,
3113
+ onChange: (event) => {
3114
+ if (isComposing) setComposingValue(event.target.value);
3115
+ onChange(event.target.value);
3116
+ },
2937
3117
  onCompositionStart: (event) => {
3118
+ setComposingValue(event.currentTarget.value);
2938
3119
  setIsComposing(true);
2939
3120
  onCompositionStart?.(event);
2940
3121
  },
2941
3122
  onCompositionEnd: (event) => {
2942
3123
  setIsComposing(false);
3124
+ onChange(event.currentTarget.value);
2943
3125
  onCompositionEnd?.(event);
2944
3126
  },
2945
3127
  onScroll: (event) => {
@@ -2981,7 +3163,7 @@ function buildSegments(text, dialect) {
2981
3163
  }
2982
3164
 
2983
3165
  // src/useManuscriptDraft.ts
2984
- import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef13, useState as useState11 } from "react";
3166
+ import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef14, useState as useState11 } from "react";
2985
3167
  var DEFAULT_DELAY = 800;
2986
3168
  function defaultChapter(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
2987
3169
  const index = existing.length;
@@ -2996,46 +3178,63 @@ function useManuscriptDraft(options = {}) {
2996
3178
  defaultChapterTitle,
2997
3179
  defaultChapterBody
2998
3180
  } = options;
2999
- const titleForRef = useRef13(defaultChapterTitle);
3181
+ const titleForRef = useRef14(defaultChapterTitle);
3000
3182
  titleForRef.current = defaultChapterTitle;
3001
- const bodyForRef = useRef13(defaultChapterBody);
3183
+ const bodyForRef = useRef14(defaultChapterBody);
3002
3184
  bodyForRef.current = defaultChapterBody;
3003
3185
  const [chapters, setChaptersState] = useState11(
3004
3186
  () => options.initialChapters?.length ? options.initialChapters : [defaultChapter([], defaultChapterTitle, defaultChapterBody)]
3005
3187
  );
3006
3188
  const [selected, setSelectedState] = useState11(0);
3007
3189
  const [autosaveError, setAutosaveError] = useState11(null);
3008
- const saveRef = useRef13(onAutosave);
3190
+ const saveRef = useRef14(onAutosave);
3009
3191
  saveRef.current = onAutosave;
3010
- const payloadRef = useRef13(autosavePayload);
3192
+ const payloadRef = useRef14(autosavePayload);
3011
3193
  payloadRef.current = autosavePayload;
3012
- const chaptersRef = useRef13(chapters);
3194
+ const chaptersRef = useRef14(chapters);
3013
3195
  chaptersRef.current = chapters;
3014
- const selectedRef = useRef13(selected);
3196
+ const selectedRef = useRef14(selected);
3015
3197
  selectedRef.current = selected;
3016
- const trackerRef = useRef13(null);
3198
+ const trackerRef = useRef14(null);
3017
3199
  if (!trackerRef.current)
3018
3200
  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);
3201
+ const dirtyRef = useRef14(false);
3202
+ const mountedRef = useRef14(true);
3203
+ const revisionRef = useRef14(0);
3204
+ const inFlightRef = useRef14(false);
3205
+ const flushQueuedRef = useRef14(false);
3023
3206
  const flushAutosave = useCallback8(() => {
3024
3207
  const callback = saveRef.current;
3025
- if (!(callback && dirtyRef.current) || inFlightRevisionRef.current === revisionRef.current) {
3208
+ if (!(callback && dirtyRef.current)) return;
3209
+ if (inFlightRef.current) {
3210
+ flushQueuedRef.current = true;
3026
3211
  return;
3027
3212
  }
3028
3213
  const revision = revisionRef.current;
3029
- inFlightRevisionRef.current = revision;
3030
3214
  const plain = snapshotChapters(chaptersRef.current);
3031
3215
  const payload = payloadRef.current ? payloadRef.current(plain) : plain;
3032
- void Promise.resolve(callback(payload)).then(() => {
3216
+ const markSaved = () => {
3033
3217
  if (revisionRef.current === revision) dirtyRef.current = false;
3034
- }).catch((err) => {
3218
+ };
3219
+ let saved;
3220
+ try {
3221
+ saved = callback(payload);
3222
+ } catch (err) {
3223
+ saved = Promise.reject(err);
3224
+ }
3225
+ if (saved === void 0) {
3226
+ markSaved();
3227
+ return;
3228
+ }
3229
+ inFlightRef.current = true;
3230
+ void Promise.resolve(saved).then(markSaved).catch((err) => {
3035
3231
  if (!mountedRef.current) return;
3036
3232
  setAutosaveError(toError(err));
3037
3233
  }).finally(() => {
3038
- if (inFlightRevisionRef.current === revision) inFlightRevisionRef.current = -1;
3234
+ inFlightRef.current = false;
3235
+ if (!flushQueuedRef.current) return;
3236
+ flushQueuedRef.current = false;
3237
+ flushAutosave();
3039
3238
  });
3040
3239
  }, []);
3041
3240
  useEffect10(() => {
@@ -3153,14 +3352,6 @@ function useManuscriptDraft(options = {}) {
3153
3352
 
3154
3353
  // src/MejiroManuscriptEditor.tsx
3155
3354
  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
3355
  function coverExtension(mediaType) {
3165
3356
  switch (mediaType) {
3166
3357
  case "image/gif":
@@ -3207,8 +3398,8 @@ function MejiroManuscriptEditor({
3207
3398
  panelSide = "right"
3208
3399
  }) {
3209
3400
  const messages = useI18n();
3210
- const coverInputRef = useRef14(null);
3211
- const bodyTextareaRef = useRef14(null);
3401
+ const coverInputRef = useRef15(null);
3402
+ const bodyTextareaRef = useRef15(null);
3212
3403
  const titleControlled = onTitleChange !== void 0;
3213
3404
  const authorControlled = onAuthorChange !== void 0;
3214
3405
  const coverControlled = onCoverChange !== void 0;
@@ -3308,7 +3499,7 @@ function MejiroManuscriptEditor({
3308
3499
  const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
3309
3500
  setExportError(null);
3310
3501
  onExport?.(buffer);
3311
- downloadEpub(buffer, title);
3502
+ downloadEpub(buffer, `${title || "book"}.epub`);
3312
3503
  },
3313
3504
  (error) => {
3314
3505
  setExportError(error);
@@ -3327,7 +3518,7 @@ function MejiroManuscriptEditor({
3327
3518
  onExport,
3328
3519
  title
3329
3520
  ]);
3330
- const previewManuscript = useMemo7(
3521
+ const previewManuscript = useMemo8(
3331
3522
  () => chapters.map((chapter) => ({
3332
3523
  id: chapter.id,
3333
3524
  title: chapter.title || messages.untitled,
@@ -3441,7 +3632,7 @@ function MejiroManuscriptEditor({
3441
3632
  onChange: (event) => patchChapter(selected, { title: event.target.value })
3442
3633
  }
3443
3634
  ),
3444
- /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
3635
+ dialect === "mejiro" && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
3445
3636
  /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u300A\u300A", "\u300B\u300B"), children: messages.manuscriptEmphasisDots }),
3446
3637
  /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u3014", "\u3015"), children: messages.manuscriptTcy }),
3447
3638
  /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("*", "*"), children: messages.manuscriptEm }),
@@ -3501,7 +3692,7 @@ function MejiroShelf({
3501
3692
  }
3502
3693
 
3503
3694
  // src/MejiroToc.tsx
3504
- import { useMemo as useMemo8, useState as useState13 } from "react";
3695
+ import { useMemo as useMemo9, useState as useState13 } from "react";
3505
3696
  import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3506
3697
  function buildEntries(epub, messages) {
3507
3698
  return epub.chapters.map((ch, i) => {
@@ -3525,8 +3716,8 @@ function MejiroToc({
3525
3716
  const resolvedTitle = title ?? messages.tocTitle;
3526
3717
  const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
3527
3718
  const [query, setQuery] = useState13("");
3528
- const entries = useMemo8(() => buildEntries(epub, messages), [epub, messages]);
3529
- const filtered = useMemo8(() => {
3719
+ const entries = useMemo9(() => buildEntries(epub, messages), [epub, messages]);
3720
+ const filtered = useMemo9(() => {
3530
3721
  if (!query) return entries;
3531
3722
  const needle = query.toLowerCase();
3532
3723
  return entries.filter(
@@ -3576,7 +3767,7 @@ import {
3576
3767
  serializeAnnotations,
3577
3768
  sortAnnotations
3578
3769
  } from "@libraz/mejiro";
3579
- import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef15, useState as useState14 } from "react";
3770
+ import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef16, useState as useState14 } from "react";
3580
3771
  function parseSorted(raw) {
3581
3772
  return sortAnnotations(parseAnnotations(raw));
3582
3773
  }
@@ -3586,14 +3777,14 @@ function useAnnotations(options) {
3586
3777
  const [annotations, setAnnotations] = useState14(
3587
3778
  () => readStorage(storage, key, parseSorted, [])
3588
3779
  );
3589
- const storageRef = useRef15(storage);
3780
+ const storageRef = useRef16(storage);
3590
3781
  storageRef.current = storage;
3591
- const annotationsRef = useRef15(annotations);
3782
+ const annotationsRef = useRef16(annotations);
3592
3783
  const applyNext = useCallback10((next) => {
3593
3784
  annotationsRef.current = next;
3594
3785
  setAnnotations(next);
3595
3786
  }, []);
3596
- const pendingRef = useRef15(null);
3787
+ const pendingRef = useRef16(null);
3597
3788
  if (!pendingRef.current) pendingRef.current = createPendingWrite();
3598
3789
  const pending = pendingRef.current;
3599
3790
  useEffect12(() => flushOnPageHide(pending.flush), [pending]);
@@ -3601,7 +3792,7 @@ function useAnnotations(options) {
3601
3792
  applyNext(readStorage(storageRef.current, key, parseSorted, []));
3602
3793
  return pending.flush;
3603
3794
  }, [key, applyNext, pending]);
3604
- const onChangeRef = useRef15(options.onChange);
3795
+ const onChangeRef = useRef16(options.onChange);
3605
3796
  onChangeRef.current = options.onChange;
3606
3797
  const commit = useCallback10(
3607
3798
  (next) => {
@@ -3669,7 +3860,7 @@ import {
3669
3860
  cloneEditableEpubBook as cloneEditableEpubBook2,
3670
3861
  EditableEpub as EditableEpub2
3671
3862
  } from "@libraz/mejiro/epub";
3672
- import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef16, useState as useState15 } from "react";
3863
+ import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo10, useRef as useRef17, useState as useState15 } from "react";
3673
3864
  function useEditableEpub(options = {}) {
3674
3865
  const [editor, setEditor] = useState15(null);
3675
3866
  const [loading, setLoading] = useState15(false);
@@ -3680,20 +3871,20 @@ function useEditableEpub(options = {}) {
3680
3871
  chapter: 0,
3681
3872
  paragraph: 0
3682
3873
  });
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);
3874
+ const requestIdRef = useRef17(0);
3875
+ const onLoadRef = useRef17(options.onLoad);
3876
+ const onErrorRef = useRef17(options.onError);
3877
+ const onExportRef = useRef17(options.onExport);
3878
+ const limitsRef = useRef17(options.limits);
3879
+ const assetResolverRef = useRef17(options.assetResolver);
3689
3880
  onLoadRef.current = options.onLoad;
3690
3881
  onErrorRef.current = options.onError;
3691
3882
  onExportRef.current = options.onExport;
3692
3883
  limitsRef.current = options.limits;
3693
3884
  assetResolverRef.current = options.assetResolver;
3694
- const editorRef = useRef16(editor);
3885
+ const editorRef = useRef17(editor);
3695
3886
  editorRef.current = editor;
3696
- const selectionRef = useRef16(selection);
3887
+ const selectionRef = useRef17(selection);
3697
3888
  selectionRef.current = selection;
3698
3889
  const loadBufferWithRequest = useCallback11(
3699
3890
  async (buffer, requestId) => {
@@ -3773,7 +3964,7 @@ function useEditableEpub(options = {}) {
3773
3964
  }, [options.defaultUrl, loadUrl]);
3774
3965
  const book = editor?.book ?? null;
3775
3966
  const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
3776
- const previewBook = useMemo9(() => {
3967
+ const previewBook = useMemo10(() => {
3777
3968
  void revision;
3778
3969
  return book ? cloneEditableEpubBook2(book) : null;
3779
3970
  }, [book, revision]);
@@ -3870,7 +4061,7 @@ import {
3870
4061
  EpubProject as EpubProject2,
3871
4062
  parseEpub as parseEpub2
3872
4063
  } from "@libraz/mejiro/epub";
3873
- import { useCallback as useCallback12, useEffect as useEffect14, useMemo as useMemo10, useRef as useRef17, useState as useState16 } from "react";
4064
+ import { useCallback as useCallback12, useEffect as useEffect14, useMemo as useMemo11, useRef as useRef18, useState as useState16 } from "react";
3874
4065
  function useEpubProject(options = {}) {
3875
4066
  const defaultTitle = options.defaultChapterTitle;
3876
4067
  const defaultBody = options.defaultChapterBody;
@@ -3886,22 +4077,22 @@ function useEpubProject(options = {}) {
3886
4077
  const [previewBook, setPreviewBook] = useState16(null);
3887
4078
  const [previewError, setPreviewError] = useState16(null);
3888
4079
  const [previewing, setPreviewing] = useState16(false);
3889
- const previewRequestIdRef = useRef17(0);
3890
- const onPreviewRef = useRef17(options.onPreview);
3891
- const onExportRef = useRef17(options.onExport);
4080
+ const previewRequestIdRef = useRef18(0);
4081
+ const onPreviewRef = useRef18(options.onPreview);
4082
+ const onExportRef = useRef18(options.onExport);
3892
4083
  onPreviewRef.current = options.onPreview;
3893
4084
  onExportRef.current = options.onExport;
3894
- const assetResolverRef = useRef17(options.assetResolver);
4085
+ const assetResolverRef = useRef18(options.assetResolver);
3895
4086
  assetResolverRef.current = options.assetResolver;
3896
- const metadataRef = useRef17(metadata);
4087
+ const metadataRef = useRef18(metadata);
3897
4088
  metadataRef.current = metadata;
3898
- const chaptersRef = useRef17(chapters);
4089
+ const chaptersRef = useRef18(chapters);
3899
4090
  chaptersRef.current = chapters;
3900
- const selectedRef = useRef17(selectedChapter);
4091
+ const selectedRef = useRef18(selectedChapter);
3901
4092
  selectedRef.current = selectedChapter;
3902
- const coverRef = useRef17(cover);
4093
+ const coverRef = useRef18(cover);
3903
4094
  coverRef.current = cover;
3904
- const assetsRef = useRef17(assets);
4095
+ const assetsRef = useRef18(assets);
3905
4096
  assetsRef.current = assets;
3906
4097
  const buildProject = useCallback12(() => {
3907
4098
  const currentCover = coverRef.current;
@@ -3956,7 +4147,7 @@ function useEpubProject(options = {}) {
3956
4147
  }, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
3957
4148
  const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
3958
4149
  const setMetadata = useCallback12((patch) => {
3959
- const next = { ...metadataRef.current, ...patch };
4150
+ const next = mergeMetadata(metadataRef.current, patch);
3960
4151
  metadataRef.current = next;
3961
4152
  setMetadataState(next);
3962
4153
  }, []);
@@ -4051,7 +4242,7 @@ function useEpubProject(options = {}) {
4051
4242
  onExportRef.current?.(buffer);
4052
4243
  return buffer;
4053
4244
  }, [buildProject, exportOptions]);
4054
- return useMemo10(
4245
+ return useMemo11(
4055
4246
  () => ({
4056
4247
  metadata,
4057
4248
  chapters,
@@ -4098,6 +4289,11 @@ function useEpubProject(options = {}) {
4098
4289
  ]
4099
4290
  );
4100
4291
  }
4292
+ function mergeMetadata(current, patch) {
4293
+ const next = { ...current, ...patch };
4294
+ if (!next.identifier?.trim()) next.identifier = current.identifier;
4295
+ return next;
4296
+ }
4101
4297
  function initialMetadata(patch = {}) {
4102
4298
  const metadata = mergeDefined(
4103
4299
  { title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
@@ -4116,7 +4312,7 @@ import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointer
4116
4312
  import {
4117
4313
  useCallback as useCallback13,
4118
4314
  useEffect as useEffect15,
4119
- useRef as useRef18,
4315
+ useRef as useRef19,
4120
4316
  useState as useState17
4121
4317
  } from "react";
4122
4318
  function useImageOverlay(layout, spreadIdx, onUpdate, options) {
@@ -4126,15 +4322,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4126
4322
  const defY = options?.defaultY ?? 100;
4127
4323
  const margin = options?.margin;
4128
4324
  const [imageRect, setImageRect] = useState17(null);
4129
- const rectRef = useRef18(imageRect);
4325
+ const rectRef = useRef19(imageRect);
4130
4326
  rectRef.current = imageRect;
4131
- const layoutRef = useRef18(layout);
4327
+ const layoutRef = useRef19(layout);
4132
4328
  layoutRef.current = layout;
4133
- const spreadRef = useRef18(spreadIdx);
4329
+ const spreadRef = useRef19(spreadIdx);
4134
4330
  spreadRef.current = spreadIdx;
4135
- const onUpdateRef = useRef18(onUpdate);
4331
+ const onUpdateRef = useRef19(onUpdate);
4136
4332
  onUpdateRef.current = onUpdate;
4137
- const activeDragCleanupsRef = useRef18(/* @__PURE__ */ new Set());
4333
+ const marginRef = useRef19(margin);
4334
+ marginRef.current = margin;
4335
+ const activeDragCleanupsRef = useRef19(/* @__PURE__ */ new Set());
4138
4336
  useEffect15(
4139
4337
  () => () => {
4140
4338
  for (const cleanup of activeDragCleanupsRef.current) cleanup();
@@ -4142,17 +4340,14 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4142
4340
  },
4143
4341
  []
4144
4342
  );
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 });
4343
+ const syncToLayout = useCallback13((rect) => {
4344
+ const lo = layoutRef.current;
4345
+ if (!lo) return;
4346
+ const images = rect ? [{ x: rect.x, y: rect.y, w: rect.w, h: rect.h, margin: marginRef.current }] : void 0;
4347
+ const spread = lo.syncImages(spreadRef.current, images);
4348
+ onUpdateRef.current(spread);
4349
+ }, []);
4350
+ const syncedRef = useRef19({ layout, spreadIdx, margin });
4156
4351
  useEffect15(() => {
4157
4352
  const prev = syncedRef.current;
4158
4353
  syncedRef.current = { layout, spreadIdx, margin };
@@ -4165,16 +4360,21 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4165
4360
  }, [layout, spreadIdx, margin, syncToLayout]);
4166
4361
  const toggleImage = useCallback13(() => {
4167
4362
  if (rectRef.current) {
4363
+ for (const cancel of [...activeDragCleanupsRef.current]) cancel();
4364
+ rectRef.current = null;
4168
4365
  setImageRect(null);
4169
4366
  syncToLayout(null);
4170
4367
  } else {
4171
4368
  const r = { x: defX, y: defY, w: defW, h: defH };
4369
+ rectRef.current = r;
4172
4370
  setImageRect(r);
4173
4371
  syncToLayout(r);
4174
4372
  }
4175
4373
  }, [defX, defY, defW, defH, syncToLayout]);
4176
4374
  const applyRect = useCallback13(
4177
4375
  (rect) => {
4376
+ if (rectRef.current === null) return;
4377
+ rectRef.current = rect;
4178
4378
  setImageRect(rect);
4179
4379
  syncToLayout(rect);
4180
4380
  },
@@ -4235,7 +4435,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
4235
4435
  }
4236
4436
 
4237
4437
  // src/useLibrary.ts
4238
- import { useCallback as useCallback14, useEffect as useEffect16, useMemo as useMemo11, useState as useState18 } from "react";
4438
+ import { useCallback as useCallback14, useEffect as useEffect16, useMemo as useMemo12, useState as useState18 } from "react";
4239
4439
  function resolveCurrentId(volumes, currentId, preferredId) {
4240
4440
  if (volumes.length === 0) return null;
4241
4441
  if (preferredId != null && volumes.some((v) => v.id === preferredId)) return preferredId;
@@ -4255,11 +4455,11 @@ function useLibrary(options) {
4255
4455
  const volume = volumes.find((v) => v.id === resolvedId);
4256
4456
  if (volume) onChange?.(volume);
4257
4457
  }, [currentId, resolvedId, volumes, onChange]);
4258
- const index = useMemo11(
4458
+ const index = useMemo12(
4259
4459
  () => resolvedId == null ? -1 : volumes.findIndex((v) => v.id === resolvedId),
4260
4460
  [resolvedId, volumes]
4261
4461
  );
4262
- const current = useMemo11(
4462
+ const current = useMemo12(
4263
4463
  () => index >= 0 && index < volumes.length ? volumes[index] : null,
4264
4464
  [index, volumes]
4265
4465
  );
@@ -4295,122 +4495,24 @@ function useLibrary(options) {
4295
4495
  }
4296
4496
 
4297
4497
  // 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";
4498
+ import { useMemo as useMemo13 } from "react";
4305
4499
  function useManuscriptLayout(book, chapter, surface, options = {}) {
4306
4500
  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;
4381
- }
4382
- };
4383
- const scheduleReflow = (immediate) => {
4384
- clear();
4385
- if (immediate) {
4386
- void recompute({ blank: false });
4387
- return;
4501
+ const source = useMemo13(() => {
4502
+ if (!chapter) return null;
4503
+ return {
4504
+ measure: (el) => book.computePageSize(el),
4505
+ layout: async () => {
4506
+ const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
4507
+ return layouts.values().next().value ?? null;
4388
4508
  }
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
4509
  };
4412
- }, [surface, enableResize, resizeDebounce, recompute]);
4413
- return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
4510
+ }, [book, chapter, dialect]);
4511
+ return useSurfaceLayout(source, surface, {
4512
+ ...options,
4513
+ enableResize: options.enableResize ?? true,
4514
+ resizeDebounce: options.resizeDebounce ?? 120
4515
+ });
4414
4516
  }
4415
4517
 
4416
4518
  // src/useReadingPosition.ts
@@ -4418,11 +4520,11 @@ import {
4418
4520
  parseReadingPosition,
4419
4521
  serializeReadingPosition
4420
4522
  } from "@libraz/mejiro";
4421
- import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef20, useState as useState20 } from "react";
4523
+ import { useCallback as useCallback15, useEffect as useEffect17, useRef as useRef20, useState as useState19 } from "react";
4422
4524
  function useReadingPosition(options) {
4423
4525
  const { key, throttleMs = 250 } = options;
4424
4526
  const storage = options.storage ?? resolveDefaultStorage();
4425
- const [position, setPosition] = useState20(
4527
+ const [position, setPosition] = useState19(
4426
4528
  () => readStorage(storage, key, parseReadingPosition, null)
4427
4529
  );
4428
4530
  const storageRef = useRef20(storage);
@@ -4430,14 +4532,14 @@ function useReadingPosition(options) {
4430
4532
  const pendingRef = useRef20(null);
4431
4533
  if (!pendingRef.current) pendingRef.current = createPendingWrite();
4432
4534
  const pending = pendingRef.current;
4433
- useEffect18(() => flushOnPageHide(pending.flush), [pending]);
4434
- useEffect18(() => {
4535
+ useEffect17(() => flushOnPageHide(pending.flush), [pending]);
4536
+ useEffect17(() => {
4435
4537
  setPosition(readStorage(storageRef.current, key, parseReadingPosition, null));
4436
4538
  return pending.flush;
4437
4539
  }, [key, pending]);
4438
4540
  const onChangeRef = useRef20(options.onChange);
4439
4541
  onChangeRef.current = options.onChange;
4440
- const save = useCallback16(
4542
+ const save = useCallback15(
4441
4543
  (next) => {
4442
4544
  setPosition(next);
4443
4545
  const currentStorage = storageRef.current;
@@ -4450,7 +4552,7 @@ function useReadingPosition(options) {
4450
4552
  },
4451
4553
  [key, throttleMs, pending]
4452
4554
  );
4453
- const clear = useCallback16(() => {
4555
+ const clear = useCallback15(() => {
4454
4556
  setPosition(null);
4455
4557
  pending.cancel();
4456
4558
  removeStorage(storageRef.current, key);