@libraz/mejiro-react 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +313 -123
  2. package/dist/index.js +1556 -1201
  3. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -61,7 +61,9 @@ function MejiroChapterNav({
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
63
  const chapterKey = `${title}-${ch.paragraphs.map((p) => p.text).join("|").slice(0, 120)}`;
64
- const headings = ch.paragraphs.filter((p) => p.headingLevel && p.text.trim() && p.text.trim() !== title).slice(0, 3);
64
+ const headings = ch.paragraphs.flatMap(
65
+ (p, paragraph) => p.headingLevel && p.text.trim() && p.text.trim() !== title ? [{ paragraph, text: p.text }] : []
66
+ ).slice(0, 3);
65
67
  return /* @__PURE__ */ jsx2("li", { className: "mejiro-reader-chapter-list-item", children: /* @__PURE__ */ jsxs(
66
68
  "button",
67
69
  {
@@ -74,7 +76,7 @@ function MejiroChapterNav({
74
76
  /* @__PURE__ */ jsxs("span", { className: "mejiro-reader-chapter-main", children: [
75
77
  /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-title", children: title }),
76
78
  preview && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-preview", children: textPreview(preview) }),
77
- headings.length > 0 && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-subheads", children: headings.map((heading) => /* @__PURE__ */ jsx2("span", { children: textPreview(heading.text, 30) }, heading.text)) })
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)) })
78
80
  ] })
79
81
  ]
80
82
  }
@@ -91,6 +93,17 @@ function MejiroChapterNav({
91
93
  // src/MejiroDropZone.tsx
92
94
  import { useRef, useState } from "react";
93
95
  import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
96
+ function matchesAccept(accept, file) {
97
+ const tokens = accept.split(",").map((token) => token.trim().toLowerCase()).filter(Boolean);
98
+ if (tokens.length === 0) return true;
99
+ const name = file.name.toLowerCase();
100
+ const type = file.type.toLowerCase();
101
+ return tokens.some((token) => {
102
+ if (token.startsWith(".")) return name.endsWith(token);
103
+ if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
104
+ return type === token;
105
+ });
106
+ }
94
107
  function MejiroDropZone({
95
108
  accept = ".epub",
96
109
  validateFile,
@@ -104,7 +117,7 @@ function MejiroDropZone({
104
117
  const [dragover, setDragover] = useState(false);
105
118
  const isValid = (file) => {
106
119
  if (validateFile) return validateFile(file);
107
- return file.name.endsWith(".epub");
120
+ return matchesAccept(accept, file);
108
121
  };
109
122
  const rootClass = ["mejiro-reader-drop-zone", dragover ? "is-dragover" : "", className ?? ""].filter(Boolean).join(" ");
110
123
  return /* @__PURE__ */ jsxs2(
@@ -160,7 +173,30 @@ import {
160
173
  EditableEpub,
161
174
  exportEditableEpub
162
175
  } from "@libraz/mejiro/epub";
163
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef10, useState as useState8 } from "react";
176
+ import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef11, useState as useState9 } from "react";
177
+
178
+ // src/errors.ts
179
+ import { assertEpubInputSize } from "@libraz/mejiro/epub";
180
+ function toError(cause) {
181
+ return cause instanceof Error ? cause : new Error(String(cause));
182
+ }
183
+ async function fetchEpubBuffer(url, init) {
184
+ const res = init ? await fetch(url, init) : await fetch(url);
185
+ if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
186
+ return res.arrayBuffer();
187
+ }
188
+ async function readEpubFile(file, limits) {
189
+ assertEpubInputSize(file.size, limits);
190
+ return file.arrayBuffer();
191
+ }
192
+ async function withErrorReporting(task, report) {
193
+ try {
194
+ return await task();
195
+ } catch (cause) {
196
+ report(toError(cause));
197
+ return void 0;
198
+ }
199
+ }
164
200
 
165
201
  // src/MejiroReader.tsx
166
202
  import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
@@ -169,12 +205,12 @@ import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
169
205
  import {
170
206
  forwardRef,
171
207
  useCallback as useCallback6,
172
- useEffect as useEffect7,
208
+ useEffect as useEffect8,
173
209
  useImperativeHandle,
174
210
  useLayoutEffect as useLayoutEffect4,
175
211
  useMemo as useMemo4,
176
- useRef as useRef9,
177
- useState as useState7
212
+ useRef as useRef10,
213
+ useState as useState8
178
214
  } from "react";
179
215
 
180
216
  // src/MejiroPageIndicator.tsx
@@ -190,14 +226,59 @@ function MejiroPageIndicator({ current, total }) {
190
226
  // src/MejiroScrollView.tsx
191
227
  import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
192
228
  import {
193
- useEffect,
229
+ useEffect as useEffect2,
194
230
  useLayoutEffect,
195
- useMemo as useMemo2,
196
- useRef as useRef2
231
+ useMemo as useMemo3,
232
+ useRef as useRef3
197
233
  } from "react";
198
234
 
235
+ // src/MejiroImageOverlay.tsx
236
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
237
+ function MejiroImageOverlay({
238
+ rect,
239
+ label,
240
+ onOverlayPointerDown,
241
+ onResizePointerDown,
242
+ onClose
243
+ }) {
244
+ const messages = useI18n();
245
+ const resolvedLabel = label ?? messages.imageButton;
246
+ return /* @__PURE__ */ jsxs4(
247
+ "div",
248
+ {
249
+ className: "mejiro-reader-image-overlay",
250
+ style: {
251
+ left: rect.x,
252
+ top: rect.y,
253
+ width: rect.w,
254
+ height: rect.h
255
+ },
256
+ onPointerDown: onOverlayPointerDown,
257
+ children: [
258
+ /* @__PURE__ */ jsxs4("div", { className: "mejiro-reader-image-overlay-label", children: [
259
+ /* @__PURE__ */ jsx4("div", { className: "mejiro-reader-image-overlay-icon" }),
260
+ /* @__PURE__ */ jsx4("span", { children: resolvedLabel })
261
+ ] }),
262
+ /* @__PURE__ */ jsx4("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
263
+ /* @__PURE__ */ jsx4(
264
+ "button",
265
+ {
266
+ type: "button",
267
+ "aria-label": messages.imageRemoveButton,
268
+ title: messages.imageRemoveButton,
269
+ className: "mejiro-reader-image-overlay-close",
270
+ onPointerDown: (e) => e.stopPropagation(),
271
+ onClick: () => onClose?.()
272
+ }
273
+ )
274
+ ]
275
+ }
276
+ );
277
+ }
278
+
199
279
  // src/MejiroPageView.tsx
200
280
  import { normalizeFontFamily } from "@libraz/mejiro/browser";
281
+ import { paragraphClassName as paragraphClassName2 } from "@libraz/mejiro/render";
201
282
 
202
283
  // src/MejiroPage.tsx
203
284
  import { paragraphClassName } from "@libraz/mejiro/render";
@@ -207,12 +288,12 @@ import {
207
288
  segmentToInlineNode
208
289
  } from "@libraz/mejiro/render";
209
290
  import { createElement, Fragment as Fragment2 } from "react";
210
- import { jsx as jsx4 } from "react/jsx-runtime";
291
+ import { jsx as jsx5 } from "react/jsx-runtime";
211
292
  function renderSegment(segment, key) {
212
293
  return renderInlineNode(segmentToInlineNode(segment), key);
213
294
  }
214
295
  function renderInlineNode(node, key) {
215
- if (node.type === "text") return /* @__PURE__ */ jsx4(Fragment2, { children: node.text }, key);
296
+ if (node.type === "text") return /* @__PURE__ */ jsx5(Fragment2, { children: node.text }, key);
216
297
  return createElement(
217
298
  node.tag,
218
299
  {
@@ -226,33 +307,36 @@ function renderInlineNode(node, key) {
226
307
  }
227
308
 
228
309
  // src/MejiroPage.tsx
229
- import { jsx as jsx5 } from "react/jsx-runtime";
310
+ import { jsx as jsx6 } from "react/jsx-runtime";
230
311
  function renderLine(line, lineIndex) {
231
312
  const nodes = [];
232
313
  if (lineIndex > 0) {
233
- nodes.push(/* @__PURE__ */ jsx5("br", {}, `br-${lineIndex}`));
314
+ nodes.push(/* @__PURE__ */ jsx6("br", {}, `br-${lineIndex}`));
234
315
  }
235
316
  for (let i = 0; i < line.segments.length; i++) {
236
317
  nodes.push(renderSegment(line.segments[i], `${lineIndex}-${i}`));
237
318
  }
238
319
  return nodes;
239
320
  }
321
+ function paragraphScaleStyle(scale) {
322
+ return scale == null ? void 0 : { "--mejiro-paragraph-scale": scale };
323
+ }
240
324
  function MejiroPage({ page, className, style }) {
241
325
  const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
242
- return /* @__PURE__ */ jsx5("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
326
+ return /* @__PURE__ */ jsx6("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
243
327
  const paraClass = paragraphClassName(
244
328
  paragraph.kind ?? (paragraph.isHeading ? "heading" : void 0),
245
329
  paragraph.headingLevel
246
330
  );
247
331
  return (
248
332
  // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
249
- /* @__PURE__ */ jsx5("div", { className: paraClass, children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
333
+ /* @__PURE__ */ jsx6("div", { className: paraClass, style: paragraphScaleStyle(paragraph.scale), children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
250
334
  );
251
335
  }) });
252
336
  }
253
337
 
254
338
  // src/MejiroPageView.tsx
255
- import { jsx as jsx6 } from "react/jsx-runtime";
339
+ import { jsx as jsx7 } from "react/jsx-runtime";
256
340
  function MejiroPageView({
257
341
  result,
258
342
  fontFamily,
@@ -264,33 +348,307 @@ function MejiroPageView({
264
348
  if (result.hasImages || slotMode) {
265
349
  const rootClass = className ? `mejiro-page-slots ${className}` : "mejiro-page-slots";
266
350
  const fontFamilyCss = fontFamily != null ? normalizeFontFamily(fontFamily) : void 0;
267
- return /* @__PURE__ */ jsx6("div", { className: rootClass, style: { position: "relative", ...style }, children: result.lines.map((line, i) => {
268
- const slot = result.slots[i];
269
- if (!slot || slot.height <= 0) return null;
270
- const colStyle = {
351
+ return /* @__PURE__ */ jsx7(
352
+ "div",
353
+ {
354
+ className: rootClass,
355
+ style: { position: "relative", fontFamily: fontFamilyCss, ...style },
356
+ children: result.lines.map((line, i) => {
357
+ const slot = result.slots[i];
358
+ if (!slot || slot.height <= 0) return null;
359
+ const colStyle = {
360
+ position: "absolute",
361
+ writingMode: "vertical-rl",
362
+ overflow: "hidden",
363
+ margin: 0,
364
+ right: slot.xPos,
365
+ top: slot.yStart,
366
+ height: slot.height,
367
+ fontSize: line.fontSize,
368
+ lineHeight: lineSpacing
369
+ };
370
+ return /* @__PURE__ */ jsx7(
371
+ "div",
372
+ {
373
+ className: paragraphClassName2(line.kind, line.headingLevel),
374
+ style: colStyle,
375
+ children: line.segments.map((seg, si) => renderSegment(seg, `${i}-${si}`))
376
+ },
377
+ i
378
+ );
379
+ })
380
+ }
381
+ );
382
+ }
383
+ return /* @__PURE__ */ jsx7(MejiroPage, { page: result.page, className, style });
384
+ }
385
+
386
+ // src/MejiroSelectionLayer.tsx
387
+ import { jsx as jsx8 } from "react/jsx-runtime";
388
+ function MejiroSelectionLayer({
389
+ rects,
390
+ side,
391
+ className,
392
+ style,
393
+ rectClassName,
394
+ rectStyle
395
+ }) {
396
+ const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
397
+ const rectClass = rectClassName ?? "mejiro-selection-rect";
398
+ return /* @__PURE__ */ jsx8(
399
+ "div",
400
+ {
401
+ className: wrapperClass,
402
+ style: {
271
403
  position: "absolute",
272
- writingMode: "vertical-rl",
273
- whiteSpace: "nowrap",
274
- overflow: "hidden",
275
- right: slot.xPos,
276
- top: slot.yStart,
277
- height: slot.height,
278
- fontSize: line.fontSize,
279
- fontFamily: fontFamilyCss,
280
- lineHeight: lineSpacing,
281
- fontWeight: line.headingLevel != null ? 700 : void 0
404
+ inset: 0,
405
+ pointerEvents: "none",
406
+ ...style
407
+ },
408
+ children: rects.map((r, i) => {
409
+ if (r.side !== side) return null;
410
+ const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
411
+ return /* @__PURE__ */ jsx8(
412
+ "div",
413
+ {
414
+ className: rectClass,
415
+ style: {
416
+ position: "absolute",
417
+ width: r.width,
418
+ height: r.height,
419
+ ...posStyle,
420
+ ...r.color != null ? { backgroundColor: r.color } : null,
421
+ ...rectStyle
422
+ }
423
+ },
424
+ i
425
+ );
426
+ })
427
+ }
428
+ );
429
+ }
430
+
431
+ // src/useMultiImageOverlay.ts
432
+ import { createOverlayDragSession, isPrimaryPointerPress } from "@libraz/mejiro/browser";
433
+ import {
434
+ useCallback,
435
+ useEffect,
436
+ useMemo as useMemo2,
437
+ useRef as useRef2,
438
+ useState as useState2
439
+ } from "react";
440
+ var nextId = 0;
441
+ function applyImages(layout, images, spreads, margin) {
442
+ for (const si of spreads) {
443
+ const bookImages = (images.get(si) ?? []).map((it) => ({
444
+ x: it.rect.x,
445
+ y: it.rect.y,
446
+ w: it.rect.w,
447
+ h: it.rect.h,
448
+ margin
449
+ }));
450
+ layout.setImages(si, bookImages);
451
+ }
452
+ }
453
+ function overlayPageSide(rect) {
454
+ return rect.x + rect.w / 2 < 0 ? "left" : "right";
455
+ }
456
+ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
457
+ const defW = options.defaultWidth ?? 120;
458
+ const defH = options.defaultHeight ?? 160;
459
+ const defX = options.defaultX ?? 80;
460
+ const defY = options.defaultY ?? 100;
461
+ const margin = options.margin;
462
+ const [images, setImages] = useState2(() => /* @__PURE__ */ new Map());
463
+ const imagesRef = useRef2(images);
464
+ imagesRef.current = images;
465
+ const layoutRef = useRef2(null);
466
+ layoutRef.current = layout;
467
+ const spreadIdxRef = useRef2(spreadIdx);
468
+ spreadIdxRef.current = spreadIdx;
469
+ const onUpdateRef = useRef2(options.onUpdate);
470
+ onUpdateRef.current = options.onUpdate;
471
+ const activeDragCleanupsRef = useRef2(/* @__PURE__ */ new Set());
472
+ useEffect(
473
+ () => () => {
474
+ for (const cleanup of activeDragCleanupsRef.current) cleanup();
475
+ activeDragCleanupsRef.current.clear();
476
+ },
477
+ []
478
+ );
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
+ );
488
+ const appliedRef = useRef2({
489
+ layout: null,
490
+ margin
491
+ });
492
+ if (layout && (appliedRef.current.layout !== layout || appliedRef.current.margin !== margin)) {
493
+ appliedRef.current = { layout, margin };
494
+ applyImages(layout, imagesRef.current, [...imagesRef.current.keys()], margin);
495
+ }
496
+ const commit = useCallback(
497
+ (next, affectedSpread) => {
498
+ imagesRef.current = next;
499
+ setImages(next);
500
+ syncSpreads([affectedSpread]);
501
+ },
502
+ [syncSpreads]
503
+ );
504
+ const findById = useCallback(
505
+ (id) => {
506
+ for (const [s, list] of imagesRef.current) {
507
+ const i = list.findIndex((it) => it.id === id);
508
+ if (i >= 0) return { spread: s, index: i, item: list[i] };
509
+ }
510
+ return null;
511
+ },
512
+ []
513
+ );
514
+ const addImage = useCallback(
515
+ (overrides) => {
516
+ const si = spreadIdxRef.current;
517
+ const list = imagesRef.current.get(si) ?? [];
518
+ const last = list[list.length - 1];
519
+ const baseX = last ? last.rect.x - 100 : defX;
520
+ const baseY = last ? last.rect.y + 40 : defY;
521
+ nextId += 1;
522
+ const item = {
523
+ id: `mejiro-img-${nextId}`,
524
+ rect: {
525
+ x: overrides?.x ?? baseX,
526
+ y: overrides?.y ?? baseY,
527
+ w: overrides?.w ?? defW,
528
+ h: overrides?.h ?? defH
529
+ }
282
530
  };
283
- return (
284
- // biome-ignore lint/suspicious/noArrayIndexKey: lines have no stable ID
285
- /* @__PURE__ */ jsx6("div", { style: colStyle, children: line.segments.map((seg, si) => renderSegment(seg, `${i}-${si}`)) }, i)
531
+ const next = new Map(imagesRef.current);
532
+ next.set(si, [...list, item]);
533
+ commit(next, si);
534
+ return item;
535
+ },
536
+ [defX, defY, defW, defH, commit]
537
+ );
538
+ const removeImage = useCallback(
539
+ (id) => {
540
+ const found = findById(id);
541
+ if (!found) return;
542
+ const next = new Map(imagesRef.current);
543
+ const list = (next.get(found.spread) ?? []).filter((it) => it.id !== id);
544
+ if (list.length === 0) next.delete(found.spread);
545
+ else next.set(found.spread, list);
546
+ commit(next, found.spread);
547
+ },
548
+ [findById, commit]
549
+ );
550
+ const updateImage = useCallback(
551
+ (id, rect) => {
552
+ const found = findById(id);
553
+ if (!found) return;
554
+ const next = new Map(imagesRef.current);
555
+ const list = (next.get(found.spread) ?? []).map(
556
+ (it) => it.id === id ? { ...it, rect: { ...it.rect, ...rect } } : it
286
557
  );
287
- }) });
288
- }
289
- return /* @__PURE__ */ jsx6(MejiroPage, { page: result.page, className, style });
558
+ next.set(found.spread, list);
559
+ commit(next, found.spread);
560
+ },
561
+ [findById, commit]
562
+ );
563
+ const clearImages = useCallback(
564
+ (target) => {
565
+ if (target == null) {
566
+ const old = imagesRef.current;
567
+ imagesRef.current = /* @__PURE__ */ new Map();
568
+ setImages(/* @__PURE__ */ new Map());
569
+ layoutRef.current?.clearImages();
570
+ if (layoutRef.current) {
571
+ for (const si of old.keys()) onUpdateRef.current?.(layoutRef.current.getSpread(si));
572
+ }
573
+ } else {
574
+ const next = new Map(imagesRef.current);
575
+ next.delete(target);
576
+ commit(next, target);
577
+ }
578
+ },
579
+ [commit]
580
+ );
581
+ const onOverlayPointerDown = useCallback(
582
+ (id, e) => {
583
+ if (!isPrimaryPointerPress(e)) return;
584
+ e.preventDefault();
585
+ const found = findById(id);
586
+ if (!found) return;
587
+ const target = e.currentTarget;
588
+ createOverlayDragSession({
589
+ mode: "move",
590
+ rect: found.item.rect,
591
+ startX: e.clientX,
592
+ startY: e.clientY,
593
+ pointerId: e.nativeEvent.pointerId,
594
+ captureElement: target,
595
+ activeElement: target,
596
+ dragClass: "is-dragging",
597
+ registry: activeDragCleanupsRef.current,
598
+ onChange: (rect) => updateImage(id, rect)
599
+ });
600
+ },
601
+ [findById, updateImage]
602
+ );
603
+ const onResizePointerDown = useCallback(
604
+ (id, e) => {
605
+ if (!isPrimaryPointerPress(e)) return;
606
+ e.preventDefault();
607
+ e.stopPropagation();
608
+ const found = findById(id);
609
+ if (!found) return;
610
+ const target = e.currentTarget;
611
+ createOverlayDragSession({
612
+ mode: "resize",
613
+ rect: found.item.rect,
614
+ startX: e.clientX,
615
+ startY: e.clientY,
616
+ pointerId: e.nativeEvent.pointerId,
617
+ captureElement: target,
618
+ activeElement: target.parentElement,
619
+ dragClass: "is-dragging",
620
+ registry: activeDragCleanupsRef.current,
621
+ onChange: (rect) => updateImage(id, rect)
622
+ });
623
+ },
624
+ [findById, updateImage]
625
+ );
626
+ useEffect(() => {
627
+ if (!layout) return;
628
+ for (const si of imagesRef.current.keys()) onUpdateRef.current?.(layout.getSpread(si));
629
+ }, [layout, margin]);
630
+ const imagesBySpread = useMemo2(() => new Map(images), [images, margin]);
631
+ const currentImages = useMemo2(
632
+ () => [...imagesBySpread.get(spreadIdx) ?? []],
633
+ [imagesBySpread, spreadIdx]
634
+ );
635
+ const hasImages = useMemo2(() => [...images.values()].some((list) => list.length > 0), [images]);
636
+ return {
637
+ imagesBySpread,
638
+ currentImages,
639
+ hasImages,
640
+ addImage,
641
+ removeImage,
642
+ updateImage,
643
+ clearImages,
644
+ onOverlayPointerDown,
645
+ onResizePointerDown
646
+ };
290
647
  }
291
648
 
292
649
  // src/MejiroScrollView.tsx
293
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
650
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
651
+ var NO_IMAGES = [];
294
652
  function MejiroScrollView({
295
653
  layout,
296
654
  pageWidth,
@@ -302,23 +660,49 @@ function MejiroScrollView({
302
660
  slotMode,
303
661
  onVisiblePageChange,
304
662
  scrollToPage,
305
- pageGap = 24
663
+ pageGap = 24,
664
+ selectionRects,
665
+ spreadIdx,
666
+ images = NO_IMAGES,
667
+ onImagePointerDown,
668
+ onImageResizePointerDown,
669
+ onImageClose
306
670
  }) {
307
- const containerRef = useRef2(null);
308
- const pageRefs = useRef2([]);
309
- const onVisiblePageChangeRef = useRef2(onVisiblePageChange);
671
+ const containerRef = useRef3(null);
672
+ const pageRefs = useRef3([]);
673
+ const onVisiblePageChangeRef = useRef3(onVisiblePageChange);
310
674
  onVisiblePageChangeRef.current = onVisiblePageChange;
311
- const programmaticScrollRef = useRef2(false);
312
- const pages = useMemo2(() => {
675
+ const programmaticScrollRef = useRef3(false);
676
+ const userPageRef = useRef3(null);
677
+ const rightPage = spreadIdx != null ? spreadIdx * 2 : -1;
678
+ const leftPage = spreadIdx != null ? spreadIdx * 2 + 1 : -1;
679
+ const rightImages = useMemo3(
680
+ () => images.filter((item) => overlayPageSide(item.rect) === "right"),
681
+ [images]
682
+ );
683
+ const leftImages = useMemo3(
684
+ () => images.filter((item) => overlayPageSide(item.rect) === "left"),
685
+ [images]
686
+ );
687
+ const pages = useMemo3(() => {
313
688
  const total = layout.totalPages;
314
689
  return Array.from({ length: total }, (_, i) => layout.getPage(i));
315
- }, [layout]);
690
+ }, [layout, images]);
316
691
  const pageCount = pages.length;
692
+ const rectsByPage = useMemo3(() => {
693
+ const byPage = [];
694
+ for (const rect of selectionRects ?? []) {
695
+ const list = byPage[rect.pageIdx] ?? [];
696
+ list.push(rect);
697
+ byPage[rect.pageIdx] = list;
698
+ }
699
+ return byPage;
700
+ }, [selectionRects]);
317
701
  const contentStyle = { height: contentHeight };
318
702
  if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
319
703
  if (fontSize != null) contentStyle.fontSize = fontSize;
320
704
  if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
321
- useEffect(() => {
705
+ useEffect2(() => {
322
706
  const container = containerRef.current;
323
707
  if (!container) return;
324
708
  if (pageCount === 0) return;
@@ -341,10 +725,9 @@ function MejiroScrollView({
341
725
  }
342
726
  }
343
727
  if (mostVisibleIdx >= 0) {
344
- onVisiblePageChangeRef.current?.(
345
- mostVisibleIdx,
346
- programmaticScrollRef.current ? "programmatic" : "user"
347
- );
728
+ const source = programmaticScrollRef.current ? "programmatic" : "user";
729
+ if (source === "user") userPageRef.current = mostVisibleIdx;
730
+ onVisiblePageChangeRef.current?.(mostVisibleIdx, source);
348
731
  }
349
732
  },
350
733
  { root: container, threshold: [0.25, 0.5, 0.75] }
@@ -357,8 +740,10 @@ function MejiroScrollView({
357
740
  useLayoutEffect(() => {
358
741
  if (scrollToPage == null) return;
359
742
  if (pageCount === 0) return;
743
+ if (scrollToPage === userPageRef.current) return;
360
744
  const el = pageRefs.current[scrollToPage];
361
745
  if (!(el && containerRef.current)) return;
746
+ userPageRef.current = null;
362
747
  programmaticScrollRef.current = true;
363
748
  containerRef.current.scrollTo({
364
749
  top: el.offsetTop,
@@ -369,12 +754,12 @@ function MejiroScrollView({
369
754
  }, 0);
370
755
  return () => clearTimeout(timer);
371
756
  }, [scrollToPage, pageCount]);
372
- return /* @__PURE__ */ jsx7("div", { ref: containerRef, className: "mejiro-reader-scroll", children: /* @__PURE__ */ jsx7(
757
+ return /* @__PURE__ */ jsx9("div", { ref: containerRef, className: "mejiro-reader-scroll", children: /* @__PURE__ */ jsx9(
373
758
  "div",
374
759
  {
375
760
  className: "mejiro-reader-scroll-track",
376
761
  style: { display: "flex", flexDirection: "column", gap: pageGap },
377
- children: pages.map((result, i) => /* @__PURE__ */ jsxs4(
762
+ children: pages.map((result, i) => /* @__PURE__ */ jsxs5(
378
763
  "div",
379
764
  {
380
765
  ref: (el) => {
@@ -382,24 +767,58 @@ function MejiroScrollView({
382
767
  },
383
768
  "data-page-idx": i,
384
769
  className: "mejiro-reader-page",
385
- style: { width: pageWidth, height: pageHeight, flexShrink: 0 },
770
+ style: {
771
+ width: pageWidth,
772
+ height: pageHeight,
773
+ flexShrink: 0,
774
+ ...i === rightPage && rightImages.length > 0 || i === leftPage && leftImages.length > 0 ? { overflow: "visible" } : null
775
+ },
386
776
  children: [
387
- /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-page-rule" }),
388
- /* @__PURE__ */ jsxs4("div", { className: "mejiro-reader-page-header", children: [
389
- /* @__PURE__ */ jsx7("span", { className: "mejiro-reader-page-header-title" }),
390
- /* @__PURE__ */ jsx7("span", { className: "mejiro-reader-page-header-num", children: i + 1 })
777
+ /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-rule" }),
778
+ /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-header", children: [
779
+ /* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-title" }),
780
+ /* @__PURE__ */ jsx9("span", { className: "mejiro-reader-page-header-num", children: i + 1 })
391
781
  ] }),
392
- /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: /* @__PURE__ */ jsx7(
393
- MejiroPageView,
782
+ /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
783
+ /* @__PURE__ */ jsx9(
784
+ MejiroPageView,
785
+ {
786
+ result,
787
+ slotMode,
788
+ fontFamily,
789
+ lineSpacing,
790
+ className: "mejiro-reader-page-content",
791
+ style: contentStyle
792
+ }
793
+ ),
794
+ rectsByPage[i] && /* @__PURE__ */ jsx9(
795
+ MejiroSelectionLayer,
796
+ {
797
+ rects: rectsByPage[i],
798
+ side: i % 2 === 0 ? "right" : "left"
799
+ }
800
+ )
801
+ ] }) }),
802
+ i === rightPage && rightImages.map((item) => /* @__PURE__ */ jsx9(
803
+ MejiroImageOverlay,
394
804
  {
395
- result,
396
- slotMode,
397
- fontFamily,
398
- lineSpacing,
399
- className: "mejiro-reader-page-content",
400
- style: contentStyle
401
- }
402
- ) }) })
805
+ rect: item.rect,
806
+ onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
807
+ onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
808
+ onClose: () => onImageClose?.(item.id)
809
+ },
810
+ item.id
811
+ )),
812
+ i === leftPage && leftImages.map((item) => /* @__PURE__ */ jsx9(
813
+ MejiroImageOverlay,
814
+ {
815
+ rect: { ...item.rect, x: item.rect.x + pageWidth },
816
+ onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
817
+ onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
818
+ onClose: () => onImageClose?.(item.id)
819
+ },
820
+ item.id
821
+ ))
403
822
  ]
404
823
  },
405
824
  i
@@ -410,7 +829,8 @@ function MejiroScrollView({
410
829
 
411
830
  // src/MejiroSettingsPanel.tsx
412
831
  import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
413
- import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
832
+ import { useEffect as useEffect3, useRef as useRef4, useState as useState3 } from "react";
833
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
414
834
  var DEFAULT_FONTS = [
415
835
  { value: "serif", label: "System Serif" },
416
836
  { value: "sans-serif", label: "System Sans" }
@@ -427,6 +847,48 @@ function parseClampedInput(value, min, max, fallback) {
427
847
  if (value.trim() === "") return fallback;
428
848
  return clampNumber(Number(value), min, max, fallback);
429
849
  }
850
+ function CommittedNumberInput({
851
+ id,
852
+ className,
853
+ value,
854
+ min,
855
+ max,
856
+ step,
857
+ onCommit
858
+ }) {
859
+ const [draft, setDraft] = useState3(String(value));
860
+ const [input, setInput] = useState3(null);
861
+ const latest = useRef4({ value, min, max, onCommit });
862
+ latest.current = { value, min, max, onCommit };
863
+ useEffect3(() => {
864
+ setDraft(String(value));
865
+ }, [value]);
866
+ useEffect3(() => {
867
+ if (!input) return;
868
+ const commit = () => {
869
+ const { value: current, min: lo, max: hi, onCommit: emit } = latest.current;
870
+ const next = parseClampedInput(input.value, lo, hi, current);
871
+ setDraft(String(next));
872
+ emit(next);
873
+ };
874
+ input.addEventListener("change", commit);
875
+ return () => input.removeEventListener("change", commit);
876
+ }, [input]);
877
+ return /* @__PURE__ */ jsx10(
878
+ "input",
879
+ {
880
+ ref: setInput,
881
+ id,
882
+ className,
883
+ type: "number",
884
+ value: draft,
885
+ min,
886
+ max,
887
+ step,
888
+ onChange: (e) => setDraft(e.target.value)
889
+ }
890
+ );
891
+ }
430
892
  function MejiroSettingsPanel({
431
893
  open,
432
894
  settings,
@@ -439,13 +901,13 @@ function MejiroSettingsPanel({
439
901
  const patch = (next) => onChange({ ...settings, ...next });
440
902
  const currentFont = normalizeFontFamily3(settings.fontFamily);
441
903
  const fontOptions = fonts.some((f) => f.value === currentFont) ? fonts : [{ value: currentFont, label: fontLabelFromCss(currentFont) }, ...fonts];
442
- return /* @__PURE__ */ jsx8("div", { className: `mejiro-reader-settings-panel${open ? " is-open" : ""}`, children: /* @__PURE__ */ jsx8("div", { className: "mejiro-reader-settings-inner", children: /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-settings-content", children: [
443
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-settings-group", children: [
444
- /* @__PURE__ */ jsx8("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
445
- /* @__PURE__ */ jsx8("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx8("select", { value: currentFont, onChange: (e) => patch({ fontFamily: e.target.value }), children: fontOptions.map((f) => /* @__PURE__ */ jsx8("option", { value: f.value, children: f.label }, f.value)) }) }),
446
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-control", children: [
447
- /* @__PURE__ */ jsx8("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-font-size", children: messages.settingsSize }),
448
- /* @__PURE__ */ jsx8(
904
+ return /* @__PURE__ */ jsx10("div", { className: `mejiro-reader-settings-panel${open ? " is-open" : ""}`, children: /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-settings-inner", children: /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-content", children: [
905
+ /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
906
+ /* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
907
+ /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx10("select", { value: currentFont, onChange: (e) => patch({ fontFamily: e.target.value }), children: fontOptions.map((f) => /* @__PURE__ */ jsx10("option", { value: f.value, children: f.label }, f.value)) }) }),
908
+ /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
909
+ /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-font-size", children: messages.settingsSize }),
910
+ /* @__PURE__ */ jsx10(
449
911
  "button",
450
912
  {
451
913
  type: "button",
@@ -462,25 +924,17 @@ function MejiroSettingsPanel({
462
924
  children: "A\u2212"
463
925
  }
464
926
  ),
465
- /* @__PURE__ */ jsx8(
466
- "input",
927
+ /* @__PURE__ */ jsx10(
928
+ CommittedNumberInput,
467
929
  {
468
930
  id: "mejiro-reader-font-size",
469
- type: "number",
470
931
  value: settings.fontSize,
471
932
  min: minFontSize,
472
933
  max: maxFontSize,
473
- onChange: (e) => patch({
474
- fontSize: parseClampedInput(
475
- e.target.value,
476
- minFontSize,
477
- maxFontSize,
478
- settings.fontSize
479
- )
480
- })
934
+ onCommit: (fontSize) => patch({ fontSize })
481
935
  }
482
936
  ),
483
- /* @__PURE__ */ jsx8(
937
+ /* @__PURE__ */ jsx10(
484
938
  "button",
485
939
  {
486
940
  type: "button",
@@ -499,58 +953,50 @@ function MejiroSettingsPanel({
499
953
  )
500
954
  ] })
501
955
  ] }),
502
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-settings-group", children: [
503
- /* @__PURE__ */ jsx8("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
504
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-control", children: [
505
- /* @__PURE__ */ jsx8("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-kinsoku", children: messages.settingsKinsoku }),
506
- /* @__PURE__ */ jsxs5(
956
+ /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-settings-group", children: [
957
+ /* @__PURE__ */ jsx10("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
958
+ /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
959
+ /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-kinsoku", children: messages.settingsKinsoku }),
960
+ /* @__PURE__ */ jsxs6(
507
961
  "select",
508
962
  {
509
963
  id: "mejiro-reader-kinsoku",
510
964
  value: settings.mode ?? "strict",
511
965
  onChange: (e) => patch({ mode: e.target.value }),
512
966
  children: [
513
- /* @__PURE__ */ jsx8("option", { value: "strict", children: messages.settingsStrict }),
514
- /* @__PURE__ */ jsx8("option", { value: "loose", children: messages.settingsLoose })
967
+ /* @__PURE__ */ jsx10("option", { value: "strict", children: messages.settingsStrict }),
968
+ /* @__PURE__ */ jsx10("option", { value: "loose", children: messages.settingsLoose })
515
969
  ]
516
970
  }
517
971
  )
518
972
  ] }),
519
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-control", children: [
520
- /* @__PURE__ */ jsx8("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-hanging", children: messages.settingsHanging }),
521
- /* @__PURE__ */ jsxs5(
973
+ /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
974
+ /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-hanging", children: messages.settingsHanging }),
975
+ /* @__PURE__ */ jsxs6(
522
976
  "select",
523
977
  {
524
978
  id: "mejiro-reader-hanging",
525
979
  value: String(settings.enableHanging ?? true),
526
980
  onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
527
981
  children: [
528
- /* @__PURE__ */ jsx8("option", { value: "true", children: messages.toggleOn }),
529
- /* @__PURE__ */ jsx8("option", { value: "false", children: messages.toggleOff })
982
+ /* @__PURE__ */ jsx10("option", { value: "true", children: messages.toggleOn }),
983
+ /* @__PURE__ */ jsx10("option", { value: "false", children: messages.toggleOff })
530
984
  ]
531
985
  }
532
986
  )
533
987
  ] }),
534
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-control", children: [
535
- /* @__PURE__ */ jsx8("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-line-spacing", children: messages.settingsLineSpacing }),
536
- /* @__PURE__ */ jsx8(
537
- "input",
988
+ /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-control", children: [
989
+ /* @__PURE__ */ jsx10("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-line-spacing", children: messages.settingsLineSpacing }),
990
+ /* @__PURE__ */ jsx10(
991
+ CommittedNumberInput,
538
992
  {
539
993
  id: "mejiro-reader-line-spacing",
540
994
  className: "mejiro-reader-control--wide",
541
- type: "number",
542
995
  value: settings.lineSpacing ?? 1.8,
543
996
  min: 1,
544
997
  max: 3,
545
998
  step: 0.1,
546
- onChange: (e) => patch({
547
- lineSpacing: parseClampedInput(
548
- e.target.value,
549
- 1,
550
- 3,
551
- settings.lineSpacing ?? 1.8
552
- )
553
- })
999
+ onCommit: (lineSpacing) => patch({ lineSpacing })
554
1000
  }
555
1001
  )
556
1002
  ] })
@@ -559,111 +1005,23 @@ function MejiroSettingsPanel({
559
1005
  }
560
1006
 
561
1007
  // src/MejiroSpread.tsx
562
- import { normalizeFontFamily as normalizeFontFamily4 } from "@libraz/mejiro/browser";
563
1008
  import {
564
- useRef as useRef3
1009
+ isPrimaryPointerPress as isPrimaryPointerPress2,
1010
+ normalizeFontFamily as normalizeFontFamily4
1011
+ } from "@libraz/mejiro/browser";
1012
+ import {
1013
+ useRef as useRef5
565
1014
  } from "react";
566
-
567
- // src/MejiroImageOverlay.tsx
568
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
569
- function MejiroImageOverlay({
570
- rect,
571
- label,
572
- onOverlayPointerDown,
573
- onResizePointerDown,
574
- onClose
575
- }) {
576
- const messages = useI18n();
577
- const resolvedLabel = label ?? messages.imageButton;
578
- return /* @__PURE__ */ jsxs6(
579
- "div",
580
- {
581
- className: "mejiro-reader-image-overlay",
582
- style: {
583
- left: rect.x,
584
- top: rect.y,
585
- width: rect.w,
586
- height: rect.h
587
- },
588
- onPointerDown: onOverlayPointerDown,
589
- children: [
590
- /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-image-overlay-label", children: [
591
- /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-image-overlay-icon" }),
592
- /* @__PURE__ */ jsx9("span", { children: resolvedLabel })
593
- ] }),
594
- /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
595
- /* @__PURE__ */ jsx9(
596
- "button",
597
- {
598
- type: "button",
599
- "aria-label": messages.imageRemoveButton,
600
- title: messages.imageRemoveButton,
601
- className: "mejiro-reader-image-overlay-close",
602
- onPointerDown: (e) => {
603
- e.stopPropagation();
604
- e.preventDefault();
605
- onClose?.();
606
- },
607
- onKeyDown: (e) => {
608
- if (e.key !== "Enter" && e.key !== " ") return;
609
- e.stopPropagation();
610
- e.preventDefault();
611
- onClose?.();
612
- }
613
- }
614
- )
615
- ]
616
- }
617
- );
618
- }
619
-
620
- // src/MejiroSelectionLayer.tsx
621
- import { jsx as jsx10 } from "react/jsx-runtime";
622
- function MejiroSelectionLayer({
623
- rects,
624
- side,
625
- className,
626
- style,
627
- rectClassName,
628
- rectStyle
629
- }) {
630
- const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
631
- const rectClass = rectClassName ?? "mejiro-selection-rect";
632
- return /* @__PURE__ */ jsx10(
633
- "div",
634
- {
635
- className: wrapperClass,
636
- style: {
637
- position: "absolute",
638
- inset: 0,
639
- pointerEvents: "none",
640
- ...style
641
- },
642
- children: rects.map((r, i) => {
643
- if (r.side !== side) return null;
644
- const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
645
- return /* @__PURE__ */ jsx10(
646
- "div",
647
- {
648
- className: rectClass,
649
- style: {
650
- position: "absolute",
651
- width: r.width,
652
- height: r.height,
653
- ...posStyle,
654
- ...r.color != null ? { backgroundColor: r.color } : null,
655
- ...rectStyle
656
- }
657
- },
658
- i
659
- );
660
- })
661
- }
662
- );
663
- }
664
-
665
- // src/MejiroSpread.tsx
666
1015
  import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
1016
+ var OVERLAY_SELECTOR = ".mejiro-reader-image-overlay";
1017
+ var CONTENT_SELECTOR = ".mejiro-reader-page-content";
1018
+ function contentAtPoint(root, x, y) {
1019
+ for (const content of root.querySelectorAll(CONTENT_SELECTOR)) {
1020
+ const r = content.getBoundingClientRect();
1021
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return content;
1022
+ }
1023
+ return null;
1024
+ }
667
1025
  function defaultHeader(data) {
668
1026
  return /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-page-header", children: [
669
1027
  /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-page-header-title", children: data.title ?? "" }),
@@ -695,20 +1053,21 @@ function MejiroSpread({
695
1053
  selectionRects,
696
1054
  onSelectionChange,
697
1055
  singlePage = false,
1056
+ singleSide = "right",
698
1057
  onSwipe,
699
1058
  onSurfaceTap
700
1059
  }) {
701
1060
  const messages = useI18n();
702
- const hasImages = images.length > 0;
703
- const selectionStartRef = useRef3(null);
1061
+ const selectionRef = useRef5(null);
704
1062
  const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
705
- const gestureRef = useRef3(null);
1063
+ const gestureRef = useRef5(null);
706
1064
  const gestureEnabled = onSwipe != null || onSurfaceTap != null;
707
1065
  const SWIPE_THRESHOLD = 40;
708
1066
  const TAP_MOVE_THRESHOLD = 8;
709
1067
  function handleGesturePointerDown(e) {
710
1068
  if (!gestureEnabled) return;
711
- if (e.pointerType === "mouse" && e.button !== 0) return;
1069
+ if (!isPrimaryPointerPress2(e)) return;
1070
+ if (e.target?.closest(OVERLAY_SELECTOR)) return;
712
1071
  gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
713
1072
  }
714
1073
  function handleGesturePointerUp(e) {
@@ -726,15 +1085,13 @@ function MejiroSpread({
726
1085
  }
727
1086
  if (ax < TAP_MOVE_THRESHOLD && ay < TAP_MOVE_THRESHOLD) {
728
1087
  const target = e.target;
729
- if (target?.closest("button, a, .mejiro-reader-image-overlay")) return;
1088
+ if (target?.closest(`button, a, ${OVERLAY_SELECTOR}`)) return;
730
1089
  onSurfaceTap?.();
731
1090
  }
732
1091
  }
733
1092
  function resolvePointer(e) {
734
1093
  if (!anchorAtCoord) return null;
735
- const target = e.target;
736
- if (!target) return null;
737
- const content = target.closest(".mejiro-reader-page-content");
1094
+ const content = contentAtPoint(e.currentTarget, e.clientX, e.clientY);
738
1095
  if (!content) return null;
739
1096
  const pageEl = content.closest(".mejiro-reader-page");
740
1097
  const isRight = pageEl?.classList.contains("mejiro-reader-page--right") ?? true;
@@ -745,26 +1102,28 @@ function MejiroSpread({
745
1102
  return anchorAtCoord(spreadX, offsetY);
746
1103
  }
747
1104
  function handlePointerDown(e) {
748
- if (!selectionEnabled) return;
1105
+ if (!selectionEnabled || selectionRef.current) return;
1106
+ if (!e.target?.closest(CONTENT_SELECTOR)) return;
749
1107
  const anchor = resolvePointer(e);
750
1108
  if (!anchor) return;
751
1109
  e.preventDefault();
752
- selectionStartRef.current = anchor;
1110
+ selectionRef.current = { start: anchor, pointerId: e.pointerId };
753
1111
  e.currentTarget.setPointerCapture(e.pointerId);
754
1112
  onSelectionChange?.(null);
755
1113
  }
756
1114
  function handlePointerMove(e) {
757
- if (!selectionEnabled) return;
758
- const start = selectionStartRef.current;
759
- if (!start) return;
1115
+ const session = selectionRef.current;
1116
+ if (!session || e.pointerId !== session.pointerId) return;
760
1117
  const end = resolvePointer(e);
761
1118
  if (!end) return;
1119
+ const { start } = session;
762
1120
  if (end.paragraph === start.paragraph && end.charIndex === start.charIndex) return;
763
1121
  onSelectionChange?.({ start, end });
764
1122
  }
765
- function handlePointerUp(e) {
766
- if (!selectionEnabled) return;
767
- selectionStartRef.current = null;
1123
+ function endSelection(e) {
1124
+ const session = selectionRef.current;
1125
+ if (!session || e.pointerId !== session.pointerId) return;
1126
+ selectionRef.current = null;
768
1127
  if (e.currentTarget.hasPointerCapture(e.pointerId)) {
769
1128
  e.currentTarget.releasePointerCapture(e.pointerId);
770
1129
  }
@@ -774,8 +1133,12 @@ function MejiroSpread({
774
1133
  if (fontSize != null) contentStyle.fontSize = fontSize;
775
1134
  if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
776
1135
  const visibleRects = selectionRects && spreadIdx != null ? selectionRects.filter((r) => r.spreadIdx === spreadIdx) : selectionRects;
1136
+ const imageSide = singlePage ? singleSide : "right";
1137
+ const hostedImages = singlePage ? images.filter((item) => overlayPageSide(item.rect) === singleSide) : images;
1138
+ const hasImages = hostedImages.length > 0;
777
1139
  const renderPage = (side) => {
778
1140
  const isRight = side === "right";
1141
+ const hostsImages = side === imageSide;
779
1142
  const result = isRight ? spread.right : spread.left;
780
1143
  const header = isRight ? rightHeader : leftHeader;
781
1144
  const pageKey = `${side}-${header.pageNumber ?? "blank"}`;
@@ -783,7 +1146,7 @@ function MejiroSpread({
783
1146
  width: pageWidth,
784
1147
  height: pageHeight
785
1148
  };
786
- if (isRight && hasImages) pageStyle.overflow = "visible";
1149
+ if (hostsImages && hasImages) pageStyle.overflow = "visible";
787
1150
  return /* @__PURE__ */ jsxs7(
788
1151
  "div",
789
1152
  {
@@ -807,10 +1170,10 @@ function MejiroSpread({
807
1170
  ),
808
1171
  visibleRects && visibleRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: visibleRects, side })
809
1172
  ] }) }),
810
- isRight && images.map((item) => /* @__PURE__ */ jsx11(
1173
+ hostsImages && hostedImages.map((item) => /* @__PURE__ */ jsx11(
811
1174
  MejiroImageOverlay,
812
1175
  {
813
- rect: item.rect,
1176
+ rect: isRight ? item.rect : { ...item.rect, x: item.rect.x + pageWidth },
814
1177
  onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
815
1178
  onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
816
1179
  onClose: () => onImageClose?.(item.id)
@@ -826,12 +1189,13 @@ function MejiroSpread({
826
1189
  if (gestureEnabled) handleGesturePointerDown(e);
827
1190
  if (selectionEnabled) handlePointerDown(e);
828
1191
  }
829
- function combinedPointerMove(e) {
830
- if (selectionEnabled) handlePointerMove(e);
831
- }
832
1192
  function combinedPointerUp(e) {
833
1193
  if (gestureEnabled) handleGesturePointerUp(e);
834
- if (selectionEnabled) handlePointerUp(e);
1194
+ endSelection(e);
1195
+ }
1196
+ function combinedPointerCancel(e) {
1197
+ gestureRef.current = null;
1198
+ endSelection(e);
835
1199
  }
836
1200
  const useCombined = selectionEnabled || gestureEnabled;
837
1201
  return /* @__PURE__ */ jsx11("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs7(
@@ -839,11 +1203,12 @@ function MejiroSpread({
839
1203
  {
840
1204
  className: `mejiro-reader-spread${turning ? " is-turning" : ""}${singlePage ? " mejiro-reader-spread--single" : ""}`,
841
1205
  onPointerDown: useCombined ? combinedPointerDown : void 0,
842
- onPointerMove: useCombined ? combinedPointerMove : void 0,
1206
+ onPointerMove: useCombined ? handlePointerMove : void 0,
843
1207
  onPointerUp: useCombined ? combinedPointerUp : void 0,
844
- onPointerCancel: useCombined ? combinedPointerUp : void 0,
1208
+ onPointerCancel: useCombined ? combinedPointerCancel : void 0,
1209
+ onLostPointerCapture: selectionEnabled ? endSelection : void 0,
845
1210
  children: [
846
- renderPage("right"),
1211
+ renderPage(singlePage ? singleSide : "right"),
847
1212
  !singlePage && renderPage("left"),
848
1213
  /* @__PURE__ */ jsx11(
849
1214
  "button",
@@ -902,29 +1267,127 @@ function MejiroStats({
902
1267
  return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats", children: parts.filter(Boolean).join(" / ") });
903
1268
  }
904
1269
 
1270
+ // src/persistence.ts
1271
+ function resolveDefaultStorage() {
1272
+ try {
1273
+ return globalThis.localStorage ?? null;
1274
+ } catch {
1275
+ return null;
1276
+ }
1277
+ }
1278
+ function readStorage(storage, key, parse, fallback) {
1279
+ if (!storage) return fallback;
1280
+ try {
1281
+ return parse(storage.getItem(key));
1282
+ } catch {
1283
+ return fallback;
1284
+ }
1285
+ }
1286
+ function removeStorage(storage, key) {
1287
+ if (!storage) return;
1288
+ try {
1289
+ storage.removeItem(key);
1290
+ } catch {
1291
+ }
1292
+ }
1293
+ function uniqueChapterId(existing) {
1294
+ const taken = new Set(existing.map((chapter) => chapter.id));
1295
+ const stamp = Date.now();
1296
+ let n = existing.length;
1297
+ while (taken.has(`chapter-${stamp}-${n}`)) n++;
1298
+ return `chapter-${stamp}-${n}`;
1299
+ }
1300
+ function mergeDefined(base, patch) {
1301
+ const next = { ...base };
1302
+ for (const key of Object.keys(patch)) {
1303
+ const value = patch[key];
1304
+ if (value !== void 0) next[key] = value;
1305
+ }
1306
+ return next;
1307
+ }
1308
+ function createPendingWrite() {
1309
+ let timer = null;
1310
+ let pending = null;
1311
+ function cancel() {
1312
+ if (timer) {
1313
+ clearTimeout(timer);
1314
+ timer = null;
1315
+ }
1316
+ pending = null;
1317
+ }
1318
+ function flush() {
1319
+ const write = pending;
1320
+ cancel();
1321
+ if (!write) return;
1322
+ try {
1323
+ write();
1324
+ } catch {
1325
+ }
1326
+ }
1327
+ return {
1328
+ schedule(write, delayMs) {
1329
+ cancel();
1330
+ pending = write;
1331
+ timer = setTimeout(flush, delayMs);
1332
+ },
1333
+ flush,
1334
+ cancel
1335
+ };
1336
+ }
1337
+ function flushOnPageHide(flush) {
1338
+ if (typeof window === "undefined") return () => {
1339
+ };
1340
+ const handler = () => flush();
1341
+ window.addEventListener("beforeunload", handler);
1342
+ window.addEventListener("pagehide", handler);
1343
+ return () => {
1344
+ window.removeEventListener("beforeunload", handler);
1345
+ window.removeEventListener("pagehide", handler);
1346
+ };
1347
+ }
1348
+ function snapshotChapters(chapters) {
1349
+ return chapters.map(({ id, title, body }) => ({ id, title, body }));
1350
+ }
1351
+ function createDraftChangeTracker(chapters, key) {
1352
+ let lastChapters = snapshotChapters(chapters);
1353
+ let lastKey = key;
1354
+ return {
1355
+ changed(nextChapters, nextKey) {
1356
+ const same = nextKey === lastKey && nextChapters.length === lastChapters.length && nextChapters.every((chapter, i) => {
1357
+ const prev = lastChapters[i];
1358
+ return chapter.id === prev.id && chapter.title === prev.title && chapter.body === prev.body;
1359
+ });
1360
+ if (same) return false;
1361
+ lastChapters = snapshotChapters(nextChapters);
1362
+ lastKey = nextKey;
1363
+ return true;
1364
+ }
1365
+ };
1366
+ }
1367
+
905
1368
  // src/useChapterLayout.ts
906
1369
  import {
907
- useCallback,
908
- useEffect as useEffect2,
1370
+ useCallback as useCallback2,
1371
+ useEffect as useEffect4,
909
1372
  useLayoutEffect as useLayoutEffect2,
910
- useRef as useRef4,
911
- useState as useState2
1373
+ useRef as useRef6,
1374
+ useState as useState4
912
1375
  } from "react";
913
1376
  function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
914
1377
  const enableResize = options.enableResize ?? true;
915
1378
  const resizeDebounce = options.resizeDebounce ?? 120;
916
- const [layout, setLayout] = useState2(null);
917
- const [pageWidth, setPageWidth] = useState2(0);
918
- const [pageHeight, setPageHeight] = useState2(0);
919
- const [contentHeight, setContentHeight] = useState2(0);
920
- const [elapsedMs, setElapsedMs] = useState2(0);
921
- const layoutRef = useRef4(null);
922
- const requestIdRef = useRef4(0);
923
- const pendingRestore = useRef4(null);
1379
+ const [layout, setLayout] = useState4(null);
1380
+ const [pageWidth, setPageWidth] = useState4(0);
1381
+ const [pageHeight, setPageHeight] = useState4(0);
1382
+ const [contentHeight, setContentHeight] = useState4(0);
1383
+ const [elapsedMs, setElapsedMs] = useState4(0);
1384
+ const layoutRef = useRef6(null);
1385
+ const requestIdRef = useRef6(0);
1386
+ const pendingRestore = useRef6(null);
924
1387
  layoutRef.current = layout;
925
- const optionsRef = useRef4(options);
1388
+ const optionsRef = useRef6(options);
926
1389
  optionsRef.current = options;
927
- const recompute = useCallback(
1390
+ const recompute = useCallback2(
928
1391
  async (opts = {}) => {
929
1392
  const blank = opts.blank ?? true;
930
1393
  const o = optionsRef.current;
@@ -963,13 +1426,20 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
963
1426
  setContentHeight(0);
964
1427
  setElapsedMs(0);
965
1428
  }, [recompute]);
966
- useEffect2(() => {
1429
+ useEffect4(() => {
967
1430
  void recompute();
968
1431
  return () => {
969
1432
  requestIdRef.current++;
970
1433
  };
971
1434
  }, [recompute]);
972
- useEffect2(() => {
1435
+ useLayoutEffect2(() => {
1436
+ const anchor = pendingRestore.current;
1437
+ const restore = optionsRef.current.restorePosition;
1438
+ if (!(layout && anchor && restore)) return;
1439
+ pendingRestore.current = null;
1440
+ restore(layout, anchor);
1441
+ }, [layout]);
1442
+ useEffect4(() => {
973
1443
  if (!enableResize) return;
974
1444
  const el = surface.current;
975
1445
  if (!el) return;
@@ -1024,86 +1494,35 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1024
1494
 
1025
1495
  // src/useEpub.ts
1026
1496
  import { parseEpub } from "@libraz/mejiro/epub";
1027
- import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef5, useState as useState3 } from "react";
1497
+ import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef7, useState as useState5 } from "react";
1028
1498
  function useEpub(options = {}) {
1029
- const [epub, setEpub] = useState3(null);
1030
- const [loading, setLoading] = useState3(false);
1031
- const [error, setError] = useState3(null);
1032
- const requestIdRef = useRef5(0);
1033
- const onLoadRef = useRef5(options.onLoad);
1034
- onLoadRef.current = options.onLoad;
1035
- const onErrorRef = useRef5(options.onError);
1036
- onErrorRef.current = options.onError;
1037
- const fetchOptionsRef = useRef5(options.fetchOptions);
1038
- fetchOptionsRef.current = options.fetchOptions;
1039
- const fetchEpubRef = useRef5(options.fetchEpub);
1040
- fetchEpubRef.current = options.fetchEpub;
1041
- const limitsRef = useRef5(options.limits);
1042
- limitsRef.current = options.limits;
1043
- const loadBuffer = useCallback2(async (buffer) => {
1044
- const requestId = ++requestIdRef.current;
1045
- setLoading(true);
1046
- setError(null);
1047
- try {
1048
- const book = await parseEpub(buffer, { limits: limitsRef.current });
1049
- if (requestId !== requestIdRef.current) return null;
1050
- setEpub(book);
1051
- onLoadRef.current?.(book);
1052
- return book;
1053
- } catch (err) {
1054
- if (requestId === requestIdRef.current) {
1055
- const nextError = err instanceof Error ? err : new Error(String(err));
1056
- setError(nextError);
1057
- onErrorRef.current?.(nextError);
1058
- }
1059
- return null;
1060
- } finally {
1061
- if (requestId === requestIdRef.current) setLoading(false);
1062
- }
1063
- }, []);
1064
- const loadFile = useCallback2(async (file) => {
1065
- const requestId = ++requestIdRef.current;
1066
- setLoading(true);
1067
- setError(null);
1068
- try {
1069
- const book = await parseEpub(await file.arrayBuffer(), { limits: limitsRef.current });
1070
- if (requestId !== requestIdRef.current) return null;
1071
- setEpub(book);
1072
- onLoadRef.current?.(book);
1073
- return book;
1074
- } catch (err) {
1075
- if (requestId === requestIdRef.current) {
1076
- const nextError = err instanceof Error ? err : new Error(String(err));
1077
- setError(nextError);
1078
- onErrorRef.current?.(nextError);
1079
- }
1080
- return null;
1081
- } finally {
1082
- if (requestId === requestIdRef.current) setLoading(false);
1083
- }
1084
- }, []);
1085
- const loadUrl = useCallback2(async (url) => {
1499
+ const [epub, setEpub] = useState5(null);
1500
+ const [loading, setLoading] = useState5(false);
1501
+ const [error, setError] = useState5(null);
1502
+ const requestIdRef = useRef7(0);
1503
+ const onLoadRef = useRef7(options.onLoad);
1504
+ onLoadRef.current = options.onLoad;
1505
+ const onErrorRef = useRef7(options.onError);
1506
+ onErrorRef.current = options.onError;
1507
+ const fetchOptionsRef = useRef7(options.fetchOptions);
1508
+ fetchOptionsRef.current = options.fetchOptions;
1509
+ const fetchEpubRef = useRef7(options.fetchEpub);
1510
+ fetchEpubRef.current = options.fetchEpub;
1511
+ const limitsRef = useRef7(options.limits);
1512
+ limitsRef.current = options.limits;
1513
+ const load = useCallback3(async (read) => {
1086
1514
  const requestId = ++requestIdRef.current;
1087
1515
  setLoading(true);
1088
1516
  setError(null);
1089
1517
  try {
1090
- let buffer;
1091
- if (fetchEpubRef.current) {
1092
- buffer = await fetchEpubRef.current(url);
1093
- } else {
1094
- const init = fetchOptionsRef.current;
1095
- const res = init ? await fetch(url, init) : await fetch(url);
1096
- if (!res.ok) return null;
1097
- buffer = await res.arrayBuffer();
1098
- }
1099
- const book = await parseEpub(buffer, { limits: limitsRef.current });
1518
+ const book = await parseEpub(await read(), { limits: limitsRef.current });
1100
1519
  if (requestId !== requestIdRef.current) return null;
1101
1520
  setEpub(book);
1102
1521
  onLoadRef.current?.(book);
1103
1522
  return book;
1104
1523
  } catch (err) {
1105
1524
  if (requestId === requestIdRef.current) {
1106
- const nextError = err instanceof Error ? err : new Error(String(err));
1525
+ const nextError = toError(err);
1107
1526
  setError(nextError);
1108
1527
  onErrorRef.current?.(nextError);
1109
1528
  }
@@ -1112,11 +1531,22 @@ function useEpub(options = {}) {
1112
1531
  if (requestId === requestIdRef.current) setLoading(false);
1113
1532
  }
1114
1533
  }, []);
1534
+ const loadBuffer = useCallback3((buffer) => load(async () => buffer), [load]);
1535
+ const loadFile = useCallback3(
1536
+ (file) => load(() => readEpubFile(file, limitsRef.current)),
1537
+ [load]
1538
+ );
1539
+ const loadUrl = useCallback3(
1540
+ (url) => load(
1541
+ () => fetchEpubRef.current ? fetchEpubRef.current(url) : fetchEpubBuffer(url, fetchOptionsRef.current)
1542
+ ),
1543
+ [load]
1544
+ );
1115
1545
  const defaultUrl = options.defaultUrl;
1116
- useEffect3(() => {
1546
+ useEffect5(() => {
1117
1547
  if (defaultUrl) void loadUrl(defaultUrl);
1118
1548
  }, [defaultUrl, loadUrl]);
1119
- const replaceEpub = useCallback2((book) => {
1549
+ const replaceEpub = useCallback3((book) => {
1120
1550
  requestIdRef.current++;
1121
1551
  setLoading(false);
1122
1552
  setError(null);
@@ -1127,15 +1557,15 @@ function useEpub(options = {}) {
1127
1557
 
1128
1558
  // src/useMejiroBook.ts
1129
1559
  import { MejiroBook } from "@libraz/mejiro/book";
1130
- import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef6, useState as useState4 } from "react";
1560
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef8, useState as useState6 } from "react";
1131
1561
  function useMejiroBook(initial, options = {}) {
1132
- const bookRef = useRef6(null);
1562
+ const bookRef = useRef8(null);
1133
1563
  if (!bookRef.current) bookRef.current = new MejiroBook(initial);
1134
- const [snapshot, setLocal] = useState4(initial);
1135
- const optionsRef = useRef6(options);
1564
+ const [snapshot, setLocal] = useState6(initial);
1565
+ const optionsRef = useRef8(options);
1136
1566
  optionsRef.current = options;
1137
- const pendingRef = useRef6(null);
1138
- const flush = useCallback3(() => {
1567
+ const pendingRef = useRef8(null);
1568
+ const flush = useCallback4(() => {
1139
1569
  const pending = pendingRef.current;
1140
1570
  if (!pending) return;
1141
1571
  pendingRef.current = null;
@@ -1147,7 +1577,7 @@ function useMejiroBook(initial, options = {}) {
1147
1577
  for (const waiter of waiters) waiter.resolve();
1148
1578
  },
1149
1579
  (err) => {
1150
- const error = err instanceof Error ? err : new Error(String(err));
1580
+ const error = toError(err);
1151
1581
  const handler = optionsRef.current.onError;
1152
1582
  if (handler) {
1153
1583
  handler(error);
@@ -1158,12 +1588,12 @@ function useMejiroBook(initial, options = {}) {
1158
1588
  }
1159
1589
  );
1160
1590
  }, []);
1161
- const setOptions = useCallback3(
1591
+ const setOptions = useCallback4(
1162
1592
  (next) => {
1163
- setLocal((prev) => ({ ...prev, ...next }));
1593
+ setLocal((prev) => mergeDefined(prev, next));
1164
1594
  const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
1165
1595
  pendingRef.current = pending;
1166
- Object.assign(pending.patch, next);
1596
+ pending.patch = mergeDefined(pending.patch, next);
1167
1597
  const settled = new Promise((resolve, reject) => {
1168
1598
  pending.waiters.push({ resolve, reject });
1169
1599
  });
@@ -1182,8 +1612,9 @@ function useMejiroBook(initial, options = {}) {
1182
1612
  },
1183
1613
  [flush]
1184
1614
  );
1185
- useEffect4(
1615
+ useEffect6(
1186
1616
  () => () => {
1617
+ bookRef.current?.dispose();
1187
1618
  const pending = pendingRef.current;
1188
1619
  if (!pending) return;
1189
1620
  pendingRef.current = null;
@@ -1195,205 +1626,8 @@ function useMejiroBook(initial, options = {}) {
1195
1626
  return { book: bookRef.current, options: snapshot, setOptions };
1196
1627
  }
1197
1628
 
1198
- // src/useMultiImageOverlay.ts
1199
- import { createOverlayDragSession } from "@libraz/mejiro/browser";
1200
- import {
1201
- useCallback as useCallback4,
1202
- useEffect as useEffect5,
1203
- useMemo as useMemo3,
1204
- useRef as useRef7,
1205
- useState as useState5
1206
- } from "react";
1207
- var nextId = 0;
1208
- function useMultiImageOverlay(layout, spreadIdx, options = {}) {
1209
- const defW = options.defaultWidth ?? 120;
1210
- const defH = options.defaultHeight ?? 160;
1211
- const defX = options.defaultX ?? 80;
1212
- const defY = options.defaultY ?? 100;
1213
- const margin = options.margin;
1214
- const [images, setImages] = useState5(() => /* @__PURE__ */ new Map());
1215
- const imagesRef = useRef7(images);
1216
- imagesRef.current = images;
1217
- const layoutRef = useRef7(null);
1218
- layoutRef.current = layout;
1219
- const spreadIdxRef = useRef7(spreadIdx);
1220
- spreadIdxRef.current = spreadIdx;
1221
- const onUpdateRef = useRef7(options.onUpdate);
1222
- onUpdateRef.current = options.onUpdate;
1223
- const activeDragCleanupsRef = useRef7(/* @__PURE__ */ new Set());
1224
- useEffect5(
1225
- () => () => {
1226
- for (const cleanup of activeDragCleanupsRef.current) cleanup();
1227
- activeDragCleanupsRef.current.clear();
1228
- },
1229
- []
1230
- );
1231
- const syncSpread = useCallback4(
1232
- (si) => {
1233
- const lo = layoutRef.current;
1234
- if (!lo) return;
1235
- const items = imagesRef.current.get(si) ?? [];
1236
- const bookImages = items.map((it) => ({
1237
- x: it.rect.x,
1238
- y: it.rect.y,
1239
- w: it.rect.w,
1240
- h: it.rect.h,
1241
- margin
1242
- }));
1243
- lo.setImages(si, bookImages);
1244
- onUpdateRef.current?.(lo.getSpread(si));
1245
- },
1246
- [margin]
1247
- );
1248
- const commit = useCallback4(
1249
- (next, affectedSpread) => {
1250
- imagesRef.current = next;
1251
- setImages(next);
1252
- syncSpread(affectedSpread);
1253
- },
1254
- [syncSpread]
1255
- );
1256
- const findById = useCallback4(
1257
- (id) => {
1258
- for (const [s, list] of imagesRef.current) {
1259
- const i = list.findIndex((it) => it.id === id);
1260
- if (i >= 0) return { spread: s, index: i, item: list[i] };
1261
- }
1262
- return null;
1263
- },
1264
- []
1265
- );
1266
- const addImage = useCallback4(
1267
- (overrides) => {
1268
- const si = spreadIdxRef.current;
1269
- const list = imagesRef.current.get(si) ?? [];
1270
- const last = list[list.length - 1];
1271
- const baseX = last ? last.rect.x - 100 : defX;
1272
- const baseY = last ? last.rect.y + 40 : defY;
1273
- nextId += 1;
1274
- const item = {
1275
- id: `mejiro-img-${nextId}`,
1276
- rect: {
1277
- x: overrides?.x ?? baseX,
1278
- y: overrides?.y ?? baseY,
1279
- w: overrides?.w ?? defW,
1280
- h: overrides?.h ?? defH
1281
- }
1282
- };
1283
- const next = new Map(imagesRef.current);
1284
- next.set(si, [...list, item]);
1285
- commit(next, si);
1286
- return item;
1287
- },
1288
- [defX, defY, defW, defH, commit]
1289
- );
1290
- const removeImage = useCallback4(
1291
- (id) => {
1292
- const found = findById(id);
1293
- if (!found) return;
1294
- const next = new Map(imagesRef.current);
1295
- const list = (next.get(found.spread) ?? []).filter((it) => it.id !== id);
1296
- if (list.length === 0) next.delete(found.spread);
1297
- else next.set(found.spread, list);
1298
- commit(next, found.spread);
1299
- },
1300
- [findById, commit]
1301
- );
1302
- const updateImage = useCallback4(
1303
- (id, rect) => {
1304
- const found = findById(id);
1305
- if (!found) return;
1306
- const next = new Map(imagesRef.current);
1307
- const list = (next.get(found.spread) ?? []).map(
1308
- (it) => it.id === id ? { ...it, rect: { ...it.rect, ...rect } } : it
1309
- );
1310
- next.set(found.spread, list);
1311
- commit(next, found.spread);
1312
- },
1313
- [findById, commit]
1314
- );
1315
- const clearImages = useCallback4(
1316
- (target) => {
1317
- if (target == null) {
1318
- const old = imagesRef.current;
1319
- imagesRef.current = /* @__PURE__ */ new Map();
1320
- setImages(/* @__PURE__ */ new Map());
1321
- layoutRef.current?.clearImages();
1322
- if (layoutRef.current) {
1323
- for (const si of old.keys()) onUpdateRef.current?.(layoutRef.current.getSpread(si));
1324
- }
1325
- } else {
1326
- const next = new Map(imagesRef.current);
1327
- next.delete(target);
1328
- commit(next, target);
1329
- }
1330
- },
1331
- [commit]
1332
- );
1333
- const onOverlayPointerDown = useCallback4(
1334
- (id, e) => {
1335
- e.preventDefault();
1336
- const found = findById(id);
1337
- if (!found) return;
1338
- const target = e.currentTarget;
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
- });
1351
- },
1352
- [findById, updateImage]
1353
- );
1354
- const onResizePointerDown = useCallback4(
1355
- (id, e) => {
1356
- e.preventDefault();
1357
- e.stopPropagation();
1358
- const found = findById(id);
1359
- if (!found) return;
1360
- const target = e.currentTarget;
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
- });
1373
- },
1374
- [findById, updateImage]
1375
- );
1376
- useEffect5(() => {
1377
- if (!layout) return;
1378
- for (const si of imagesRef.current.keys()) syncSpread(si);
1379
- }, [layout, syncSpread]);
1380
- const currentImages = useMemo3(() => images.get(spreadIdx) ?? [], [images, spreadIdx]);
1381
- const hasImages = useMemo3(() => [...images.values()].some((list) => list.length > 0), [images]);
1382
- return {
1383
- imagesBySpread: images,
1384
- currentImages,
1385
- hasImages,
1386
- addImage,
1387
- removeImage,
1388
- updateImage,
1389
- clearImages,
1390
- onOverlayPointerDown,
1391
- onResizePointerDown
1392
- };
1393
- }
1394
-
1395
1629
  // src/useSpread.ts
1396
- import { useCallback as useCallback5, useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef8, useState as useState6 } from "react";
1630
+ import { useCallback as useCallback5, useEffect as useEffect7, useLayoutEffect as useLayoutEffect3, useRef as useRef9, useState as useState7 } from "react";
1397
1631
  function ignoreNavigationKey(e) {
1398
1632
  if (e.defaultPrevented) return true;
1399
1633
  if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
@@ -1403,29 +1637,57 @@ function ignoreNavigationKey(e) {
1403
1637
  const tag = target.tagName;
1404
1638
  return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
1405
1639
  }
1640
+ function navigationCount(totalPages, single) {
1641
+ return Math.max(1, single ? totalPages : Math.ceil(totalPages / 2));
1642
+ }
1643
+ function firstPageOf(index, single) {
1644
+ return single ? index : index * 2;
1645
+ }
1646
+ function indexOfPageIn(pageIdx, single) {
1647
+ return single ? pageIdx : Math.floor(pageIdx / 2);
1648
+ }
1649
+ function firstTextAnchorFrom(layout, page) {
1650
+ for (let p = page; p < layout.totalPages; p++) {
1651
+ const anchor = layout.anchorAt(Math.floor(p / 2), p % 2 === 0 ? "right" : "left");
1652
+ if (anchor) return anchor;
1653
+ }
1654
+ return null;
1655
+ }
1406
1656
  function useSpread(layout, options = {}) {
1407
1657
  const enableKeyboard = options.enableKeyboard ?? true;
1408
1658
  const turnDuration = options.turnDuration ?? 180;
1409
- const onChangeRef = useRef8(options.onChange);
1659
+ const single = options.single ?? false;
1660
+ const onChangeRef = useRef9(options.onChange);
1410
1661
  onChangeRef.current = options.onChange;
1411
- const [spreadIdx, setSpreadIdx] = useState6(0);
1412
- const [spread, setSpread] = useState6(null);
1413
- const [turning, setTurning] = useState6(false);
1414
- const layoutRef = useRef8(null);
1662
+ const [spreadIdx, setSpreadIdx] = useState7(0);
1663
+ const [indexSingle, setIndexSingle] = useState7(single);
1664
+ if (indexSingle !== single) {
1665
+ setIndexSingle(single);
1666
+ setSpreadIdx(indexOfPageIn(firstPageOf(spreadIdx, indexSingle), single));
1667
+ }
1668
+ const [spread, setSpread] = useState7(null);
1669
+ const [turning, setTurning] = useState7(false);
1670
+ const layoutRef = useRef9(null);
1415
1671
  layoutRef.current = layout;
1416
- const spreadIdxRef = useRef8(0);
1672
+ const spreadIdxRef = useRef9(0);
1417
1673
  spreadIdxRef.current = spreadIdx;
1418
- const turnTimerRef = useRef8(null);
1419
- const layoutGenerationRef = useRef8(0);
1420
- const lastEmittedRef = useRef8(null);
1421
- const totalPages = layout?.totalPages ?? 0;
1422
- const totalSpreads = Math.max(1, Math.ceil(totalPages / 2));
1674
+ const singleRef = useRef9(single);
1675
+ singleRef.current = single;
1676
+ const turnTimerRef = useRef9(null);
1677
+ const layoutGenerationRef = useRef9(0);
1678
+ const lastEmittedRef = useRef9(null);
1679
+ const totalPages = spread?.totalPages ?? 0;
1680
+ const totalSpreads = navigationCount(totalPages, single);
1681
+ const firstPage = firstPageOf(spreadIdx, single);
1682
+ const layoutSpreadIdx = Math.floor(firstPage / 2);
1683
+ const singleSide = single ? firstPage % 2 === 0 ? "right" : "left" : null;
1423
1684
  const refresh = useCallback5(() => {
1424
1685
  if (!layoutRef.current) {
1425
1686
  setSpread(null);
1426
1687
  return;
1427
1688
  }
1428
- setSpread(layoutRef.current.getSpread(spreadIdxRef.current));
1689
+ const page = firstPageOf(spreadIdxRef.current, singleRef.current);
1690
+ setSpread(layoutRef.current.getSpread(Math.floor(page / 2)));
1429
1691
  }, []);
1430
1692
  useLayoutEffect3(() => {
1431
1693
  layoutGenerationRef.current++;
@@ -1441,9 +1703,9 @@ function useSpread(layout, options = {}) {
1441
1703
  }
1442
1704
  setSpread(layout.getSpread(0));
1443
1705
  }, [layout]);
1444
- useEffect6(() => {
1706
+ useEffect7(() => {
1445
1707
  if (!layout) return;
1446
- setSpread(layout.getSpread(spreadIdx));
1708
+ setSpread(layout.getSpread(layoutSpreadIdx));
1447
1709
  const last = lastEmittedRef.current;
1448
1710
  if (!last) {
1449
1711
  lastEmittedRef.current = { layout, spreadIdx };
@@ -1453,11 +1715,12 @@ function useSpread(layout, options = {}) {
1453
1715
  onChangeRef.current?.(spreadIdx);
1454
1716
  }
1455
1717
  lastEmittedRef.current = { layout, spreadIdx };
1456
- }, [layout, spreadIdx]);
1718
+ }, [layout, spreadIdx, layoutSpreadIdx]);
1457
1719
  const goTo = useCallback5(
1458
1720
  (index) => {
1459
1721
  if (!layoutRef.current) return;
1460
- const max = Math.max(1, Math.ceil(layoutRef.current.totalPages / 2)) - 1;
1722
+ const targetSingle = singleRef.current;
1723
+ const max = navigationCount(layoutRef.current.totalPages, targetSingle) - 1;
1461
1724
  const target = Math.max(0, Math.min(max, index));
1462
1725
  if (target === spreadIdxRef.current) return;
1463
1726
  if (turnDuration > 0) {
@@ -1466,7 +1729,7 @@ function useSpread(layout, options = {}) {
1466
1729
  setTurning(true);
1467
1730
  turnTimerRef.current = setTimeout(() => {
1468
1731
  if (generation !== layoutGenerationRef.current) return;
1469
- setSpreadIdx(target);
1732
+ setSpreadIdx(indexOfPageIn(firstPageOf(target, targetSingle), singleRef.current));
1470
1733
  setTurning(false);
1471
1734
  turnTimerRef.current = null;
1472
1735
  }, turnDuration);
@@ -1483,12 +1746,20 @@ function useSpread(layout, options = {}) {
1483
1746
  turnTimerRef.current = null;
1484
1747
  }
1485
1748
  setTurning(false);
1486
- const max = Math.max(1, Math.ceil(layoutRef.current.totalPages / 2)) - 1;
1749
+ const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
1487
1750
  setSpreadIdx(Math.max(0, Math.min(max, index)));
1488
1751
  }, []);
1752
+ const indexOfPage = useCallback5((pageIdx) => indexOfPageIn(pageIdx, single), [single]);
1753
+ const anchorAt = useCallback5(
1754
+ (index) => {
1755
+ if (!layout || index < 0 || index >= navigationCount(layout.totalPages, single)) return null;
1756
+ return firstTextAnchorFrom(layout, firstPageOf(index, single));
1757
+ },
1758
+ [layout, single]
1759
+ );
1489
1760
  const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
1490
1761
  const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
1491
- useEffect6(() => {
1762
+ useEffect7(() => {
1492
1763
  if (!enableKeyboard) return;
1493
1764
  const onKey = (e) => {
1494
1765
  if (!layoutRef.current) return;
@@ -1499,7 +1770,7 @@ function useSpread(layout, options = {}) {
1499
1770
  window.addEventListener("keydown", onKey);
1500
1771
  return () => window.removeEventListener("keydown", onKey);
1501
1772
  }, [enableKeyboard, next, prev]);
1502
- useEffect6(
1773
+ useEffect7(
1503
1774
  () => () => {
1504
1775
  if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
1505
1776
  },
@@ -1508,6 +1779,9 @@ function useSpread(layout, options = {}) {
1508
1779
  return {
1509
1780
  spreadIdx,
1510
1781
  spread,
1782
+ firstPage,
1783
+ layoutSpreadIdx,
1784
+ singleSide,
1511
1785
  totalPages,
1512
1786
  totalSpreads,
1513
1787
  turning,
@@ -1515,6 +1789,8 @@ function useSpread(layout, options = {}) {
1515
1789
  prev,
1516
1790
  goTo,
1517
1791
  setSpread: setSpreadIndex,
1792
+ indexOfPage,
1793
+ anchorAt,
1518
1794
  refresh
1519
1795
  };
1520
1796
  }
@@ -1522,6 +1798,52 @@ function useSpread(layout, options = {}) {
1522
1798
  // src/MejiroReader.tsx
1523
1799
  import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
1524
1800
  var OPTIONS_DEBOUNCE_MS = 60;
1801
+ var IMAGE_DEFAULT_X = 80;
1802
+ function singlePageImagePlacement(side, pageWidth) {
1803
+ if (side == null) return void 0;
1804
+ return { x: side === "left" ? IMAGE_DEFAULT_X - pageWidth : IMAGE_DEFAULT_X };
1805
+ }
1806
+ var MANUSCRIPT_SOURCE = /* @__PURE__ */ Symbol("manuscript");
1807
+ function manuscriptKey(manuscript) {
1808
+ if (manuscript === void 0) return void 0;
1809
+ return JSON.stringify(manuscript.map(({ id, title, body }) => ({ id, title, body })));
1810
+ }
1811
+ function reflowOptionsKey(o) {
1812
+ return JSON.stringify([
1813
+ o.fontFamily,
1814
+ o.fontSize,
1815
+ o.lineSpacing,
1816
+ o.mode,
1817
+ o.enableHanging,
1818
+ o.headingScale,
1819
+ o.headingStyles
1820
+ ]);
1821
+ }
1822
+ function scrollTargetPage(spread, userPage) {
1823
+ return userPage != null && spread.indexOfPage(userPage) === spread.spreadIdx ? userPage : spread.firstPage;
1824
+ }
1825
+ function createLifecycleTracker(sink) {
1826
+ let last = null;
1827
+ let dwell = null;
1828
+ return (chapter, spreadIdx, view, controlledSpreadIdx) => {
1829
+ if (!view) return;
1830
+ const totalSpreads = view.total;
1831
+ if (controlledSpreadIdx != null) {
1832
+ const target = Math.max(0, Math.min(totalSpreads - 1, controlledSpreadIdx));
1833
+ if (spreadIdx !== target) return;
1834
+ }
1835
+ if (last && last.chapter === chapter && last.spreadIdx === spreadIdx) return;
1836
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
1837
+ const previous = last;
1838
+ if (previous && dwell) sink.pageRead(dwell.anchor, now - dwell.ts);
1839
+ const inCh = view.anchor;
1840
+ dwell = inCh ? { anchor: { chapter, ...inCh }, ts: now } : null;
1841
+ last = { chapter, spreadIdx };
1842
+ if (!previous) return;
1843
+ sink.spreadChanged(chapter, spreadIdx);
1844
+ if (spreadIdx === totalSpreads - 1) sink.chapterFinished(chapter);
1845
+ };
1846
+ }
1525
1847
  function MejiroReaderInner(props, ref) {
1526
1848
  const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
1527
1849
  const dialectProp = "dialect" in props ? props.dialect : void 0;
@@ -1568,15 +1890,18 @@ function MejiroReaderInner(props, ref) {
1568
1890
  onPageRead,
1569
1891
  onChapterCompleted
1570
1892
  } = props;
1571
- const surfaceRef = useRef9(null);
1572
- const fileRef = useRef9(null);
1573
- const [settingsOpen, setSettingsOpen] = useState7(false);
1574
- const [chapterState, setChapterState] = useState7(chapterProp ?? 0);
1893
+ const surfaceRef = useRef10(null);
1894
+ const fileRef = useRef10(null);
1895
+ const [settingsOpen, setSettingsOpen] = useState8(false);
1896
+ const [chapterState, setChapterState] = useState8(chapterProp ?? 0);
1575
1897
  const chapterIsUncontrolled = chapterProp == null;
1576
1898
  const chapter = chapterProp ?? chapterState;
1577
- const [chromeHidden, setChromeHidden] = useState7(false);
1578
- const [autoSingle, setAutoSingle] = useState7(false);
1579
- useEffect7(() => {
1899
+ const [chromeHidden, setChromeHidden] = useState8(false);
1900
+ const [userScroll, setUserScroll] = useState8(
1901
+ null
1902
+ );
1903
+ const [autoSingle, setAutoSingle] = useState8(false);
1904
+ useEffect8(() => {
1580
1905
  if (!settingsOpen) return;
1581
1906
  const onKey = (e2) => {
1582
1907
  if (e2.key === "Escape") setSettingsOpen(false);
@@ -1584,7 +1909,7 @@ function MejiroReaderInner(props, ref) {
1584
1909
  window.addEventListener("keydown", onKey);
1585
1910
  return () => window.removeEventListener("keydown", onKey);
1586
1911
  }, [settingsOpen]);
1587
- useEffect7(() => {
1912
+ useEffect8(() => {
1588
1913
  if (spreadMode !== "auto") return;
1589
1914
  const surface = surfaceRef.current;
1590
1915
  if (!surface) return;
@@ -1601,16 +1926,19 @@ function MejiroReaderInner(props, ref) {
1601
1926
  const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
1602
1927
  const resolvedGeometry = useMemo4(() => {
1603
1928
  const columns = effectiveSingle ? 1 : 2;
1604
- if (fit !== "width") return { columns, ...pageGeometryProp };
1605
- return {
1606
- columns,
1607
- gutterOffset: 0,
1608
- headerOffset: 0,
1609
- maxHeight: Number.POSITIVE_INFINITY,
1610
- minWidth: 0,
1611
- minHeight: 0,
1612
- ...pageGeometryProp
1613
- };
1929
+ const geometry = pageGeometryProp ?? {};
1930
+ if (fit !== "width") return mergeDefined({ columns }, geometry);
1931
+ return mergeDefined(
1932
+ {
1933
+ columns,
1934
+ gutterOffset: 0,
1935
+ headerOffset: 0,
1936
+ maxHeight: Number.POSITIVE_INFINITY,
1937
+ minWidth: 0,
1938
+ minHeight: 0
1939
+ },
1940
+ geometry
1941
+ );
1614
1942
  }, [fit, pageGeometryProp, effectiveSingle]);
1615
1943
  const surfaceAspect = useMemo4(() => {
1616
1944
  const columns = effectiveSingle ? 1 : 2;
@@ -1618,10 +1946,10 @@ function MejiroReaderInner(props, ref) {
1618
1946
  return `${columns} / ${aspect}`;
1619
1947
  }, [effectiveSingle, resolvedGeometry]);
1620
1948
  const resolvedOptions = useMemo4(
1621
- () => ({ ...DEFAULT_BOOK_OPTIONS, ...optionsProp ?? {} }),
1949
+ () => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
1622
1950
  [optionsProp]
1623
1951
  );
1624
- const onErrorRef = useRef9(onError);
1952
+ const onErrorRef = useRef10(onError);
1625
1953
  onErrorRef.current = onError;
1626
1954
  const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
1627
1955
  const {
@@ -1633,18 +1961,20 @@ function MejiroReaderInner(props, ref) {
1633
1961
  onError: reportError
1634
1962
  });
1635
1963
  const optionsPropKey = JSON.stringify(resolvedOptions);
1636
- const syncedOptionsPropKeyRef = useRef9(optionsPropKey);
1637
- const resolvedOptionsRef = useRef9(resolvedOptions);
1964
+ const syncedOptionsPropKeyRef = useRef10(optionsPropKey);
1965
+ const resolvedOptionsRef = useRef10(resolvedOptions);
1638
1966
  resolvedOptionsRef.current = resolvedOptions;
1639
- useEffect7(() => {
1967
+ useEffect8(() => {
1640
1968
  if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
1641
1969
  syncedOptionsPropKeyRef.current = optionsPropKey;
1642
1970
  void setOptions(resolvedOptionsRef.current);
1643
1971
  }, [optionsPropKey, setOptions]);
1972
+ const manuscriptSourceKey = manuscriptKey(manuscriptProp);
1644
1973
  const synthesizedEpub = useMemo4(() => {
1645
- if (manuscriptProp === void 0) return null;
1646
- return manuscriptToEpubBook(manuscriptProp, { dialect: dialectProp });
1647
- }, [manuscriptProp, dialectProp]);
1974
+ if (manuscriptSourceKey === void 0) return null;
1975
+ const chapters = JSON.parse(manuscriptSourceKey);
1976
+ return manuscriptToEpubBook(chapters, { dialect: dialectProp });
1977
+ }, [manuscriptSourceKey, dialectProp]);
1648
1978
  const epubCtx = useEpub({
1649
1979
  // `epub` / `manuscript` take precedence: skip the URL fetch when a parsed
1650
1980
  // book (or a synthesized manuscript book) is supplied.
@@ -1654,59 +1984,74 @@ function MejiroReaderInner(props, ref) {
1654
1984
  fetchEpub: fetchEpubFn,
1655
1985
  onLoad: (b) => {
1656
1986
  if (chapterProp == null) setChapterState(0);
1987
+ clearImagesRef.current();
1657
1988
  onLoad?.(b);
1658
1989
  }
1659
1990
  });
1660
1991
  const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
1661
1992
  const isManuscriptSource = manuscriptProp !== void 0;
1662
1993
  const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
1663
- useEffect7(() => {
1994
+ useEffect8(() => {
1664
1995
  if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
1665
1996
  }, [epubCtx.error]);
1666
- const pageGeometryRef = useRef9(resolvedGeometry);
1997
+ const pageGeometryRef = useRef10(resolvedGeometry);
1667
1998
  pageGeometryRef.current = resolvedGeometry;
1668
- const curSpreadIdxRef = useRef9(0);
1669
- const spreadIdxPropRef = useRef9(spreadIdxProp);
1999
+ const spreadAnchorRef = useRef10(() => null);
2000
+ const spreadIdxPropRef = useRef10(spreadIdxProp);
1670
2001
  spreadIdxPropRef.current = spreadIdxProp;
2002
+ const visibleAnchorRef = useRef10(
2003
+ null
2004
+ );
1671
2005
  const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef, {
1672
2006
  pageGeometry: () => pageGeometryRef.current,
1673
2007
  // Preserve the reading position across a reflow re-layout (size / option
1674
2008
  // changes), but only in uncontrolled mode — when `spreadIdx` is controlled
1675
2009
  // the host owns the position and the controlled-restore effect handles it.
1676
- capturePosition: (layout2) => spreadIdxPropRef.current != null ? null : layout2.anchorAt(curSpreadIdxRef.current, "right")
2010
+ capturePosition: (layout2) => {
2011
+ if (spreadIdxPropRef.current != null) return null;
2012
+ const visible = visibleAnchorRef.current;
2013
+ if (visible?.layout === layout2) return visible.anchor;
2014
+ return spreadAnchorRef.current();
2015
+ }
1677
2016
  });
1678
- const spreadChangedRef = useRef9(void 0);
2017
+ const spreadChangedRef = useRef10(void 0);
1679
2018
  spreadChangedRef.current = (i) => {
1680
2019
  onSpreadChange?.(i);
1681
2020
  onSpreadIdxChange?.(i);
1682
2021
  };
1683
2022
  const spreadCtx = useSpread(layoutCtx.layout, {
1684
2023
  enableKeyboard,
2024
+ single: effectiveSingle,
1685
2025
  onChange: (i) => spreadChangedRef.current?.(i)
1686
2026
  });
1687
- curSpreadIdxRef.current = spreadCtx.spreadIdx;
1688
- const setSpreadRef = useRef9(spreadCtx.setSpread);
2027
+ spreadAnchorRef.current = () => spreadCtx.anchorAt(spreadCtx.spreadIdx);
2028
+ const indexOfPageRef = useRef10(spreadCtx.indexOfPage);
2029
+ indexOfPageRef.current = spreadCtx.indexOfPage;
2030
+ const anchorSingleRef = useRef10(effectiveSingle);
2031
+ useLayoutEffect4(() => {
2032
+ const l = layoutCtx.layout;
2033
+ const flipped = anchorSingleRef.current !== effectiveSingle;
2034
+ anchorSingleRef.current = effectiveSingle;
2035
+ if (flipped && l && visibleAnchorRef.current?.layout === l) return;
2036
+ visibleAnchorRef.current = l ? { layout: l, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
2037
+ }, [layoutCtx.layout, spreadCtx.spreadIdx, spreadCtx.anchorAt, effectiveSingle]);
2038
+ const setSpreadRef = useRef10(spreadCtx.setSpread);
1689
2039
  setSpreadRef.current = spreadCtx.setSpread;
1690
2040
  useLayoutEffect4(() => {
1691
2041
  const anchor = layoutCtx.pendingRestore.current;
1692
2042
  if (!(anchor && layoutCtx.layout)) return;
1693
2043
  layoutCtx.pendingRestore.current = null;
1694
2044
  const loc = layoutCtx.layout.locateAnchor(anchor);
1695
- setSpreadRef.current(loc?.spreadIdx ?? 0);
2045
+ setSpreadRef.current(loc ? indexOfPageRef.current(loc.pageIdx) : 0);
2046
+ visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
1696
2047
  }, [layoutCtx.layout]);
1697
- const optionsKey = [
1698
- bookOptions.fontFamily,
1699
- bookOptions.fontSize,
1700
- bookOptions.lineSpacing,
1701
- bookOptions.mode,
1702
- bookOptions.enableHanging
1703
- ].join("|");
1704
- const optionsKeyRef = useRef9(optionsKey);
1705
- const bookOptionsRef = useRef9(bookOptions);
2048
+ const optionsKey = reflowOptionsKey(bookOptions);
2049
+ const optionsKeyRef = useRef10(optionsKey);
2050
+ const bookOptionsRef = useRef10(bookOptions);
1706
2051
  bookOptionsRef.current = bookOptions;
1707
- const recomputeRef = useRef9(layoutCtx.recompute);
2052
+ const recomputeRef = useRef10(layoutCtx.recompute);
1708
2053
  recomputeRef.current = layoutCtx.recompute;
1709
- useEffect7(() => {
2054
+ useEffect8(() => {
1710
2055
  if (optionsKeyRef.current === optionsKey) return;
1711
2056
  optionsKeyRef.current = optionsKey;
1712
2057
  const timer = setTimeout(() => {
@@ -1715,20 +2060,21 @@ function MejiroReaderInner(props, ref) {
1715
2060
  await setOptions({ ...bookOptionsRef.current });
1716
2061
  await recomputeRef.current({ blank: false });
1717
2062
  } catch (err) {
1718
- reportError(err instanceof Error ? err : new Error(String(err)));
2063
+ reportError(toError(err));
1719
2064
  }
1720
2065
  })();
1721
2066
  }, OPTIONS_DEBOUNCE_MS);
1722
2067
  return () => clearTimeout(timer);
1723
2068
  }, [optionsKey, setOptions, reportError]);
1724
2069
  const geometryKey = JSON.stringify(resolvedGeometry ?? null);
1725
- const geometryKeyRef = useRef9(geometryKey);
1726
- useEffect7(() => {
2070
+ const geometryKeyRef = useRef10(geometryKey);
2071
+ useEffect8(() => {
1727
2072
  if (geometryKeyRef.current === geometryKey) return;
1728
2073
  geometryKeyRef.current = geometryKey;
1729
2074
  void recomputeRef.current({ blank: false });
1730
2075
  }, [geometryKey]);
1731
- const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.spreadIdx, {
2076
+ const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.layoutSpreadIdx, {
2077
+ defaultX: IMAGE_DEFAULT_X,
1732
2078
  onUpdate: () => spreadCtx.refresh()
1733
2079
  });
1734
2080
  const annotationRects = useMemo4(() => {
@@ -1743,22 +2089,31 @@ function MejiroReaderInner(props, ref) {
1743
2089
  for (const rect of rects) result.push({ ...rect, color: annotation.color });
1744
2090
  }
1745
2091
  return result;
1746
- }, [annotations, layoutCtx.layout, chapter]);
1747
- const clearImagesRef = useRef9(imageCtx.clearImages);
2092
+ }, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
2093
+ const clearImagesRef = useRef10(imageCtx.clearImages);
1748
2094
  clearImagesRef.current = imageCtx.clearImages;
1749
- const onLoadRef = useRef9(onLoad);
2095
+ const onLoadRef = useRef10(onLoad);
1750
2096
  onLoadRef.current = onLoad;
1751
- useEffect7(() => {
2097
+ const eRef = useRef10(e);
2098
+ eRef.current = e;
2099
+ const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
2100
+ useEffect8(() => {
1752
2101
  if (!controlled) return;
1753
2102
  if (chapterIsUncontrolled) setChapterState(0);
1754
2103
  clearImagesRef.current();
1755
2104
  book.clearCache();
1756
- if (e) onLoadRef.current?.(e);
1757
- }, [controlled, e, book, chapterIsUncontrolled]);
1758
- const spreadGoToRef = useRef9(spreadCtx.goTo);
2105
+ if (eRef.current) onLoadRef.current?.(eRef.current);
2106
+ }, [controlled, sourceKey, book, chapterIsUncontrolled]);
2107
+ const imagesChapterRef = useRef10(chapter);
2108
+ useEffect8(() => {
2109
+ if (imagesChapterRef.current === chapter) return;
2110
+ imagesChapterRef.current = chapter;
2111
+ clearImagesRef.current();
2112
+ }, [chapter]);
2113
+ const spreadGoToRef = useRef10(spreadCtx.goTo);
1759
2114
  spreadGoToRef.current = spreadCtx.goTo;
1760
- const appliedSpreadIdxPropRef = useRef9(spreadIdxProp);
1761
- useEffect7(() => {
2115
+ const appliedSpreadIdxPropRef = useRef10(spreadIdxProp);
2116
+ useEffect8(() => {
1762
2117
  if (spreadIdxProp == null) {
1763
2118
  appliedSpreadIdxPropRef.current = spreadIdxProp;
1764
2119
  return;
@@ -1782,7 +2137,7 @@ function MejiroReaderInner(props, ref) {
1782
2137
  },
1783
2138
  [chapter, chapterProp, onChapterChange]
1784
2139
  );
1785
- const listenersRef = useRef9(/* @__PURE__ */ new Map());
2140
+ const listenersRef = useRef10(/* @__PURE__ */ new Map());
1786
2141
  const emit = useCallback6(
1787
2142
  (event, payload) => {
1788
2143
  const set = listenersRef.current.get(event);
@@ -1791,8 +2146,15 @@ function MejiroReaderInner(props, ref) {
1791
2146
  },
1792
2147
  []
1793
2148
  );
1794
- const pendingAnchorRef = useRef9(null);
1795
- const layout = layoutCtx.layout;
2149
+ const pendingAnchorRef = useRef10(null);
2150
+ const layoutOwnerRef = useRef10({
2151
+ layout: null,
2152
+ chapter
2153
+ });
2154
+ if (layoutOwnerRef.current.layout !== layoutCtx.layout) {
2155
+ layoutOwnerRef.current = { layout: layoutCtx.layout, chapter };
2156
+ }
2157
+ const layout = layoutOwnerRef.current.chapter === chapter ? layoutCtx.layout : null;
1796
2158
  const tryApplyPendingAnchor = useCallback6(() => {
1797
2159
  const pending = pendingAnchorRef.current;
1798
2160
  if (!pending) return;
@@ -1804,48 +2166,45 @@ function MejiroReaderInner(props, ref) {
1804
2166
  });
1805
2167
  if (!loc) return;
1806
2168
  pendingAnchorRef.current = null;
1807
- spreadGoToRef.current(loc.spreadIdx);
2169
+ spreadGoToRef.current(indexOfPageRef.current(loc.pageIdx));
1808
2170
  pending.resolve();
1809
2171
  }, [chapter, layout]);
1810
- useEffect7(() => {
2172
+ useEffect8(() => {
1811
2173
  tryApplyPendingAnchor();
1812
2174
  }, [tryApplyPendingAnchor]);
1813
- useEffect7(
2175
+ useEffect8(
1814
2176
  () => () => {
1815
2177
  pendingAnchorRef.current?.resolve();
1816
2178
  pendingAnchorRef.current = null;
1817
2179
  },
1818
2180
  []
1819
2181
  );
1820
- const didMountRef = useRef9(false);
1821
- const dwellRef = useRef9(null);
1822
- const onPageReadRef = useRef9(onPageRead);
2182
+ const onPageReadRef = useRef10(onPageRead);
1823
2183
  onPageReadRef.current = onPageRead;
1824
- const onChapterCompletedRef = useRef9(onChapterCompleted);
2184
+ const onChapterCompletedRef = useRef10(onChapterCompleted);
1825
2185
  onChapterCompletedRef.current = onChapterCompleted;
1826
- useEffect7(() => {
1827
- const now = typeof performance !== "undefined" ? performance.now() : Date.now();
1828
- if (didMountRef.current && dwellRef.current) {
1829
- onPageReadRef.current?.(dwellRef.current.anchor, now - dwellRef.current.ts);
1830
- }
1831
- if (layout) {
1832
- const inCh = layout.anchorAt(spreadCtx.spreadIdx, "right");
1833
- dwellRef.current = inCh ? { anchor: { chapter, ...inCh }, ts: now } : null;
1834
- } else {
1835
- dwellRef.current = null;
1836
- }
1837
- if (!didMountRef.current) {
1838
- didMountRef.current = true;
1839
- return;
1840
- }
1841
- emit("spreadChanged", { chapter, spreadIdx: spreadCtx.spreadIdx });
1842
- if (spreadCtx.totalSpreads > 0 && spreadCtx.spreadIdx === spreadCtx.totalSpreads - 1) {
1843
- emit("chapterFinished", { chapter });
1844
- onChapterCompletedRef.current?.(chapter);
1845
- }
1846
- }, [chapter, spreadCtx.spreadIdx, spreadCtx.totalSpreads, layout, emit]);
1847
- const prevTurningRef = useRef9(false);
1848
- useEffect7(() => {
2186
+ const trackLifecycleRef = useRef10(null);
2187
+ trackLifecycleRef.current ??= createLifecycleTracker({
2188
+ spreadChanged: (ch, spreadIdx) => emit("spreadChanged", { chapter: ch, spreadIdx }),
2189
+ chapterFinished: (ch) => {
2190
+ emit("chapterFinished", { chapter: ch });
2191
+ onChapterCompletedRef.current?.(ch);
2192
+ },
2193
+ pageRead: (anchor, dwellMs) => onPageReadRef.current?.(anchor, dwellMs)
2194
+ });
2195
+ useEffect8(() => {
2196
+ const view = layout ? { total: spreadCtx.totalSpreads, anchor: spreadCtx.anchorAt(spreadCtx.spreadIdx) } : null;
2197
+ trackLifecycleRef.current?.(chapter, spreadCtx.spreadIdx, view, spreadIdxProp);
2198
+ }, [
2199
+ chapter,
2200
+ spreadCtx.spreadIdx,
2201
+ spreadCtx.totalSpreads,
2202
+ spreadCtx.anchorAt,
2203
+ layout,
2204
+ spreadIdxProp
2205
+ ]);
2206
+ const prevTurningRef = useRef10(false);
2207
+ useEffect8(() => {
1849
2208
  if (spreadCtx.turning && !prevTurningRef.current) {
1850
2209
  emit("turnStart", { from: spreadCtx.spreadIdx });
1851
2210
  } else if (!spreadCtx.turning && prevTurningRef.current) {
@@ -1874,23 +2233,14 @@ function MejiroReaderInner(props, ref) {
1874
2233
  }),
1875
2234
  getAnchor: () => {
1876
2235
  if (!layout) return null;
1877
- const inCh = layout.anchorAt(spreadCtx.spreadIdx, "right");
2236
+ const inCh = spreadCtx.anchorAt(spreadCtx.spreadIdx);
1878
2237
  return inCh ? { chapter, ...inCh } : null;
1879
2238
  },
1880
2239
  getVisibleRange: () => {
1881
2240
  if (!layout) return null;
1882
- const start = layout.anchorAt(spreadCtx.spreadIdx, "right");
2241
+ const start = spreadCtx.anchorAt(spreadCtx.spreadIdx);
1883
2242
  if (!start) return null;
1884
- const next = layout.anchorAt(spreadCtx.spreadIdx + 1, "right");
1885
- let end;
1886
- if (next) {
1887
- end = next;
1888
- } else {
1889
- const ch = e?.chapters[chapter];
1890
- const lastP = (ch?.paragraphs.length ?? 1) - 1;
1891
- const lastText = ch?.paragraphs[lastP]?.text ?? "";
1892
- end = { paragraph: Math.max(0, lastP), charIndex: [...lastText].length };
1893
- }
2243
+ const end = spreadCtx.anchorAt(spreadCtx.spreadIdx + 1) ?? layout.endAnchor() ?? start;
1894
2244
  return {
1895
2245
  start: { chapter, ...start },
1896
2246
  end: { chapter, ...end }
@@ -1915,10 +2265,10 @@ function MejiroReaderInner(props, ref) {
1915
2265
  spreadCtx.spreadIdx,
1916
2266
  spreadCtx.totalPages,
1917
2267
  spreadCtx.totalSpreads,
2268
+ spreadCtx.anchorAt,
1918
2269
  onChapter,
1919
2270
  chapter,
1920
2271
  layout,
1921
- e,
1922
2272
  setOptions,
1923
2273
  tryApplyPendingAnchor
1924
2274
  ]
@@ -1970,7 +2320,7 @@ function MejiroReaderInner(props, ref) {
1970
2320
  MejiroStats,
1971
2321
  {
1972
2322
  chapter: e?.chapters[chapter] ?? null,
1973
- totalPages: layoutCtx.layout?.totalPages ?? 0,
2323
+ totalPages: spreadCtx.totalPages,
1974
2324
  elapsedMs: layoutCtx.elapsedMs,
1975
2325
  fontLabel
1976
2326
  }
@@ -1989,7 +2339,12 @@ function MejiroReaderInner(props, ref) {
1989
2339
  {
1990
2340
  type: "button",
1991
2341
  className: `mejiro-reader-btn${imageCtx.hasImages ? " is-active" : ""}`,
1992
- onClick: () => imageCtx.addImage(),
2342
+ onClick: () => imageCtx.addImage(
2343
+ singlePageImagePlacement(
2344
+ mode === "paginated" ? spreadCtx.singleSide : null,
2345
+ layoutCtx.pageWidth
2346
+ )
2347
+ ),
1993
2348
  children: resolvedMessages.imageButton
1994
2349
  }
1995
2350
  ),
@@ -2007,7 +2362,7 @@ function MejiroReaderInner(props, ref) {
2007
2362
  )
2008
2363
  ] })
2009
2364
  ] }) : null;
2010
- const currentSpread = spreadCtx.spreadIdx;
2365
+ const currentSpread = spreadCtx.layoutSpreadIdx;
2011
2366
  const rightPage = currentSpread * 2 + 1;
2012
2367
  const leftPage = currentSpread * 2 + 2;
2013
2368
  const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
@@ -2051,18 +2406,29 @@ function MejiroReaderInner(props, ref) {
2051
2406
  fontFamily: bookOptions.fontFamily,
2052
2407
  fontSize: bookOptions.fontSize,
2053
2408
  lineSpacing: bookOptions.lineSpacing,
2054
- scrollToPage: spreadCtx.spreadIdx * 2,
2409
+ scrollToPage: scrollTargetPage(
2410
+ spreadCtx,
2411
+ userScroll?.layout === layoutCtx.layout ? userScroll.page : null
2412
+ ),
2055
2413
  onVisiblePageChange: (pageIdx, source) => {
2056
2414
  if (source === "programmatic") return;
2057
- const target = Math.floor(pageIdx / 2);
2415
+ if (layoutCtx.layout) setUserScroll({ layout: layoutCtx.layout, page: pageIdx });
2416
+ const target = spreadCtx.indexOfPage(pageIdx);
2058
2417
  if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
2059
- }
2418
+ },
2419
+ selectionRects: annotationRects.length ? annotationRects : void 0,
2420
+ spreadIdx: spreadCtx.layoutSpreadIdx,
2421
+ images: imageCtx.currentImages,
2422
+ onImagePointerDown: imageCtx.onOverlayPointerDown,
2423
+ onImageResizePointerDown: imageCtx.onResizePointerDown,
2424
+ onImageClose: imageCtx.removeImage
2060
2425
  }
2061
2426
  ),
2062
2427
  layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
2063
2428
  MejiroSpread,
2064
2429
  {
2065
2430
  singlePage: effectiveSingle,
2431
+ singleSide: spreadCtx.singleSide ?? void 0,
2066
2432
  onSwipe: (dir) => dir === "next" ? spreadCtx.next() : spreadCtx.prev(),
2067
2433
  onSurfaceTap: enableSurfaceTap ? () => setChromeHidden((v) => !v) : void 0,
2068
2434
  spread: spreadCtx.spread,
@@ -2094,7 +2460,7 @@ function MejiroReaderInner(props, ref) {
2094
2460
  onImagePointerDown: imageCtx.onOverlayPointerDown,
2095
2461
  onImageResizePointerDown: imageCtx.onResizePointerDown,
2096
2462
  onImageClose: imageCtx.removeImage,
2097
- spreadIdx: spreadCtx.spreadIdx,
2463
+ spreadIdx: spreadCtx.layoutSpreadIdx,
2098
2464
  selectionRects: annotationRects.length ? annotationRects : void 0
2099
2465
  },
2100
2466
  `${chapter}-${spreadCtx.spreadIdx}-${layoutCtx.pageWidth}x${layoutCtx.pageHeight}`
@@ -2178,21 +2544,22 @@ function MejiroEditor({
2178
2544
  onError
2179
2545
  }) {
2180
2546
  const messages = useI18n();
2181
- const imageInputRef = useRef10(null);
2182
- const textareaRef = useRef10(null);
2183
- const [editor, setEditor] = useState8(null);
2184
- const [loading, setLoading] = useState8(false);
2185
- const [error, setError] = useState8(null);
2186
- const [revision, setRevision] = useState8(0);
2187
- const [selection, setSelection] = useState8({ chapter: 0, paragraph: 0 });
2188
- const [text, setText] = useState8("");
2189
- const [rubyStart, setRubyStart] = useState8(0);
2190
- const [rubyEnd, setRubyEnd] = useState8(1);
2191
- const [rubyText, setRubyText] = useState8("");
2192
- const loadRequestIdRef = useRef10(0);
2193
- const onLoadRef = useRef10(onLoad);
2194
- const onErrorRef = useRef10(onError);
2195
- const limitsRef = useRef10(limits);
2547
+ const imageInputRef = useRef11(null);
2548
+ const textareaRef = useRef11(null);
2549
+ const readerRef = useRef11(null);
2550
+ const [editor, setEditor] = useState9(null);
2551
+ const [loading, setLoading] = useState9(false);
2552
+ const [error, setError] = useState9(null);
2553
+ const [revision, setRevision] = useState9(0);
2554
+ const [selection, setSelection] = useState9({ chapter: 0, paragraph: 0 });
2555
+ const [text, setText] = useState9("");
2556
+ const [rubyStart, setRubyStart] = useState9(0);
2557
+ const [rubyEnd, setRubyEnd] = useState9(1);
2558
+ const [rubyText, setRubyText] = useState9("");
2559
+ const loadRequestIdRef = useRef11(0);
2560
+ const onLoadRef = useRef11(onLoad);
2561
+ const onErrorRef = useRef11(onError);
2562
+ const limitsRef = useRef11(limits);
2196
2563
  limitsRef.current = limits;
2197
2564
  const book = editor?.book ?? null;
2198
2565
  const chapter = book?.chapters[selection.chapter] ?? null;
@@ -2202,48 +2569,49 @@ function MejiroEditor({
2202
2569
  void revision;
2203
2570
  return book ? cloneEditableEpubBook(book) : null;
2204
2571
  }, [book, revision]);
2205
- useEffect8(() => {
2572
+ useEffect9(() => {
2206
2573
  onLoadRef.current = onLoad;
2207
2574
  }, [onLoad]);
2208
- useEffect8(() => {
2575
+ useEffect9(() => {
2209
2576
  onErrorRef.current = onError;
2210
2577
  }, [onError]);
2211
- const loadBufferForRequest = useCallback7(async (buffer, requestId) => {
2212
- setLoading(true);
2213
- setError(null);
2214
- try {
2215
- const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
2216
- if (requestId !== loadRequestIdRef.current) return;
2217
- setEditor(next);
2218
- setSelection({ chapter: 0, paragraph: 0 });
2219
- setRevision((value) => value + 1);
2220
- onLoadRef.current?.(next);
2221
- } catch (err) {
2222
- if (requestId === loadRequestIdRef.current) {
2223
- const nextError = err instanceof Error ? err : new Error(String(err));
2224
- setError(nextError);
2225
- onErrorRef.current?.(nextError);
2226
- }
2227
- } finally {
2228
- if (requestId === loadRequestIdRef.current) setLoading(false);
2229
- }
2578
+ const reportError = useCallback7((nextError) => {
2579
+ setError(nextError);
2580
+ onErrorRef.current?.(nextError);
2230
2581
  }, []);
2582
+ const loadBufferForRequest = useCallback7(
2583
+ async (buffer, requestId) => {
2584
+ setLoading(true);
2585
+ setError(null);
2586
+ try {
2587
+ const next = await EditableEpub.load(buffer, { limits: limitsRef.current });
2588
+ if (requestId !== loadRequestIdRef.current) return;
2589
+ setEditor(next);
2590
+ setSelection({ chapter: 0, paragraph: 0 });
2591
+ setRevision((value) => value + 1);
2592
+ onLoadRef.current?.(next);
2593
+ } catch (err) {
2594
+ if (requestId === loadRequestIdRef.current) reportError(toError(err));
2595
+ } finally {
2596
+ if (requestId === loadRequestIdRef.current) setLoading(false);
2597
+ }
2598
+ },
2599
+ [reportError]
2600
+ );
2231
2601
  async function loadFile(file) {
2232
2602
  const requestId = ++loadRequestIdRef.current;
2233
2603
  setLoading(true);
2234
2604
  setError(null);
2235
2605
  try {
2236
- await loadBufferForRequest(await file.arrayBuffer(), requestId);
2606
+ await loadBufferForRequest(await readEpubFile(file, limitsRef.current), requestId);
2237
2607
  } catch (err) {
2238
2608
  if (requestId === loadRequestIdRef.current) {
2239
- const nextError = err instanceof Error ? err : new Error(String(err));
2240
- setError(nextError);
2241
- onErrorRef.current?.(nextError);
2609
+ reportError(toError(err));
2242
2610
  setLoading(false);
2243
2611
  }
2244
2612
  }
2245
2613
  }
2246
- useEffect8(() => {
2614
+ useEffect9(() => {
2247
2615
  if (!epubUrl) return;
2248
2616
  let cancelled = false;
2249
2617
  const requestId = ++loadRequestIdRef.current;
@@ -2251,17 +2619,11 @@ function MejiroEditor({
2251
2619
  setLoading(true);
2252
2620
  setError(null);
2253
2621
  try {
2254
- const res = await fetch(epubUrl);
2255
- if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
2256
- const buffer = await res.arrayBuffer();
2622
+ const buffer = await fetchEpubBuffer(epubUrl);
2257
2623
  if (cancelled || requestId !== loadRequestIdRef.current) return;
2258
2624
  await loadBufferForRequest(buffer, requestId);
2259
2625
  } catch (err) {
2260
- if (!cancelled && requestId === loadRequestIdRef.current) {
2261
- const nextError = err instanceof Error ? err : new Error(String(err));
2262
- setError(nextError);
2263
- onErrorRef.current?.(nextError);
2264
- }
2626
+ if (!cancelled && requestId === loadRequestIdRef.current) reportError(toError(err));
2265
2627
  } finally {
2266
2628
  if (!cancelled && requestId === loadRequestIdRef.current) setLoading(false);
2267
2629
  }
@@ -2269,13 +2631,32 @@ function MejiroEditor({
2269
2631
  return () => {
2270
2632
  cancelled = true;
2271
2633
  };
2272
- }, [epubUrl, loadBufferForRequest]);
2273
- useEffect8(() => {
2274
- setText(paragraph?.text ?? "");
2634
+ }, [epubUrl, loadBufferForRequest, reportError]);
2635
+ const syncedRef = useRef11(
2636
+ null
2637
+ );
2638
+ useEffect9(() => {
2639
+ const nextText = paragraph?.text ?? "";
2640
+ const target = `${selection.chapter}:${selection.paragraph}`;
2641
+ const prev = syncedRef.current;
2642
+ syncedRef.current = { editor, target, text: nextText };
2643
+ if (prev && prev.editor === editor && prev.target === target) {
2644
+ setText((current) => current === prev.text ? nextText : current);
2645
+ return;
2646
+ }
2647
+ setText(nextText);
2275
2648
  setRubyStart(0);
2276
- setRubyEnd(Math.min(1, [...paragraph?.text ?? ""].length));
2649
+ setRubyEnd(Math.min(1, [...nextText].length));
2277
2650
  setRubyText("");
2278
- }, [paragraph]);
2651
+ }, [editor, selection.chapter, selection.paragraph, paragraph]);
2652
+ useEffect9(() => {
2653
+ if (!previewBook) return;
2654
+ void readerRef.current?.goToAnchor({
2655
+ chapter: selection.chapter,
2656
+ paragraph: selection.paragraph,
2657
+ charIndex: 0
2658
+ });
2659
+ }, [previewBook, selection.chapter, selection.paragraph]);
2279
2660
  function selectParagraph(chapterIndex, paragraphIndex) {
2280
2661
  if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
2281
2662
  if (textDirty) applyText();
@@ -2322,39 +2703,49 @@ function MejiroEditor({
2322
2703
  setRevision((value) => value + 1);
2323
2704
  }
2324
2705
  async function addImage(file) {
2325
- if (!(editor && chapter)) return;
2326
- editor.addImage(selection.chapter, {
2327
- filename: file.name,
2328
- mediaType: file.type || "application/octet-stream",
2329
- data: await file.arrayBuffer(),
2330
- alt: file.name,
2331
- afterBlockId: paragraphBlockId(chapter, selection.paragraph)
2332
- });
2333
- setRevision((value) => value + 1);
2706
+ await withErrorReporting(async () => {
2707
+ if (!(editor && chapter)) return;
2708
+ const chapterIndex = selection.chapter;
2709
+ const afterBlockId = paragraphBlockId(chapter, selection.paragraph);
2710
+ const data = await file.arrayBuffer();
2711
+ editor.addImage(chapterIndex, {
2712
+ filename: file.name,
2713
+ mediaType: file.type || "application/octet-stream",
2714
+ data,
2715
+ alt: file.name,
2716
+ afterBlockId
2717
+ });
2718
+ setRevision((value) => value + 1);
2719
+ }, reportError);
2334
2720
  }
2335
2721
  async function exportEpub() {
2336
- if (!editor) return;
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);
2340
- if (exportPolicy?.encrypt) buffer = await exportPolicy.encrypt(buffer);
2341
- const decision = await onBeforeExport?.(buffer);
2342
- onExport?.(buffer);
2343
- if (decision === false) return;
2344
- if (exportPolicy?.allowDownload === false) return;
2345
- const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2346
- const a = document.createElement("a");
2347
- a.href = url;
2348
- a.download = `${editor.title || "edited"}.epub`;
2349
- a.click();
2350
- URL.revokeObjectURL(url);
2722
+ await withErrorReporting(async () => {
2723
+ if (!editor) return;
2724
+ const watermark = exportPolicy?.watermark;
2725
+ const source = watermark ? watermarkedBook(editor.book, watermark) : editor.book;
2726
+ let buffer = await exportEditableEpub(source, assetResolver ? { assetResolver } : void 0);
2727
+ if (exportPolicy?.encrypt) buffer = await exportPolicy.encrypt(buffer);
2728
+ const decision = await onBeforeExport?.(buffer);
2729
+ onExport?.(buffer);
2730
+ const allowDownload = exportPolicy ? exportPolicy.allowDownload !== false : decision !== false;
2731
+ if (!allowDownload) return;
2732
+ const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2733
+ const a = document.createElement("a");
2734
+ a.href = url;
2735
+ a.download = `${editor.title || "edited"}.epub`;
2736
+ a.click();
2737
+ URL.revokeObjectURL(url);
2738
+ }, reportError);
2351
2739
  }
2352
2740
  return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
2353
2741
  /* @__PURE__ */ jsxs9("main", { className: "mejiro-editor-preview", children: [
2354
2742
  previewBook ? /* @__PURE__ */ jsx14(
2355
2743
  MejiroReader,
2356
2744
  {
2745
+ ref: readerRef,
2357
2746
  epub: previewBook,
2747
+ chapter: selection.chapter,
2748
+ onChapterChange: (index) => selectParagraph(index, 0),
2358
2749
  fonts,
2359
2750
  subtitle: messages.editorPreviewSubtitle,
2360
2751
  chapterNavMode: "panel",
@@ -2472,7 +2863,7 @@ function paragraphBlockId(chapter, paragraphIndex) {
2472
2863
 
2473
2864
  // src/MejiroManuscriptEditor.tsx
2474
2865
  import { EpubProject } from "@libraz/mejiro/epub";
2475
- import { useCallback as useCallback9, useEffect as useEffect10, useRef as useRef13, useState as useState11 } from "react";
2866
+ import { useCallback as useCallback9, useEffect as useEffect11, useMemo as useMemo7, useRef as useRef14, useState as useState12 } from "react";
2476
2867
 
2477
2868
  // src/MejiroNotationHighlighter.tsx
2478
2869
  import { tokenizeManuscriptSource } from "@libraz/mejiro";
@@ -2480,8 +2871,8 @@ import {
2480
2871
  forwardRef as forwardRef2,
2481
2872
  useImperativeHandle as useImperativeHandle2,
2482
2873
  useMemo as useMemo6,
2483
- useRef as useRef11,
2484
- useState as useState9
2874
+ useRef as useRef12,
2875
+ useState as useState10
2485
2876
  } from "react";
2486
2877
  import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2487
2878
  var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2({
@@ -2497,11 +2888,11 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
2497
2888
  onCompositionEnd,
2498
2889
  ...rest
2499
2890
  }, ref) {
2500
- const textareaRef = useRef11(null);
2501
- const overlayRef = useRef11(null);
2891
+ const textareaRef = useRef12(null);
2892
+ const overlayRef = useRef12(null);
2502
2893
  useImperativeHandle2(ref, () => textareaRef.current, []);
2503
- const [isComposing, setIsComposing] = useState9(false);
2504
- const lastSegmentsRef = useRef11(null);
2894
+ const [isComposing, setIsComposing] = useState10(false);
2895
+ const lastSegmentsRef = useRef12(null);
2505
2896
  const segments = useMemo6(() => {
2506
2897
  if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
2507
2898
  const next = buildSegments(value, dialect);
@@ -2590,10 +2981,11 @@ function buildSegments(text, dialect) {
2590
2981
  }
2591
2982
 
2592
2983
  // src/useManuscriptDraft.ts
2593
- import { useCallback as useCallback8, useEffect as useEffect9, useRef as useRef12, useState as useState10 } from "react";
2984
+ import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef13, useState as useState11 } from "react";
2594
2985
  var DEFAULT_DELAY = 800;
2595
- function defaultChapter(index, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
2596
- return { id: `chapter-${Date.now()}-${index}`, title: titleFor(index), body: bodyFor(index) };
2986
+ function defaultChapter(existing, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
2987
+ const index = existing.length;
2988
+ return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
2597
2989
  }
2598
2990
  function useManuscriptDraft(options = {}) {
2599
2991
  const {
@@ -2604,28 +2996,30 @@ function useManuscriptDraft(options = {}) {
2604
2996
  defaultChapterTitle,
2605
2997
  defaultChapterBody
2606
2998
  } = options;
2607
- const titleForRef = useRef12(defaultChapterTitle);
2999
+ const titleForRef = useRef13(defaultChapterTitle);
2608
3000
  titleForRef.current = defaultChapterTitle;
2609
- const bodyForRef = useRef12(defaultChapterBody);
3001
+ const bodyForRef = useRef13(defaultChapterBody);
2610
3002
  bodyForRef.current = defaultChapterBody;
2611
- const [chapters, setChaptersState] = useState10(
2612
- () => options.initialChapters?.length ? options.initialChapters : [defaultChapter(0, defaultChapterTitle, defaultChapterBody)]
3003
+ const [chapters, setChaptersState] = useState11(
3004
+ () => options.initialChapters?.length ? options.initialChapters : [defaultChapter([], defaultChapterTitle, defaultChapterBody)]
2613
3005
  );
2614
- const [selected, setSelectedState] = useState10(0);
2615
- const [autosaveError, setAutosaveError] = useState10(null);
2616
- const saveRef = useRef12(onAutosave);
3006
+ const [selected, setSelectedState] = useState11(0);
3007
+ const [autosaveError, setAutosaveError] = useState11(null);
3008
+ const saveRef = useRef13(onAutosave);
2617
3009
  saveRef.current = onAutosave;
2618
- const payloadRef = useRef12(autosavePayload);
3010
+ const payloadRef = useRef13(autosavePayload);
2619
3011
  payloadRef.current = autosavePayload;
2620
- const chaptersRef = useRef12(chapters);
3012
+ const chaptersRef = useRef13(chapters);
2621
3013
  chaptersRef.current = chapters;
2622
- const selectedRef = useRef12(selected);
3014
+ const selectedRef = useRef13(selected);
2623
3015
  selectedRef.current = selected;
2624
- const didMountRef = useRef12(false);
2625
- const dirtyRef = useRef12(false);
2626
- const mountedRef = useRef12(true);
2627
- const revisionRef = useRef12(0);
2628
- const inFlightRevisionRef = useRef12(-1);
3016
+ const trackerRef = useRef13(null);
3017
+ if (!trackerRef.current)
3018
+ 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);
2629
3023
  const flushAutosave = useCallback8(() => {
2630
3024
  const callback = saveRef.current;
2631
3025
  if (!(callback && dirtyRef.current) || inFlightRevisionRef.current === revisionRef.current) {
@@ -2633,38 +3027,36 @@ function useManuscriptDraft(options = {}) {
2633
3027
  }
2634
3028
  const revision = revisionRef.current;
2635
3029
  inFlightRevisionRef.current = revision;
2636
- const payload = payloadRef.current ? payloadRef.current(chaptersRef.current) : chaptersRef.current;
3030
+ const plain = snapshotChapters(chaptersRef.current);
3031
+ const payload = payloadRef.current ? payloadRef.current(plain) : plain;
2637
3032
  void Promise.resolve(callback(payload)).then(() => {
2638
3033
  if (revisionRef.current === revision) dirtyRef.current = false;
2639
3034
  }).catch((err) => {
2640
3035
  if (!mountedRef.current) return;
2641
- setAutosaveError(err instanceof Error ? err : new Error(String(err)));
3036
+ setAutosaveError(toError(err));
2642
3037
  }).finally(() => {
2643
3038
  if (inFlightRevisionRef.current === revision) inFlightRevisionRef.current = -1;
2644
3039
  });
2645
3040
  }, []);
2646
- useEffect9(() => {
2647
- if (!didMountRef.current) {
2648
- didMountRef.current = true;
2649
- return void 0;
3041
+ useEffect10(() => {
3042
+ if (trackerRef.current?.changed(chapters, autosaveKey ?? "")) {
3043
+ dirtyRef.current = true;
3044
+ revisionRef.current += 1;
3045
+ setAutosaveError(null);
2650
3046
  }
2651
- if (!saveRef.current) return void 0;
2652
- dirtyRef.current = true;
2653
- revisionRef.current += 1;
2654
- setAutosaveError(null);
3047
+ if (!(saveRef.current && dirtyRef.current)) return void 0;
2655
3048
  const timer = setTimeout(() => {
2656
3049
  flushAutosave();
2657
3050
  }, autosaveDelay);
2658
3051
  return () => clearTimeout(timer);
2659
3052
  }, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
2660
- useEffect9(() => {
3053
+ useEffect10(() => {
2661
3054
  mountedRef.current = true;
2662
- const handleBeforeUnload = () => flushAutosave();
2663
- window.addEventListener("beforeunload", handleBeforeUnload);
3055
+ const detachPageHide = flushOnPageHide(flushAutosave);
2664
3056
  return () => {
2665
3057
  flushAutosave();
2666
3058
  mountedRef.current = false;
2667
- window.removeEventListener("beforeunload", handleBeforeUnload);
3059
+ detachPageHide();
2668
3060
  };
2669
3061
  }, [flushAutosave]);
2670
3062
  const commit = useCallback8((nextChapters, nextSelected) => {
@@ -2680,7 +3072,7 @@ function useManuscriptDraft(options = {}) {
2680
3072
  }, []);
2681
3073
  const setChapters = useCallback8(
2682
3074
  (next) => {
2683
- const normalized = next.length ? next : [defaultChapter(0, titleForRef.current, bodyForRef.current)];
3075
+ const normalized = next.length ? next : [defaultChapter([], titleForRef.current, bodyForRef.current)];
2684
3076
  const prev = selectedRef.current;
2685
3077
  const selectedId = chaptersRef.current[prev]?.id;
2686
3078
  const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
@@ -2701,7 +3093,7 @@ function useManuscriptDraft(options = {}) {
2701
3093
  const addChapter = useCallback8(
2702
3094
  (chapter = {}) => {
2703
3095
  const current = chaptersRef.current;
2704
- const generated = defaultChapter(current.length, titleForRef.current, bodyForRef.current);
3096
+ const generated = defaultChapter(current, titleForRef.current, bodyForRef.current);
2705
3097
  const next = [
2706
3098
  ...current,
2707
3099
  {
@@ -2815,24 +3207,24 @@ function MejiroManuscriptEditor({
2815
3207
  panelSide = "right"
2816
3208
  }) {
2817
3209
  const messages = useI18n();
2818
- const coverInputRef = useRef13(null);
2819
- const bodyTextareaRef = useRef13(null);
3210
+ const coverInputRef = useRef14(null);
3211
+ const bodyTextareaRef = useRef14(null);
2820
3212
  const titleControlled = onTitleChange !== void 0;
2821
3213
  const authorControlled = onAuthorChange !== void 0;
2822
3214
  const coverControlled = onCoverChange !== void 0;
2823
- const [titleState, setTitleState] = useState11(titleProp ?? messages.manuscriptDefaultTitle);
2824
- const [authorState, setAuthorState] = useState11(authorProp ?? "");
2825
- const [coverState, setCoverState] = useState11(coverProp ?? null);
3215
+ const [titleState, setTitleState] = useState12(titleProp ?? messages.manuscriptDefaultTitle);
3216
+ const [authorState, setAuthorState] = useState12(authorProp ?? "");
3217
+ const [coverState, setCoverState] = useState12(coverProp ?? null);
2826
3218
  const title = titleControlled ? titleProp ?? "" : titleState;
2827
3219
  const author = authorControlled ? authorProp ?? "" : authorState;
2828
3220
  const cover = coverControlled ? coverProp ?? null : coverState;
2829
- useEffect10(() => {
3221
+ useEffect11(() => {
2830
3222
  if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
2831
3223
  }, [titleControlled, titleProp]);
2832
- useEffect10(() => {
3224
+ useEffect11(() => {
2833
3225
  if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
2834
3226
  }, [authorControlled, authorProp]);
2835
- useEffect10(() => {
3227
+ useEffect11(() => {
2836
3228
  if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
2837
3229
  }, [coverControlled, coverProp]);
2838
3230
  const setTitle = useCallback9(
@@ -2874,8 +3266,8 @@ function MejiroManuscriptEditor({
2874
3266
  reorderChapters
2875
3267
  } = draft;
2876
3268
  const current = chapters[selected] ?? chapters[0];
2877
- const [draggingIndex, setDraggingIndex] = useState11(null);
2878
- const [exportError, setExportError] = useState11(null);
3269
+ const [draggingIndex, setDraggingIndex] = useState12(null);
3270
+ const [exportError, setExportError] = useState12(null);
2879
3271
  function wrapSelection(open, close) {
2880
3272
  const el = bodyTextareaRef.current;
2881
3273
  if (!el) return;
@@ -2893,57 +3285,69 @@ function MejiroManuscriptEditor({
2893
3285
  bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
2894
3286
  });
2895
3287
  }
3288
+ const [identifier] = useState12(() => `urn:uuid:${crypto.randomUUID()}`);
2896
3289
  const exportEpub = useCallback9(async () => {
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
- }))
2906
- });
2907
- if (cover) {
2908
- project.setCover({
2909
- href: coverAssetHref(cover),
2910
- mediaType: cover.type || void 0,
2911
- data: await cover.arrayBuffer()
3290
+ await withErrorReporting(
3291
+ async () => {
3292
+ const project = EpubProject.fromManuscript({
3293
+ metadata: { title, author: author || void 0, identifier },
3294
+ dialect,
3295
+ chapters: chapters.map((chapter) => ({
3296
+ id: chapter.id,
3297
+ title: chapter.title || messages.untitled,
3298
+ body: chapter.body
3299
+ }))
2912
3300
  });
3301
+ if (cover) {
3302
+ project.setCover({
3303
+ href: coverAssetHref(cover),
3304
+ mediaType: cover.type || void 0,
3305
+ data: await cover.arrayBuffer()
3306
+ });
3307
+ }
3308
+ const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
3309
+ setExportError(null);
3310
+ onExport?.(buffer);
3311
+ downloadEpub(buffer, title);
3312
+ },
3313
+ (error) => {
3314
+ setExportError(error);
3315
+ onError?.(error);
2913
3316
  }
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);
2922
- }
3317
+ );
2923
3318
  }, [
2924
3319
  assetResolver,
2925
3320
  author,
2926
3321
  chapters,
2927
3322
  cover,
2928
3323
  dialect,
3324
+ identifier,
2929
3325
  messages.untitled,
2930
3326
  onError,
2931
3327
  onExport,
2932
3328
  title
2933
3329
  ]);
3330
+ const previewManuscript = useMemo7(
3331
+ () => chapters.map((chapter) => ({
3332
+ id: chapter.id,
3333
+ title: chapter.title || messages.untitled,
3334
+ body: chapter.body
3335
+ })),
3336
+ [chapters, messages.untitled]
3337
+ );
2934
3338
  return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor mejiro-manuscript-editor", "data-panel-side": panelSide, children: [
2935
3339
  /* @__PURE__ */ jsx16("main", { className: "mejiro-editor-preview", children: /* @__PURE__ */ jsx16(
2936
3340
  MejiroReader,
2937
3341
  {
2938
- subtitle: messages.manuscriptPreviewSubtitle,
2939
- chapterNavMode: "panel",
2940
- enableSurfaceTap: false,
2941
- ...previewProps,
2942
- manuscript: chapters.map((chapter) => ({
2943
- id: chapter.id,
2944
- title: chapter.title || messages.untitled,
2945
- body: chapter.body
2946
- })),
3342
+ ...mergeDefined(
3343
+ {
3344
+ subtitle: messages.manuscriptPreviewSubtitle,
3345
+ chapterNavMode: "panel",
3346
+ enableSurfaceTap: false
3347
+ },
3348
+ previewProps ?? {}
3349
+ ),
3350
+ manuscript: previewManuscript,
2947
3351
  dialect,
2948
3352
  fonts,
2949
3353
  chapter: selected,
@@ -3097,12 +3501,15 @@ function MejiroShelf({
3097
3501
  }
3098
3502
 
3099
3503
  // src/MejiroToc.tsx
3100
- import { useMemo as useMemo7, useState as useState12 } from "react";
3504
+ import { useMemo as useMemo8, useState as useState13 } from "react";
3101
3505
  import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
3102
3506
  function buildEntries(epub, messages) {
3103
3507
  return epub.chapters.map((ch, i) => {
3104
3508
  const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
3105
- const headings = ch.paragraphs.filter((p) => p.headingLevel && p.text.trim() && p.text.trim() !== title).map((p) => p.text.trim());
3509
+ const headings = ch.paragraphs.flatMap((p, paragraph) => {
3510
+ const text = p.text.trim();
3511
+ return p.headingLevel && text && text !== title ? [{ paragraph, text }] : [];
3512
+ });
3106
3513
  return { index: i, title, headings };
3107
3514
  });
3108
3515
  }
@@ -3117,13 +3524,13 @@ function MejiroToc({
3117
3524
  const messages = useI18n();
3118
3525
  const resolvedTitle = title ?? messages.tocTitle;
3119
3526
  const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
3120
- const [query, setQuery] = useState12("");
3121
- const entries = useMemo7(() => buildEntries(epub, messages), [epub, messages]);
3122
- const filtered = useMemo7(() => {
3527
+ const [query, setQuery] = useState13("");
3528
+ const entries = useMemo8(() => buildEntries(epub, messages), [epub, messages]);
3529
+ const filtered = useMemo8(() => {
3123
3530
  if (!query) return entries;
3124
3531
  const needle = query.toLowerCase();
3125
3532
  return entries.filter(
3126
- (e) => e.title.toLowerCase().includes(needle) || e.headings.some((h) => h.toLowerCase().includes(needle))
3533
+ (e) => e.title.toLowerCase().includes(needle) || e.headings.some((h) => h.text.toLowerCase().includes(needle))
3127
3534
  );
3128
3535
  }, [entries, query]);
3129
3536
  const activeIndex = currentAnchor?.chapter ?? -1;
@@ -3156,7 +3563,7 @@ function MejiroToc({
3156
3563
  ]
3157
3564
  }
3158
3565
  ),
3159
- entry.headings.length > 0 && /* @__PURE__ */ jsx18("ul", { className: "mejiro-toc-subheads", children: entry.headings.slice(0, 5).map((h) => /* @__PURE__ */ jsx18("li", { className: "mejiro-toc-subhead", children: h }, h)) })
3566
+ entry.headings.length > 0 && /* @__PURE__ */ jsx18("ul", { className: "mejiro-toc-subheads", children: entry.headings.slice(0, 5).map((h) => /* @__PURE__ */ jsx18("li", { className: "mejiro-toc-subhead", children: h.text }, h.paragraph)) })
3160
3567
  ] }, entry.index)) }),
3161
3568
  filtered.length === 0 && /* @__PURE__ */ jsx18("div", { className: "mejiro-toc-empty", role: "status", children: format(messages.tocEmpty, { query }) })
3162
3569
  ] });
@@ -3169,80 +3576,44 @@ import {
3169
3576
  serializeAnnotations,
3170
3577
  sortAnnotations
3171
3578
  } from "@libraz/mejiro";
3172
- import { useCallback as useCallback10, useEffect as useEffect11, useRef as useRef14, useState as useState13 } from "react";
3173
- function resolveDefaultStorage() {
3174
- if (typeof globalThis === "undefined") return null;
3175
- const w = globalThis.localStorage;
3176
- return w ?? null;
3579
+ import { useCallback as useCallback10, useEffect as useEffect12, useRef as useRef15, useState as useState14 } from "react";
3580
+ function parseSorted(raw) {
3581
+ return sortAnnotations(parseAnnotations(raw));
3177
3582
  }
3178
3583
  function useAnnotations(options) {
3179
3584
  const { key, throttleMs = 250 } = options;
3180
3585
  const storage = options.storage ?? resolveDefaultStorage();
3181
- const [annotations, setAnnotations] = useState13(() => {
3182
- if (!storage) return [];
3183
- try {
3184
- return sortAnnotations(parseAnnotations(storage.getItem(key)));
3185
- } catch {
3186
- return [];
3187
- }
3188
- });
3189
- const storageRef = useRef14(storage);
3586
+ const [annotations, setAnnotations] = useState14(
3587
+ () => readStorage(storage, key, parseSorted, [])
3588
+ );
3589
+ const storageRef = useRef15(storage);
3190
3590
  storageRef.current = storage;
3191
- const annotationsRef = useRef14(annotations);
3591
+ const annotationsRef = useRef15(annotations);
3192
3592
  const applyNext = useCallback10((next) => {
3193
3593
  annotationsRef.current = next;
3194
3594
  setAnnotations(next);
3195
3595
  }, []);
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;
3202
- }
3203
- const write = pendingWriteRef.current;
3204
- pendingWriteRef.current = null;
3205
- if (!write) return;
3206
- try {
3207
- write();
3208
- } catch {
3209
- }
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]);
3231
- const onChangeRef = useRef14(options.onChange);
3596
+ const pendingRef = useRef15(null);
3597
+ if (!pendingRef.current) pendingRef.current = createPendingWrite();
3598
+ const pending = pendingRef.current;
3599
+ useEffect12(() => flushOnPageHide(pending.flush), [pending]);
3600
+ useEffect12(() => {
3601
+ applyNext(readStorage(storageRef.current, key, parseSorted, []));
3602
+ return pending.flush;
3603
+ }, [key, applyNext, pending]);
3604
+ const onChangeRef = useRef15(options.onChange);
3232
3605
  onChangeRef.current = options.onChange;
3233
3606
  const commit = useCallback10(
3234
3607
  (next) => {
3235
3608
  const currentStorage = storageRef.current;
3236
3609
  if (currentStorage) {
3237
- cancelPending();
3238
- pendingWriteRef.current = () => {
3610
+ pending.schedule(() => {
3239
3611
  currentStorage.setItem(key, serializeAnnotations(next));
3240
- };
3241
- timerRef.current = setTimeout(flushPending, throttleMs);
3612
+ }, throttleMs);
3242
3613
  }
3243
3614
  onChangeRef.current?.(next);
3244
3615
  },
3245
- [key, throttleMs, cancelPending, flushPending]
3616
+ [key, throttleMs, pending]
3246
3617
  );
3247
3618
  const add = useCallback10(
3248
3619
  (input) => {
@@ -3274,7 +3645,7 @@ function useAnnotations(options) {
3274
3645
  const next = annotationsRef.current.map((annotation) => {
3275
3646
  if (annotation.id !== id) return annotation;
3276
3647
  changed = true;
3277
- return { ...annotation, ...patch, id };
3648
+ return { ...mergeDefined(annotation, patch), id };
3278
3649
  });
3279
3650
  if (!changed) return;
3280
3651
  const sorted = sortAnnotations(next);
@@ -3285,16 +3656,10 @@ function useAnnotations(options) {
3285
3656
  );
3286
3657
  const clear = useCallback10(() => {
3287
3658
  applyNext([]);
3288
- const currentStorage = storageRef.current;
3289
- if (currentStorage) {
3290
- cancelPending();
3291
- try {
3292
- currentStorage.removeItem(key);
3293
- } catch {
3294
- }
3295
- }
3659
+ pending.cancel();
3660
+ removeStorage(storageRef.current, key);
3296
3661
  onChangeRef.current?.([]);
3297
- }, [key, cancelPending, applyNext]);
3662
+ }, [key, pending, applyNext]);
3298
3663
  return { annotations, add, remove, update, clear };
3299
3664
  }
3300
3665
 
@@ -3304,26 +3669,32 @@ import {
3304
3669
  cloneEditableEpubBook as cloneEditableEpubBook2,
3305
3670
  EditableEpub as EditableEpub2
3306
3671
  } from "@libraz/mejiro/epub";
3307
- import { useCallback as useCallback11, useEffect as useEffect12, useMemo as useMemo8, useRef as useRef15, useState as useState14 } from "react";
3672
+ import { useCallback as useCallback11, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef16, useState as useState15 } from "react";
3308
3673
  function useEditableEpub(options = {}) {
3309
- const [editor, setEditor] = useState14(null);
3310
- const [loading, setLoading] = useState14(false);
3311
- const [exporting, setExporting] = useState14(false);
3312
- const [error, setError] = useState14(null);
3313
- const [revision, setRevision] = useState14(0);
3314
- const [selection, setSelectionState] = useState14({
3674
+ const [editor, setEditor] = useState15(null);
3675
+ const [loading, setLoading] = useState15(false);
3676
+ const [exporting, setExporting] = useState15(false);
3677
+ const [error, setError] = useState15(null);
3678
+ const [revision, setRevision] = useState15(0);
3679
+ const [selection, setSelectionState] = useState15({
3315
3680
  chapter: 0,
3316
3681
  paragraph: 0
3317
3682
  });
3318
- const requestIdRef = useRef15(0);
3319
- const onLoadRef = useRef15(options.onLoad);
3320
- const onErrorRef = useRef15(options.onError);
3321
- const onExportRef = useRef15(options.onExport);
3322
- const limitsRef = useRef15(options.limits);
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);
3323
3689
  onLoadRef.current = options.onLoad;
3324
3690
  onErrorRef.current = options.onError;
3325
3691
  onExportRef.current = options.onExport;
3326
3692
  limitsRef.current = options.limits;
3693
+ assetResolverRef.current = options.assetResolver;
3694
+ const editorRef = useRef16(editor);
3695
+ editorRef.current = editor;
3696
+ const selectionRef = useRef16(selection);
3697
+ selectionRef.current = selection;
3327
3698
  const loadBufferWithRequest = useCallback11(
3328
3699
  async (buffer, requestId) => {
3329
3700
  setLoading(true);
@@ -3331,14 +3702,17 @@ function useEditableEpub(options = {}) {
3331
3702
  try {
3332
3703
  const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
3333
3704
  if (requestId !== requestIdRef.current) return null;
3705
+ const firstParagraph = { chapter: 0, paragraph: 0 };
3706
+ editorRef.current = next;
3707
+ selectionRef.current = firstParagraph;
3334
3708
  setEditor(next);
3335
- setSelectionState({ chapter: 0, paragraph: 0 });
3709
+ setSelectionState(firstParagraph);
3336
3710
  setRevision((value) => value + 1);
3337
3711
  onLoadRef.current?.(next);
3338
3712
  return next;
3339
3713
  } catch (err) {
3340
3714
  if (requestId === requestIdRef.current) {
3341
- const nextError = err instanceof Error ? err : new Error(String(err));
3715
+ const nextError = toError(err);
3342
3716
  setError(nextError);
3343
3717
  onErrorRef.current?.(nextError);
3344
3718
  }
@@ -3362,10 +3736,10 @@ function useEditableEpub(options = {}) {
3362
3736
  setLoading(true);
3363
3737
  setError(null);
3364
3738
  try {
3365
- return await loadBufferWithRequest(await file.arrayBuffer(), requestId);
3739
+ return await loadBufferWithRequest(await readEpubFile(file, limitsRef.current), requestId);
3366
3740
  } catch (err) {
3367
3741
  if (requestId === requestIdRef.current) {
3368
- const nextError = err instanceof Error ? err : new Error(String(err));
3742
+ const nextError = toError(err);
3369
3743
  setError(nextError);
3370
3744
  onErrorRef.current?.(nextError);
3371
3745
  setLoading(false);
@@ -3381,12 +3755,10 @@ function useEditableEpub(options = {}) {
3381
3755
  setLoading(true);
3382
3756
  setError(null);
3383
3757
  try {
3384
- const res = await fetch(url);
3385
- if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
3386
- return await loadBufferWithRequest(await res.arrayBuffer(), requestId);
3758
+ return await loadBufferWithRequest(await fetchEpubBuffer(url), requestId);
3387
3759
  } catch (err) {
3388
3760
  if (requestId === requestIdRef.current) {
3389
- const nextError = err instanceof Error ? err : new Error(String(err));
3761
+ const nextError = toError(err);
3390
3762
  setError(nextError);
3391
3763
  onErrorRef.current?.(nextError);
3392
3764
  setLoading(false);
@@ -3396,74 +3768,74 @@ function useEditableEpub(options = {}) {
3396
3768
  },
3397
3769
  [loadBufferWithRequest]
3398
3770
  );
3399
- useEffect12(() => {
3771
+ useEffect13(() => {
3400
3772
  if (options.defaultUrl) void loadUrl(options.defaultUrl);
3401
3773
  }, [options.defaultUrl, loadUrl]);
3402
3774
  const book = editor?.book ?? null;
3403
3775
  const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
3404
- const previewBook = useMemo8(() => {
3776
+ const previewBook = useMemo9(() => {
3405
3777
  void revision;
3406
3778
  return book ? cloneEditableEpubBook2(book) : null;
3407
3779
  }, [book, revision]);
3408
3780
  const history = editor?.history ?? null;
3409
- const setSelection = useCallback11(
3410
- (nextSelection) => {
3411
- setSelectionState(clampEditableEpubSelection(book, nextSelection));
3412
- },
3413
- [book]
3414
- );
3781
+ const setSelection = useCallback11((nextSelection) => {
3782
+ const next = clampEditableEpubSelection(editorRef.current?.book ?? null, nextSelection);
3783
+ selectionRef.current = next;
3784
+ setSelectionState(next);
3785
+ }, []);
3415
3786
  const updateParagraph = useCallback11(
3416
3787
  (text, inlineAnnotations) => {
3417
- if (!editor) return;
3418
- editor.updateParagraph(selection.chapter, selection.paragraph, {
3419
- text,
3420
- inlineAnnotations
3421
- });
3422
- setRevision((value) => value + 1);
3423
- },
3424
- [editor, selection.chapter, selection.paragraph]
3425
- );
3426
- const setInlineAnnotations = useCallback11(
3427
- (inlineAnnotations) => {
3428
- if (!editor) return;
3429
- editor.setInlineAnnotations(selection.chapter, selection.paragraph, inlineAnnotations);
3430
- setRevision((value) => value + 1);
3431
- },
3432
- [editor, selection.chapter, selection.paragraph]
3433
- );
3434
- const addImage = useCallback11(
3435
- (image) => {
3436
- if (!editor) return;
3437
- editor.addImage(selection.chapter, image);
3788
+ const current = editorRef.current;
3789
+ if (!current) return;
3790
+ const { chapter, paragraph } = selectionRef.current;
3791
+ current.updateParagraph(chapter, paragraph, { text, inlineAnnotations });
3438
3792
  setRevision((value) => value + 1);
3439
3793
  },
3440
- [editor, selection.chapter]
3794
+ []
3441
3795
  );
3796
+ const setInlineAnnotations = useCallback11((inlineAnnotations) => {
3797
+ const current = editorRef.current;
3798
+ if (!current) return;
3799
+ const { chapter, paragraph } = selectionRef.current;
3800
+ current.setInlineAnnotations(chapter, paragraph, inlineAnnotations);
3801
+ setRevision((value) => value + 1);
3802
+ }, []);
3803
+ const addImage = useCallback11((image) => {
3804
+ const current = editorRef.current;
3805
+ if (!current) return;
3806
+ current.addImage(selectionRef.current.chapter, image);
3807
+ setRevision((value) => value + 1);
3808
+ }, []);
3442
3809
  const undo = useCallback11(() => {
3443
- if (!editor) return false;
3444
- const changed = editor.undo();
3810
+ const current = editorRef.current;
3811
+ if (!current) return false;
3812
+ const changed = current.undo();
3445
3813
  if (changed) setRevision((value) => value + 1);
3446
3814
  return changed;
3447
- }, [editor]);
3815
+ }, []);
3448
3816
  const redo = useCallback11(() => {
3449
- if (!editor) return false;
3450
- const changed = editor.redo();
3817
+ const current = editorRef.current;
3818
+ if (!current) return false;
3819
+ const changed = current.redo();
3451
3820
  if (changed) setRevision((value) => value + 1);
3452
3821
  return changed;
3453
- }, [editor]);
3822
+ }, []);
3454
3823
  const exportEpub = useCallback11(
3455
- async (options2) => {
3456
- if (!editor) return null;
3824
+ async (exportOptions) => {
3825
+ const current = editorRef.current;
3826
+ if (!current) return null;
3457
3827
  setExporting(true);
3458
3828
  try {
3459
- const buffer = await editor.export(options2);
3829
+ const buffer = await current.export(
3830
+ withAssetResolver(exportOptions, assetResolverRef.current)
3831
+ );
3460
3832
  onExportRef.current?.(buffer);
3461
3833
  return buffer;
3462
3834
  } finally {
3463
3835
  setExporting(false);
3464
3836
  }
3465
3837
  },
3466
- [editor]
3838
+ []
3467
3839
  );
3468
3840
  return {
3469
3841
  editor,
@@ -3488,62 +3860,79 @@ function useEditableEpub(options = {}) {
3488
3860
  exportEpub
3489
3861
  };
3490
3862
  }
3863
+ function withAssetResolver(options, resolver) {
3864
+ if (!resolver || options?.assetResolver) return options;
3865
+ return { ...options, assetResolver: resolver };
3866
+ }
3491
3867
 
3492
3868
  // src/useEpubProject.ts
3493
3869
  import {
3494
3870
  EpubProject as EpubProject2,
3495
3871
  parseEpub as parseEpub2
3496
3872
  } from "@libraz/mejiro/epub";
3497
- import { useCallback as useCallback12, useEffect as useEffect13, useMemo as useMemo9, useRef as useRef16, useState as useState15 } from "react";
3873
+ import { useCallback as useCallback12, useEffect as useEffect14, useMemo as useMemo10, useRef as useRef17, useState as useState16 } from "react";
3498
3874
  function useEpubProject(options = {}) {
3499
3875
  const defaultTitle = options.defaultChapterTitle;
3500
3876
  const defaultBody = options.defaultChapterBody;
3501
- const [metadata, setMetadataState] = useState15({
3502
- title: "\u65B0\u3057\u3044\u4F5C\u54C1",
3503
- language: "ja",
3504
- ...options.metadata
3505
- });
3506
- const [chapters, setChaptersState] = useState15(
3507
- options.chapters?.length ? options.chapters : [defaultChapter3(0, defaultTitle, defaultBody)]
3877
+ const [metadata, setMetadataState] = useState16(
3878
+ () => initialMetadata(options.metadata)
3508
3879
  );
3509
- const [selectedChapter, setSelectedChapterState] = useState15(0);
3510
- const [cover, setCoverState] = useState15(options.cover ?? null);
3511
- const [assets, setAssetsState] = useState15(options.assets ?? []);
3512
- const [previewBook, setPreviewBook] = useState15(null);
3513
- const [previewError, setPreviewError] = useState15(null);
3514
- const [previewing, setPreviewing] = useState15(false);
3515
- const previewRequestIdRef = useRef16(0);
3516
- const onPreviewRef = useRef16(options.onPreview);
3517
- const onExportRef = useRef16(options.onExport);
3880
+ const [chapters, setChaptersState] = useState16(
3881
+ () => options.chapters?.length ? options.chapters : [defaultChapter3([], defaultTitle, defaultBody)]
3882
+ );
3883
+ const [selectedChapter, setSelectedChapterState] = useState16(0);
3884
+ const [cover, setCoverState] = useState16(options.cover ?? null);
3885
+ const [assets, setAssetsState] = useState16(options.assets ?? []);
3886
+ const [previewBook, setPreviewBook] = useState16(null);
3887
+ const [previewError, setPreviewError] = useState16(null);
3888
+ const [previewing, setPreviewing] = useState16(false);
3889
+ const previewRequestIdRef = useRef17(0);
3890
+ const onPreviewRef = useRef17(options.onPreview);
3891
+ const onExportRef = useRef17(options.onExport);
3518
3892
  onPreviewRef.current = options.onPreview;
3519
3893
  onExportRef.current = options.onExport;
3894
+ const assetResolverRef = useRef17(options.assetResolver);
3895
+ assetResolverRef.current = options.assetResolver;
3896
+ const metadataRef = useRef17(metadata);
3897
+ metadataRef.current = metadata;
3898
+ const chaptersRef = useRef17(chapters);
3899
+ chaptersRef.current = chapters;
3900
+ const selectedRef = useRef17(selectedChapter);
3901
+ selectedRef.current = selectedChapter;
3902
+ const coverRef = useRef17(cover);
3903
+ coverRef.current = cover;
3904
+ const assetsRef = useRef17(assets);
3905
+ assetsRef.current = assets;
3520
3906
  const buildProject = useCallback12(() => {
3907
+ const currentCover = coverRef.current;
3521
3908
  const project = EpubProject2.fromManuscript({
3522
- metadata,
3909
+ metadata: metadataRef.current,
3523
3910
  includeTitlePage: false,
3524
3911
  includeTitleInFirstChapter: true,
3525
- chapters: chapters.map((chapter) => ({
3912
+ chapters: chaptersRef.current.map((chapter) => ({
3526
3913
  id: chapter.id,
3527
3914
  title: chapter.title || "Untitled",
3528
3915
  body: chapter.body
3529
3916
  })),
3530
- ...cover ? { cover } : {}
3917
+ ...currentCover ? { cover: currentCover } : {}
3531
3918
  });
3532
- for (const asset of assets) project.addAsset(asset);
3919
+ for (const asset of assetsRef.current) project.addAsset(asset);
3533
3920
  return project;
3534
- }, [assets, chapters, cover, metadata]);
3535
- const assetResolverRef = useRef16(options.assetResolver);
3536
- assetResolverRef.current = options.assetResolver;
3537
- useEffect13(() => {
3921
+ }, []);
3922
+ const exportOptions = useCallback12((signal) => {
3923
+ const resolver = assetResolverRef.current;
3924
+ return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
3925
+ }, []);
3926
+ useEffect14(() => {
3538
3927
  const requestId = ++previewRequestIdRef.current;
3928
+ const controller = new AbortController();
3539
3929
  let cancelled = false;
3540
3930
  setPreviewing(true);
3541
3931
  const timer = setTimeout(() => {
3542
3932
  void (async () => {
3543
3933
  try {
3544
- const resolver = assetResolverRef.current;
3545
3934
  const book = await parseEpub2(
3546
- await buildProject().export(resolver ? { assetResolver: resolver } : void 0)
3935
+ await buildProject().export(exportOptions(controller.signal))
3547
3936
  );
3548
3937
  if (cancelled || requestId !== previewRequestIdRef.current) return;
3549
3938
  setPreviewBook(book);
@@ -3551,7 +3940,7 @@ function useEpubProject(options = {}) {
3551
3940
  onPreviewRef.current?.(book);
3552
3941
  } catch (err) {
3553
3942
  if (!cancelled && requestId === previewRequestIdRef.current) {
3554
- setPreviewError(err instanceof Error ? err : new Error(String(err)));
3943
+ setPreviewError(toError(err));
3555
3944
  }
3556
3945
  } finally {
3557
3946
  if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
@@ -3562,99 +3951,107 @@ function useEpubProject(options = {}) {
3562
3951
  cancelled = true;
3563
3952
  previewRequestIdRef.current++;
3564
3953
  clearTimeout(timer);
3954
+ controller.abort();
3565
3955
  };
3566
- }, [buildProject, options.debounceMs]);
3956
+ }, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
3567
3957
  const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
3568
3958
  const setMetadata = useCallback12((patch) => {
3569
- setMetadataState((current) => ({ ...current, ...patch }));
3959
+ const next = { ...metadataRef.current, ...patch };
3960
+ metadataRef.current = next;
3961
+ setMetadataState(next);
3962
+ }, []);
3963
+ const commit = useCallback12((nextChapters, nextSelected) => {
3964
+ chaptersRef.current = nextChapters;
3965
+ selectedRef.current = nextSelected;
3966
+ setChaptersState(nextChapters);
3967
+ setSelectedChapterState(nextSelected);
3570
3968
  }, []);
3571
3969
  const setChapters = useCallback12(
3572
3970
  (next) => {
3573
- const normalized = next.length ? next : [defaultChapter3(0, defaultTitle, defaultBody)];
3574
- setChaptersState(normalized);
3575
- setSelectedChapterState((current) => {
3576
- const selectedId = chapters[current]?.id;
3577
- const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
3578
- return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
3579
- });
3971
+ const normalized = next.length ? next : [defaultChapter3([], defaultTitle, defaultBody)];
3972
+ const prev = selectedRef.current;
3973
+ const selectedId = chaptersRef.current[prev]?.id;
3974
+ const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
3975
+ commit(
3976
+ normalized,
3977
+ nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
3978
+ );
3580
3979
  },
3581
- [chapters, defaultBody, defaultTitle]
3980
+ [commit, defaultBody, defaultTitle]
3582
3981
  );
3583
3982
  const setCover = useCallback12((asset) => {
3983
+ coverRef.current = asset;
3584
3984
  setCoverState(asset);
3585
3985
  }, []);
3586
3986
  const setAssets = useCallback12((next) => {
3987
+ assetsRef.current = next;
3587
3988
  setAssetsState(next);
3588
3989
  }, []);
3589
- const setSelectedChapter = useCallback12(
3590
- (index) => {
3591
- setSelectedChapterState(Math.max(0, Math.min(index, chapters.length - 1)));
3592
- },
3593
- [chapters.length]
3594
- );
3990
+ const setSelectedChapter = useCallback12((index) => {
3991
+ const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
3992
+ selectedRef.current = next;
3993
+ setSelectedChapterState(next);
3994
+ }, []);
3595
3995
  const patchChapter = useCallback12((index, patch) => {
3596
- setChaptersState(
3597
- (current) => current.map(
3598
- (chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
3599
- )
3996
+ const next = chaptersRef.current.map(
3997
+ (chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
3600
3998
  );
3999
+ chaptersRef.current = next;
4000
+ setChaptersState(next);
3601
4001
  }, []);
3602
4002
  const addChapter = useCallback12(
3603
4003
  (chapter = {}) => {
3604
- setChaptersState((current) => {
3605
- const generated = defaultChapter3(current.length, defaultTitle, defaultBody);
3606
- const next = [
3607
- ...current,
3608
- {
3609
- id: chapter.id ?? generated.id,
3610
- title: chapter.title ?? generated.title,
3611
- body: chapter.body ?? generated.body
3612
- }
3613
- ];
3614
- setSelectedChapterState(next.length - 1);
3615
- return next;
3616
- });
4004
+ const current = chaptersRef.current;
4005
+ const generated = defaultChapter3(current, defaultTitle, defaultBody);
4006
+ const next = [
4007
+ ...current,
4008
+ {
4009
+ id: chapter.id ?? generated.id,
4010
+ title: chapter.title ?? generated.title,
4011
+ body: chapter.body ?? generated.body
4012
+ }
4013
+ ];
4014
+ commit(next, next.length - 1);
3617
4015
  },
3618
- [defaultBody, defaultTitle]
4016
+ [commit, defaultBody, defaultTitle]
3619
4017
  );
3620
4018
  const removeChapter = useCallback12(
3621
- (index = selectedChapter) => {
3622
- setChaptersState((current) => {
3623
- if (current.length <= 1) return current;
3624
- const next = current.filter((_, chapterIndex) => chapterIndex !== index);
3625
- setSelectedChapterState((currentSelected) => {
3626
- if (currentSelected === index) return Math.max(0, Math.min(index, next.length - 1));
3627
- if (index < currentSelected) return currentSelected - 1;
3628
- return Math.max(0, Math.min(currentSelected, next.length - 1));
3629
- });
3630
- return next;
3631
- });
4019
+ (index = selectedRef.current) => {
4020
+ const current = chaptersRef.current;
4021
+ if (current.length <= 1) return;
4022
+ const next = current.filter((_, chapterIndex) => chapterIndex !== index);
4023
+ const prev = selectedRef.current;
4024
+ let nextSelected;
4025
+ if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
4026
+ else if (index < prev) nextSelected = prev - 1;
4027
+ else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
4028
+ commit(next, nextSelected);
3632
4029
  },
3633
- [selectedChapter]
4030
+ [commit]
3634
4031
  );
3635
- const reorderChapters = useCallback12((from, to) => {
3636
- setChaptersState((current) => {
3637
- if (from < 0 || from >= current.length) return current;
4032
+ const reorderChapters = useCallback12(
4033
+ (from, to) => {
4034
+ const current = chaptersRef.current;
4035
+ if (from < 0 || from >= current.length) return;
3638
4036
  const next = [...current];
3639
4037
  const [moved] = next.splice(from, 1);
3640
4038
  const target = Math.max(0, Math.min(next.length, to));
3641
4039
  next.splice(target, 0, moved);
3642
- setSelectedChapterState((currentSelected) => {
3643
- if (currentSelected === from) return target;
3644
- if (from < currentSelected && target >= currentSelected) return currentSelected - 1;
3645
- if (from > currentSelected && target <= currentSelected) return currentSelected + 1;
3646
- return currentSelected;
3647
- });
3648
- return next;
3649
- });
3650
- }, []);
4040
+ const prev = selectedRef.current;
4041
+ let nextSelected = prev;
4042
+ if (prev === from) nextSelected = target;
4043
+ else if (from < prev && target >= prev) nextSelected = prev - 1;
4044
+ else if (from > prev && target <= prev) nextSelected = prev + 1;
4045
+ commit(next, nextSelected);
4046
+ },
4047
+ [commit]
4048
+ );
3651
4049
  const exportEpub = useCallback12(async () => {
3652
- const resolver = assetResolverRef.current;
3653
- const buffer = await buildProject().export(resolver ? { assetResolver: resolver } : void 0);
4050
+ const buffer = await buildProject().export(exportOptions());
3654
4051
  onExportRef.current?.(buffer);
3655
4052
  return buffer;
3656
- }, [buildProject]);
3657
- return useMemo9(
4053
+ }, [buildProject, exportOptions]);
4054
+ return useMemo10(
3658
4055
  () => ({
3659
4056
  metadata,
3660
4057
  chapters,
@@ -3701,21 +4098,26 @@ function useEpubProject(options = {}) {
3701
4098
  ]
3702
4099
  );
3703
4100
  }
3704
- function defaultChapter3(index, titleFor = (i) => i === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${i + 1}\u8A71`, bodyFor = (i) => i === 0 ? "\u3053\u308C\u306F\uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B\u306E\u30EB\u30D3\u4F8B\u3067\u3059\u3002\n\n\u672C\u6587\u3092\u3053\u3053\u306B\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002" : "") {
3705
- return {
3706
- id: `chapter-${Date.now()}-${index}`,
3707
- title: titleFor(index),
3708
- body: bodyFor(index)
3709
- };
4101
+ function initialMetadata(patch = {}) {
4102
+ const metadata = mergeDefined(
4103
+ { title: "\u65B0\u3057\u3044\u4F5C\u54C1", language: "ja" },
4104
+ patch
4105
+ );
4106
+ if (!metadata.identifier?.trim()) metadata.identifier = `urn:uuid:${crypto.randomUUID()}`;
4107
+ return metadata;
4108
+ }
4109
+ function defaultChapter3(existing, titleFor = (i) => i === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${i + 1}\u8A71`, bodyFor = (i) => i === 0 ? "\u3053\u308C\u306F\uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B\u306E\u30EB\u30D3\u4F8B\u3067\u3059\u3002\n\n\u672C\u6587\u3092\u3053\u3053\u306B\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002" : "") {
4110
+ const index = existing.length;
4111
+ return { id: uniqueChapterId(existing), title: titleFor(index), body: bodyFor(index) };
3710
4112
  }
3711
4113
 
3712
4114
  // src/useImageOverlay.ts
3713
- import { createOverlayDragSession as createOverlayDragSession2 } from "@libraz/mejiro/browser";
4115
+ import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointerPress as isPrimaryPointerPress3 } from "@libraz/mejiro/browser";
3714
4116
  import {
3715
4117
  useCallback as useCallback13,
3716
- useEffect as useEffect14,
3717
- useRef as useRef17,
3718
- useState as useState16
4118
+ useEffect as useEffect15,
4119
+ useRef as useRef18,
4120
+ useState as useState17
3719
4121
  } from "react";
3720
4122
  function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3721
4123
  const defW = options?.defaultWidth ?? 120;
@@ -3723,17 +4125,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3723
4125
  const defX = options?.defaultX ?? 80;
3724
4126
  const defY = options?.defaultY ?? 100;
3725
4127
  const margin = options?.margin;
3726
- const [imageRect, setImageRect] = useState16(null);
3727
- const rectRef = useRef17(imageRect);
4128
+ const [imageRect, setImageRect] = useState17(null);
4129
+ const rectRef = useRef18(imageRect);
3728
4130
  rectRef.current = imageRect;
3729
- const layoutRef = useRef17(layout);
4131
+ const layoutRef = useRef18(layout);
3730
4132
  layoutRef.current = layout;
3731
- const spreadRef = useRef17(spreadIdx);
4133
+ const spreadRef = useRef18(spreadIdx);
3732
4134
  spreadRef.current = spreadIdx;
3733
- const onUpdateRef = useRef17(onUpdate);
4135
+ const onUpdateRef = useRef18(onUpdate);
3734
4136
  onUpdateRef.current = onUpdate;
3735
- const activeDragCleanupsRef = useRef17(/* @__PURE__ */ new Set());
3736
- useEffect14(
4137
+ const activeDragCleanupsRef = useRef18(/* @__PURE__ */ new Set());
4138
+ useEffect15(
3737
4139
  () => () => {
3738
4140
  for (const cleanup of activeDragCleanupsRef.current) cleanup();
3739
4141
  activeDragCleanupsRef.current.clear();
@@ -3750,20 +4152,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3750
4152
  },
3751
4153
  [margin]
3752
4154
  );
3753
- const syncedRef = useRef17({
3754
- layout,
3755
- spreadIdx
3756
- });
3757
- useEffect14(() => {
4155
+ const syncedRef = useRef18({ layout, spreadIdx, margin });
4156
+ useEffect15(() => {
3758
4157
  const prev = syncedRef.current;
3759
- syncedRef.current = { layout, spreadIdx };
3760
- if (prev.layout === layout && prev.spreadIdx === spreadIdx) return;
4158
+ syncedRef.current = { layout, spreadIdx, margin };
4159
+ if (prev.layout === layout && prev.spreadIdx === spreadIdx && prev.margin === margin) return;
3761
4160
  if (rectRef.current === null) return;
3762
4161
  if (prev.layout && prev.spreadIdx !== spreadIdx) {
3763
4162
  prev.layout.syncImages(prev.spreadIdx, void 0);
3764
4163
  }
3765
4164
  syncToLayout(rectRef.current);
3766
- }, [layout, spreadIdx, syncToLayout]);
4165
+ }, [layout, spreadIdx, margin, syncToLayout]);
3767
4166
  const toggleImage = useCallback13(() => {
3768
4167
  if (rectRef.current) {
3769
4168
  setImageRect(null);
@@ -3783,6 +4182,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3783
4182
  );
3784
4183
  const onOverlayPointerDown = useCallback13(
3785
4184
  (e) => {
4185
+ if (!isPrimaryPointerPress3(e)) return;
3786
4186
  e.preventDefault();
3787
4187
  const current = rectRef.current;
3788
4188
  if (!current) return;
@@ -3795,7 +4195,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3795
4195
  pointerId: e.nativeEvent.pointerId,
3796
4196
  captureElement: target,
3797
4197
  activeElement: target,
3798
- dragClass: "dragging",
4198
+ dragClass: "is-dragging",
3799
4199
  registry: activeDragCleanupsRef.current,
3800
4200
  onChange: applyRect
3801
4201
  });
@@ -3804,6 +4204,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3804
4204
  );
3805
4205
  const onResizePointerDown = useCallback13(
3806
4206
  (e) => {
4207
+ if (!isPrimaryPointerPress3(e)) return;
3807
4208
  e.preventDefault();
3808
4209
  e.stopPropagation();
3809
4210
  const current = rectRef.current;
@@ -3817,7 +4218,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3817
4218
  pointerId: e.nativeEvent.pointerId,
3818
4219
  captureElement: target,
3819
4220
  activeElement: target.parentElement,
3820
- dragClass: "dragging",
4221
+ dragClass: "is-dragging",
3821
4222
  registry: activeDragCleanupsRef.current,
3822
4223
  onChange: applyRect
3823
4224
  });
@@ -3834,69 +4235,61 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3834
4235
  }
3835
4236
 
3836
4237
  // src/useLibrary.ts
3837
- import { useCallback as useCallback14, useEffect as useEffect15, useMemo as useMemo10, useState as useState17 } from "react";
4238
+ import { useCallback as useCallback14, useEffect as useEffect16, useMemo as useMemo11, useState as useState18 } from "react";
4239
+ function resolveCurrentId(volumes, currentId, preferredId) {
4240
+ if (volumes.length === 0) return null;
4241
+ if (preferredId != null && volumes.some((v) => v.id === preferredId)) return preferredId;
4242
+ if (currentId != null && volumes.some((v) => v.id === currentId)) return currentId;
4243
+ return volumes[0]?.id ?? null;
4244
+ }
3838
4245
  function useLibrary(options) {
3839
4246
  const { volumes, initialVolumeId, onChange } = options;
3840
- const initialIndex = initialVolumeId != null ? Math.max(
3841
- 0,
3842
- volumes.findIndex((v) => v.id === initialVolumeId)
3843
- ) : 0;
3844
- const [currentId, setCurrentId] = useState17(
3845
- volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
4247
+ const [currentId, setCurrentId] = useState18(
4248
+ () => resolveCurrentId(volumes, null, initialVolumeId)
3846
4249
  );
3847
- useEffect15(() => {
3848
- if (volumes.length === 0) {
3849
- if (currentId !== null) setCurrentId(null);
3850
- return;
3851
- }
3852
- if (currentId != null && volumes.some((v) => v.id === currentId)) return;
3853
- const fallback = volumes[0] ?? null;
3854
- setCurrentId(fallback?.id ?? null);
3855
- if (fallback) onChange?.(fallback);
3856
- }, [currentId, volumes, onChange]);
3857
- const index = useMemo10(() => {
3858
- if (volumes.length === 0) return -1;
3859
- if (currentId == null) return 0;
3860
- const found = volumes.findIndex((v) => v.id === currentId);
3861
- return found >= 0 ? found : 0;
3862
- }, [currentId, volumes]);
3863
- const current = useMemo10(
4250
+ const [navigated, setNavigated] = useState18(false);
4251
+ const resolvedId = resolveCurrentId(volumes, currentId, navigated ? void 0 : initialVolumeId);
4252
+ useEffect16(() => {
4253
+ if (resolvedId === currentId) return;
4254
+ setCurrentId(resolvedId);
4255
+ const volume = volumes.find((v) => v.id === resolvedId);
4256
+ if (volume) onChange?.(volume);
4257
+ }, [currentId, resolvedId, volumes, onChange]);
4258
+ const index = useMemo11(
4259
+ () => resolvedId == null ? -1 : volumes.findIndex((v) => v.id === resolvedId),
4260
+ [resolvedId, volumes]
4261
+ );
4262
+ const current = useMemo11(
3864
4263
  () => index >= 0 && index < volumes.length ? volumes[index] : null,
3865
4264
  [index, volumes]
3866
4265
  );
3867
- const fire = useCallback14(
4266
+ const activate = useCallback14(
3868
4267
  (i) => {
3869
4268
  const v = volumes[i];
3870
- if (v) onChange?.(v);
4269
+ if (!v) return;
4270
+ setNavigated(true);
4271
+ setCurrentId(v.id);
4272
+ onChange?.(v);
3871
4273
  },
3872
4274
  [volumes, onChange]
3873
4275
  );
3874
4276
  const next = useCallback14(() => {
3875
4277
  if (index < 0) return;
3876
4278
  const ni = Math.min(volumes.length - 1, index + 1);
3877
- if (ni !== index) {
3878
- setCurrentId(volumes[ni]?.id ?? null);
3879
- fire(ni);
3880
- }
3881
- }, [index, volumes, fire]);
4279
+ if (ni !== index) activate(ni);
4280
+ }, [index, activate, volumes.length]);
3882
4281
  const prev = useCallback14(() => {
3883
4282
  if (index < 0) return;
3884
4283
  const ni = Math.max(0, index - 1);
3885
- if (ni !== index) {
3886
- setCurrentId(volumes[ni]?.id ?? null);
3887
- fire(ni);
3888
- }
3889
- }, [index, volumes, fire]);
4284
+ if (ni !== index) activate(ni);
4285
+ }, [index, activate]);
3890
4286
  const goTo = useCallback14(
3891
4287
  (id) => {
3892
4288
  const ni = volumes.findIndex((v) => v.id === id);
3893
4289
  if (ni < 0) return;
3894
- if (ni !== index) {
3895
- setCurrentId(id);
3896
- fire(ni);
3897
- }
4290
+ if (ni !== index) activate(ni);
3898
4291
  },
3899
- [index, volumes, fire]
4292
+ [index, volumes, activate]
3900
4293
  );
3901
4294
  return { list: volumes, current, currentIndex: index, next, prev, goTo };
3902
4295
  }
@@ -3904,25 +4297,25 @@ function useLibrary(options) {
3904
4297
  // src/useManuscriptLayout.ts
3905
4298
  import {
3906
4299
  useCallback as useCallback15,
3907
- useEffect as useEffect16,
4300
+ useEffect as useEffect17,
3908
4301
  useLayoutEffect as useLayoutEffect5,
3909
- useRef as useRef18,
3910
- useState as useState18
4302
+ useRef as useRef19,
4303
+ useState as useState19
3911
4304
  } from "react";
3912
4305
  function useManuscriptLayout(book, chapter, surface, options = {}) {
3913
4306
  const dialect = options.dialect ?? "mejiro";
3914
4307
  const enableResize = options.enableResize ?? true;
3915
4308
  const resizeDebounce = options.resizeDebounce ?? 120;
3916
- const [layout, setLayout] = useState18(null);
3917
- const [pageWidth, setPageWidth] = useState18(0);
3918
- const [pageHeight, setPageHeight] = useState18(0);
3919
- const [contentHeight, setContentHeight] = useState18(0);
3920
- const [elapsedMs, setElapsedMs] = useState18(0);
3921
- const requestIdRef = useRef18(0);
3922
- const layoutRef = useRef18(null);
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);
3923
4316
  layoutRef.current = layout;
3924
- const pendingRestore = useRef18(null);
3925
- const optionsRef = useRef18(options);
4317
+ const pendingRestore = useRef19(null);
4318
+ const optionsRef = useRef19(options);
3926
4319
  optionsRef.current = options;
3927
4320
  const recompute = useCallback15(
3928
4321
  async (opts = {}) => {
@@ -3962,13 +4355,20 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
3962
4355
  setContentHeight(0);
3963
4356
  setElapsedMs(0);
3964
4357
  }, [recompute]);
3965
- useEffect16(() => {
4358
+ useEffect17(() => {
3966
4359
  void recompute();
3967
4360
  return () => {
3968
4361
  requestIdRef.current++;
3969
4362
  };
3970
4363
  }, [recompute]);
3971
- useEffect16(() => {
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(() => {
3972
4372
  if (!enableResize) return;
3973
4373
  const el = surface.current;
3974
4374
  if (!el) return;
@@ -4018,89 +4418,44 @@ import {
4018
4418
  parseReadingPosition,
4019
4419
  serializeReadingPosition
4020
4420
  } from "@libraz/mejiro";
4021
- import { useCallback as useCallback16, useEffect as useEffect17, useRef as useRef19, useState as useState19 } from "react";
4022
- function resolveDefaultStorage2() {
4023
- if (typeof globalThis === "undefined") return null;
4024
- const w = globalThis.localStorage;
4025
- return w ?? null;
4026
- }
4421
+ import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef20, useState as useState20 } from "react";
4027
4422
  function useReadingPosition(options) {
4028
4423
  const { key, throttleMs = 250 } = options;
4029
- const storage = options.storage ?? resolveDefaultStorage2();
4030
- const [position, setPosition] = useState19(() => {
4031
- if (!storage) return null;
4032
- try {
4033
- return parseReadingPosition(storage.getItem(key));
4034
- } catch {
4035
- return null;
4036
- }
4037
- });
4038
- const storageRef = useRef19(storage);
4424
+ const storage = options.storage ?? resolveDefaultStorage();
4425
+ const [position, setPosition] = useState20(
4426
+ () => readStorage(storage, key, parseReadingPosition, null)
4427
+ );
4428
+ const storageRef = useRef20(storage);
4039
4429
  storageRef.current = storage;
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;
4046
- }
4047
- const write = pendingWriteRef.current;
4048
- pendingWriteRef.current = null;
4049
- if (!write) return;
4050
- try {
4051
- write();
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 {
4071
- setPosition(null);
4072
- }
4073
- return flushPending;
4074
- }, [key, flushPending]);
4075
- const onChangeRef = useRef19(options.onChange);
4430
+ const pendingRef = useRef20(null);
4431
+ if (!pendingRef.current) pendingRef.current = createPendingWrite();
4432
+ const pending = pendingRef.current;
4433
+ useEffect18(() => flushOnPageHide(pending.flush), [pending]);
4434
+ useEffect18(() => {
4435
+ setPosition(readStorage(storageRef.current, key, parseReadingPosition, null));
4436
+ return pending.flush;
4437
+ }, [key, pending]);
4438
+ const onChangeRef = useRef20(options.onChange);
4076
4439
  onChangeRef.current = options.onChange;
4077
4440
  const save = useCallback16(
4078
4441
  (next) => {
4079
4442
  setPosition(next);
4080
4443
  const currentStorage = storageRef.current;
4081
4444
  if (currentStorage) {
4082
- cancelPending();
4083
- pendingWriteRef.current = () => {
4445
+ pending.schedule(() => {
4084
4446
  currentStorage.setItem(key, serializeReadingPosition(next));
4085
- };
4086
- timerRef.current = setTimeout(flushPending, throttleMs);
4447
+ }, throttleMs);
4087
4448
  }
4088
4449
  onChangeRef.current?.(next);
4089
4450
  },
4090
- [key, throttleMs, cancelPending, flushPending]
4451
+ [key, throttleMs, pending]
4091
4452
  );
4092
4453
  const clear = useCallback16(() => {
4093
4454
  setPosition(null);
4094
- cancelPending();
4095
- const currentStorage = storageRef.current;
4096
- if (currentStorage) {
4097
- try {
4098
- currentStorage.removeItem(key);
4099
- } catch {
4100
- }
4101
- }
4455
+ pending.cancel();
4456
+ removeStorage(storageRef.current, key);
4102
4457
  onChangeRef.current?.(null);
4103
- }, [key, cancelPending]);
4458
+ }, [key, pending]);
4104
4459
  return { position, save, clear };
4105
4460
  }
4106
4461