@libraz/mejiro-react 0.8.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 (4) hide show
  1. package/README.md +12 -3
  2. package/dist/index.d.ts +521 -151
  3. package/dist/index.js +1973 -1443
  4. 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(
@@ -114,6 +127,11 @@ function MejiroDropZone({
114
127
  className: rootClass,
115
128
  style,
116
129
  onClick: () => inputRef.current?.click(),
130
+ onKeyDown: (e) => {
131
+ if (e.key !== "Enter" && e.key !== " ") return;
132
+ e.preventDefault();
133
+ inputRef.current?.click();
134
+ },
117
135
  onDragOver: (e) => {
118
136
  e.preventDefault();
119
137
  setDragover(true);
@@ -150,8 +168,35 @@ function MejiroDropZone({
150
168
  }
151
169
 
152
170
  // src/MejiroEditor.tsx
153
- import { EditableEpub } from "@libraz/mejiro/epub";
154
- import { useCallback as useCallback7, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef10, useState as useState8 } from "react";
171
+ import {
172
+ cloneEditableEpubBook,
173
+ EditableEpub,
174
+ exportEditableEpub
175
+ } from "@libraz/mejiro/epub";
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
+ }
155
200
 
156
201
  // src/MejiroReader.tsx
157
202
  import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
@@ -160,12 +205,12 @@ import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
160
205
  import {
161
206
  forwardRef,
162
207
  useCallback as useCallback6,
163
- useEffect as useEffect6,
208
+ useEffect as useEffect8,
164
209
  useImperativeHandle,
165
210
  useLayoutEffect as useLayoutEffect4,
166
211
  useMemo as useMemo4,
167
- useRef as useRef9,
168
- useState as useState7
212
+ useRef as useRef10,
213
+ useState as useState8
169
214
  } from "react";
170
215
 
171
216
  // src/MejiroPageIndicator.tsx
@@ -181,26 +226,74 @@ function MejiroPageIndicator({ current, total }) {
181
226
  // src/MejiroScrollView.tsx
182
227
  import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
183
228
  import {
184
- useEffect,
229
+ useEffect as useEffect2,
185
230
  useLayoutEffect,
186
- useMemo as useMemo2,
187
- useRef as useRef2
231
+ useMemo as useMemo3,
232
+ useRef as useRef3
188
233
  } from "react";
189
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
+
190
279
  // src/MejiroPageView.tsx
191
280
  import { normalizeFontFamily } from "@libraz/mejiro/browser";
281
+ import { paragraphClassName as paragraphClassName2 } from "@libraz/mejiro/render";
282
+
283
+ // src/MejiroPage.tsx
284
+ import { paragraphClassName } from "@libraz/mejiro/render";
192
285
 
193
286
  // src/renderInlineNode.tsx
194
287
  import {
195
288
  segmentToInlineNode
196
289
  } from "@libraz/mejiro/render";
197
290
  import { createElement, Fragment as Fragment2 } from "react";
198
- import { jsx as jsx4 } from "react/jsx-runtime";
291
+ import { jsx as jsx5 } from "react/jsx-runtime";
199
292
  function renderSegment(segment, key) {
200
293
  return renderInlineNode(segmentToInlineNode(segment), key);
201
294
  }
202
295
  function renderInlineNode(node, key) {
203
- 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);
204
297
  return createElement(
205
298
  node.tag,
206
299
  {
@@ -214,35 +307,36 @@ function renderInlineNode(node, key) {
214
307
  }
215
308
 
216
309
  // src/MejiroPage.tsx
217
- import { jsx as jsx5 } from "react/jsx-runtime";
310
+ import { jsx as jsx6 } from "react/jsx-runtime";
218
311
  function renderLine(line, lineIndex) {
219
312
  const nodes = [];
220
313
  if (lineIndex > 0) {
221
- nodes.push(/* @__PURE__ */ jsx5("br", {}, `br-${lineIndex}`));
314
+ nodes.push(/* @__PURE__ */ jsx6("br", {}, `br-${lineIndex}`));
222
315
  }
223
316
  for (let i = 0; i < line.segments.length; i++) {
224
317
  nodes.push(renderSegment(line.segments[i], `${lineIndex}-${i}`));
225
318
  }
226
319
  return nodes;
227
320
  }
321
+ function paragraphScaleStyle(scale) {
322
+ return scale == null ? void 0 : { "--mejiro-paragraph-scale": scale };
323
+ }
228
324
  function MejiroPage({ page, className, style }) {
229
325
  const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
230
- return /* @__PURE__ */ jsx5("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
231
- let paraClass = "mejiro-paragraph";
232
- if (paragraph.headingLevel != null) {
233
- paraClass += ` mejiro-paragraph--h${paragraph.headingLevel}`;
234
- } else if (paragraph.isHeading) {
235
- paraClass += " mejiro-paragraph--heading";
236
- }
326
+ return /* @__PURE__ */ jsx6("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
327
+ const paraClass = paragraphClassName(
328
+ paragraph.kind ?? (paragraph.isHeading ? "heading" : void 0),
329
+ paragraph.headingLevel
330
+ );
237
331
  return (
238
332
  // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
239
- /* @__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)
240
334
  );
241
335
  }) });
242
336
  }
243
337
 
244
338
  // src/MejiroPageView.tsx
245
- import { jsx as jsx6 } from "react/jsx-runtime";
339
+ import { jsx as jsx7 } from "react/jsx-runtime";
246
340
  function MejiroPageView({
247
341
  result,
248
342
  fontFamily,
@@ -254,33 +348,307 @@ function MejiroPageView({
254
348
  if (result.hasImages || slotMode) {
255
349
  const rootClass = className ? `mejiro-page-slots ${className}` : "mejiro-page-slots";
256
350
  const fontFamilyCss = fontFamily != null ? normalizeFontFamily(fontFamily) : void 0;
257
- return /* @__PURE__ */ jsx6("div", { className: rootClass, style: { position: "relative", ...style }, children: result.lines.map((line, i) => {
258
- const slot = result.slots[i];
259
- if (!slot || slot.height <= 0) return null;
260
- 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: {
261
403
  position: "absolute",
262
- writingMode: "vertical-rl",
263
- whiteSpace: "nowrap",
264
- overflow: "hidden",
265
- right: slot.xPos,
266
- top: slot.yStart,
267
- height: slot.height,
268
- fontSize: line.fontSize,
269
- fontFamily: fontFamilyCss,
270
- lineHeight: lineSpacing,
271
- 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
+ }
272
530
  };
273
- return (
274
- // biome-ignore lint/suspicious/noArrayIndexKey: lines have no stable ID
275
- /* @__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
276
557
  );
277
- }) });
278
- }
279
- 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
+ };
280
647
  }
281
648
 
282
649
  // src/MejiroScrollView.tsx
283
- 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 = [];
284
652
  function MejiroScrollView({
285
653
  layout,
286
654
  pageWidth,
@@ -289,26 +657,52 @@ function MejiroScrollView({
289
657
  fontFamily,
290
658
  fontSize,
291
659
  lineSpacing,
660
+ slotMode,
292
661
  onVisiblePageChange,
293
662
  scrollToPage,
294
- pageGap = 24
663
+ pageGap = 24,
664
+ selectionRects,
665
+ spreadIdx,
666
+ images = NO_IMAGES,
667
+ onImagePointerDown,
668
+ onImageResizePointerDown,
669
+ onImageClose
295
670
  }) {
296
- const containerRef = useRef2(null);
297
- const pageRefs = useRef2([]);
298
- const onVisiblePageChangeRef = useRef2(onVisiblePageChange);
671
+ const containerRef = useRef3(null);
672
+ const pageRefs = useRef3([]);
673
+ const onVisiblePageChangeRef = useRef3(onVisiblePageChange);
299
674
  onVisiblePageChangeRef.current = onVisiblePageChange;
300
- const programmaticScrollRef = useRef2(false);
301
- 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(() => {
302
688
  const total = layout.totalPages;
303
689
  return Array.from({ length: total }, (_, i) => layout.getPage(i));
304
- }, [layout]);
690
+ }, [layout, images]);
305
691
  const pageCount = pages.length;
306
- const useSlot = true;
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]);
307
701
  const contentStyle = { height: contentHeight };
308
702
  if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
309
703
  if (fontSize != null) contentStyle.fontSize = fontSize;
310
704
  if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
311
- useEffect(() => {
705
+ useEffect2(() => {
312
706
  const container = containerRef.current;
313
707
  if (!container) return;
314
708
  if (pageCount === 0) return;
@@ -331,10 +725,9 @@ function MejiroScrollView({
331
725
  }
332
726
  }
333
727
  if (mostVisibleIdx >= 0) {
334
- onVisiblePageChangeRef.current?.(
335
- mostVisibleIdx,
336
- programmaticScrollRef.current ? "programmatic" : "user"
337
- );
728
+ const source = programmaticScrollRef.current ? "programmatic" : "user";
729
+ if (source === "user") userPageRef.current = mostVisibleIdx;
730
+ onVisiblePageChangeRef.current?.(mostVisibleIdx, source);
338
731
  }
339
732
  },
340
733
  { root: container, threshold: [0.25, 0.5, 0.75] }
@@ -347,8 +740,10 @@ function MejiroScrollView({
347
740
  useLayoutEffect(() => {
348
741
  if (scrollToPage == null) return;
349
742
  if (pageCount === 0) return;
743
+ if (scrollToPage === userPageRef.current) return;
350
744
  const el = pageRefs.current[scrollToPage];
351
745
  if (!(el && containerRef.current)) return;
746
+ userPageRef.current = null;
352
747
  programmaticScrollRef.current = true;
353
748
  containerRef.current.scrollTo({
354
749
  top: el.offsetTop,
@@ -359,12 +754,12 @@ function MejiroScrollView({
359
754
  }, 0);
360
755
  return () => clearTimeout(timer);
361
756
  }, [scrollToPage, pageCount]);
362
- 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(
363
758
  "div",
364
759
  {
365
760
  className: "mejiro-reader-scroll-track",
366
761
  style: { display: "flex", flexDirection: "column", gap: pageGap },
367
- children: pages.map((result, i) => /* @__PURE__ */ jsxs4(
762
+ children: pages.map((result, i) => /* @__PURE__ */ jsxs5(
368
763
  "div",
369
764
  {
370
765
  ref: (el) => {
@@ -372,24 +767,58 @@ function MejiroScrollView({
372
767
  },
373
768
  "data-page-idx": i,
374
769
  className: "mejiro-reader-page",
375
- 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
+ },
376
776
  children: [
377
- /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-page-rule" }),
378
- /* @__PURE__ */ jsxs4("div", { className: "mejiro-reader-page-header", children: [
379
- /* @__PURE__ */ jsx7("span", { className: "mejiro-reader-page-header-title" }),
380
- /* @__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 })
381
781
  ] }),
382
- /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: /* @__PURE__ */ jsx7(
383
- 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,
384
804
  {
385
- result,
386
- slotMode: useSlot,
387
- fontFamily,
388
- lineSpacing,
389
- className: "mejiro-reader-page-content",
390
- style: contentStyle
391
- }
392
- ) }) })
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
+ ))
393
822
  ]
394
823
  },
395
824
  i
@@ -400,7 +829,8 @@ function MejiroScrollView({
400
829
 
401
830
  // src/MejiroSettingsPanel.tsx
402
831
  import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
403
- 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";
404
834
  var DEFAULT_FONTS = [
405
835
  { value: "serif", label: "System Serif" },
406
836
  { value: "sans-serif", label: "System Sans" }
@@ -417,6 +847,48 @@ function parseClampedInput(value, min, max, fallback) {
417
847
  if (value.trim() === "") return fallback;
418
848
  return clampNumber(Number(value), min, max, fallback);
419
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
+ }
420
892
  function MejiroSettingsPanel({
421
893
  open,
422
894
  settings,
@@ -429,216 +901,127 @@ function MejiroSettingsPanel({
429
901
  const patch = (next) => onChange({ ...settings, ...next });
430
902
  const currentFont = normalizeFontFamily3(settings.fontFamily);
431
903
  const fontOptions = fonts.some((f) => f.value === currentFont) ? fonts : [{ value: currentFont, label: fontLabelFromCss(currentFont) }, ...fonts];
432
- 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: [
433
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-settings-group", children: [
434
- /* @__PURE__ */ jsx8("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
435
- /* @__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)) }) }),
436
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-control", children: [
437
- /* @__PURE__ */ jsx8("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-font-size", children: messages.settingsSize }),
438
- /* @__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(
439
911
  "button",
440
912
  {
441
913
  type: "button",
442
914
  className: "mejiro-reader-btn mejiro-reader-btn--icon",
443
915
  "aria-label": messages.settingsSizeDown,
444
- onClick: () => patch({ fontSize: Math.max(minFontSize, settings.fontSize - 1) }),
916
+ onClick: () => patch({
917
+ fontSize: clampNumber(
918
+ settings.fontSize - 1,
919
+ minFontSize,
920
+ maxFontSize,
921
+ minFontSize
922
+ )
923
+ }),
445
924
  children: "A\u2212"
446
925
  }
447
926
  ),
448
- /* @__PURE__ */ jsx8(
449
- "input",
927
+ /* @__PURE__ */ jsx10(
928
+ CommittedNumberInput,
450
929
  {
451
930
  id: "mejiro-reader-font-size",
452
- type: "number",
453
931
  value: settings.fontSize,
454
932
  min: minFontSize,
455
933
  max: maxFontSize,
456
- onChange: (e) => patch({
457
- fontSize: parseClampedInput(
458
- e.target.value,
459
- minFontSize,
460
- maxFontSize,
461
- settings.fontSize
462
- )
463
- })
934
+ onCommit: (fontSize) => patch({ fontSize })
464
935
  }
465
936
  ),
466
- /* @__PURE__ */ jsx8(
937
+ /* @__PURE__ */ jsx10(
467
938
  "button",
468
939
  {
469
940
  type: "button",
470
941
  className: "mejiro-reader-btn mejiro-reader-btn--icon",
471
942
  "aria-label": messages.settingsSizeUp,
472
- onClick: () => patch({ fontSize: Math.min(maxFontSize, settings.fontSize + 1) }),
943
+ onClick: () => patch({
944
+ fontSize: clampNumber(
945
+ settings.fontSize + 1,
946
+ minFontSize,
947
+ maxFontSize,
948
+ minFontSize
949
+ )
950
+ }),
473
951
  children: "A+"
474
952
  }
475
953
  )
476
954
  ] })
477
955
  ] }),
478
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-settings-group", children: [
479
- /* @__PURE__ */ jsx8("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
480
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-control", children: [
481
- /* @__PURE__ */ jsx8("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-kinsoku", children: messages.settingsKinsoku }),
482
- /* @__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(
483
961
  "select",
484
962
  {
485
963
  id: "mejiro-reader-kinsoku",
486
964
  value: settings.mode ?? "strict",
487
965
  onChange: (e) => patch({ mode: e.target.value }),
488
966
  children: [
489
- /* @__PURE__ */ jsx8("option", { value: "strict", children: messages.settingsStrict }),
490
- /* @__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 })
491
969
  ]
492
970
  }
493
971
  )
494
972
  ] }),
495
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-control", children: [
496
- /* @__PURE__ */ jsx8("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-hanging", children: messages.settingsHanging }),
497
- /* @__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(
498
976
  "select",
499
977
  {
500
978
  id: "mejiro-reader-hanging",
501
979
  value: String(settings.enableHanging ?? true),
502
980
  onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
503
981
  children: [
504
- /* @__PURE__ */ jsx8("option", { value: "true", children: messages.toggleOn }),
505
- /* @__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 })
506
984
  ]
507
985
  }
508
986
  )
509
987
  ] }),
510
- /* @__PURE__ */ jsxs5("div", { className: "mejiro-reader-control", children: [
511
- /* @__PURE__ */ jsx8("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-line-spacing", children: messages.settingsLineSpacing }),
512
- /* @__PURE__ */ jsx8(
513
- "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,
514
992
  {
515
993
  id: "mejiro-reader-line-spacing",
516
994
  className: "mejiro-reader-control--wide",
517
- type: "number",
518
995
  value: settings.lineSpacing ?? 1.8,
519
- min: 1,
520
- max: 3,
521
- step: 0.1,
522
- onChange: (e) => patch({
523
- lineSpacing: parseClampedInput(
524
- e.target.value,
525
- 1,
526
- 3,
527
- settings.lineSpacing ?? 1.8
528
- )
529
- })
530
- }
531
- )
532
- ] })
533
- ] })
534
- ] }) }) });
535
- }
536
-
537
- // src/MejiroSpread.tsx
538
- import { normalizeFontFamily as normalizeFontFamily4 } from "@libraz/mejiro/browser";
539
- import {
540
- useRef as useRef3
541
- } from "react";
542
-
543
- // src/MejiroImageOverlay.tsx
544
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
545
- function MejiroImageOverlay({
546
- rect,
547
- label,
548
- onOverlayPointerDown,
549
- onResizePointerDown,
550
- onClose
551
- }) {
552
- const messages = useI18n();
553
- const resolvedLabel = label ?? messages.imageButton;
554
- return /* @__PURE__ */ jsxs6(
555
- "div",
556
- {
557
- className: "mejiro-reader-image-overlay",
558
- style: {
559
- left: rect.x,
560
- top: rect.y,
561
- width: rect.w,
562
- height: rect.h
563
- },
564
- onPointerDown: onOverlayPointerDown,
565
- children: [
566
- /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-image-overlay-label", children: [
567
- /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-image-overlay-icon" }),
568
- /* @__PURE__ */ jsx9("span", { children: resolvedLabel })
569
- ] }),
570
- /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
571
- /* @__PURE__ */ jsx9(
572
- "button",
573
- {
574
- type: "button",
575
- "aria-label": messages.imageRemoveButton,
576
- title: messages.imageRemoveButton,
577
- className: "mejiro-reader-image-overlay-close",
578
- onPointerDown: (e) => {
579
- e.stopPropagation();
580
- e.preventDefault();
581
- onClose?.();
582
- },
583
- onKeyDown: (e) => {
584
- if (e.key !== "Enter" && e.key !== " ") return;
585
- e.stopPropagation();
586
- e.preventDefault();
587
- onClose?.();
588
- }
996
+ min: 1,
997
+ max: 3,
998
+ step: 0.1,
999
+ onCommit: (lineSpacing) => patch({ lineSpacing })
589
1000
  }
590
1001
  )
591
- ]
592
- }
593
- );
594
- }
595
-
596
- // src/MejiroSelectionLayer.tsx
597
- import { jsx as jsx10 } from "react/jsx-runtime";
598
- function MejiroSelectionLayer({
599
- rects,
600
- side,
601
- className,
602
- style,
603
- rectClassName,
604
- rectStyle
605
- }) {
606
- const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
607
- const rectClass = rectClassName ?? "mejiro-selection-rect";
608
- return /* @__PURE__ */ jsx10(
609
- "div",
610
- {
611
- className: wrapperClass,
612
- style: {
613
- position: "absolute",
614
- inset: 0,
615
- pointerEvents: "none",
616
- ...style
617
- },
618
- children: rects.map((r, i) => {
619
- if (r.side !== side) return null;
620
- const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
621
- return /* @__PURE__ */ jsx10(
622
- "div",
623
- {
624
- className: rectClass,
625
- style: {
626
- position: "absolute",
627
- width: r.width,
628
- height: r.height,
629
- ...posStyle,
630
- ...rectStyle
631
- }
632
- },
633
- i
634
- );
635
- })
636
- }
637
- );
1002
+ ] })
1003
+ ] })
1004
+ ] }) }) });
638
1005
  }
639
1006
 
640
1007
  // src/MejiroSpread.tsx
1008
+ import {
1009
+ isPrimaryPointerPress as isPrimaryPointerPress2,
1010
+ normalizeFontFamily as normalizeFontFamily4
1011
+ } from "@libraz/mejiro/browser";
1012
+ import {
1013
+ useRef as useRef5
1014
+ } from "react";
641
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
+ }
642
1025
  function defaultHeader(data) {
643
1026
  return /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-page-header", children: [
644
1027
  /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-page-header-title", children: data.title ?? "" }),
@@ -657,6 +1040,7 @@ function MejiroSpread({
657
1040
  rightHeader = {},
658
1041
  leftHeader = {},
659
1042
  images = [],
1043
+ slotMode,
660
1044
  renderPageHeader,
661
1045
  indicator,
662
1046
  onPrev,
@@ -665,24 +1049,25 @@ function MejiroSpread({
665
1049
  onImageResizePointerDown,
666
1050
  onImageClose,
667
1051
  anchorAtCoord,
1052
+ spreadIdx,
668
1053
  selectionRects,
669
1054
  onSelectionChange,
670
1055
  singlePage = false,
1056
+ singleSide = "right",
671
1057
  onSwipe,
672
1058
  onSurfaceTap
673
1059
  }) {
674
1060
  const messages = useI18n();
675
- const hasImages = images.length > 0;
676
- const useSlot = true;
677
- const selectionStartRef = useRef3(null);
1061
+ const selectionRef = useRef5(null);
678
1062
  const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
679
- const gestureRef = useRef3(null);
1063
+ const gestureRef = useRef5(null);
680
1064
  const gestureEnabled = onSwipe != null || onSurfaceTap != null;
681
1065
  const SWIPE_THRESHOLD = 40;
682
1066
  const TAP_MOVE_THRESHOLD = 8;
683
1067
  function handleGesturePointerDown(e) {
684
1068
  if (!gestureEnabled) return;
685
- if (e.pointerType === "mouse" && e.button !== 0) return;
1069
+ if (!isPrimaryPointerPress2(e)) return;
1070
+ if (e.target?.closest(OVERLAY_SELECTOR)) return;
686
1071
  gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
687
1072
  }
688
1073
  function handleGesturePointerUp(e) {
@@ -700,15 +1085,13 @@ function MejiroSpread({
700
1085
  }
701
1086
  if (ax < TAP_MOVE_THRESHOLD && ay < TAP_MOVE_THRESHOLD) {
702
1087
  const target = e.target;
703
- if (target?.closest("button, a, .mejiro-reader-image-overlay")) return;
1088
+ if (target?.closest(`button, a, ${OVERLAY_SELECTOR}`)) return;
704
1089
  onSurfaceTap?.();
705
1090
  }
706
1091
  }
707
1092
  function resolvePointer(e) {
708
1093
  if (!anchorAtCoord) return null;
709
- const target = e.target;
710
- if (!target) return null;
711
- const content = target.closest(".mejiro-reader-page-content");
1094
+ const content = contentAtPoint(e.currentTarget, e.clientX, e.clientY);
712
1095
  if (!content) return null;
713
1096
  const pageEl = content.closest(".mejiro-reader-page");
714
1097
  const isRight = pageEl?.classList.contains("mejiro-reader-page--right") ?? true;
@@ -719,26 +1102,28 @@ function MejiroSpread({
719
1102
  return anchorAtCoord(spreadX, offsetY);
720
1103
  }
721
1104
  function handlePointerDown(e) {
722
- if (!selectionEnabled) return;
1105
+ if (!selectionEnabled || selectionRef.current) return;
1106
+ if (!e.target?.closest(CONTENT_SELECTOR)) return;
723
1107
  const anchor = resolvePointer(e);
724
1108
  if (!anchor) return;
725
1109
  e.preventDefault();
726
- selectionStartRef.current = anchor;
1110
+ selectionRef.current = { start: anchor, pointerId: e.pointerId };
727
1111
  e.currentTarget.setPointerCapture(e.pointerId);
728
1112
  onSelectionChange?.(null);
729
1113
  }
730
1114
  function handlePointerMove(e) {
731
- if (!selectionEnabled) return;
732
- const start = selectionStartRef.current;
733
- if (!start) return;
1115
+ const session = selectionRef.current;
1116
+ if (!session || e.pointerId !== session.pointerId) return;
734
1117
  const end = resolvePointer(e);
735
1118
  if (!end) return;
1119
+ const { start } = session;
736
1120
  if (end.paragraph === start.paragraph && end.charIndex === start.charIndex) return;
737
1121
  onSelectionChange?.({ start, end });
738
1122
  }
739
- function handlePointerUp(e) {
740
- if (!selectionEnabled) return;
741
- selectionStartRef.current = null;
1123
+ function endSelection(e) {
1124
+ const session = selectionRef.current;
1125
+ if (!session || e.pointerId !== session.pointerId) return;
1126
+ selectionRef.current = null;
742
1127
  if (e.currentTarget.hasPointerCapture(e.pointerId)) {
743
1128
  e.currentTarget.releasePointerCapture(e.pointerId);
744
1129
  }
@@ -747,8 +1132,13 @@ function MejiroSpread({
747
1132
  if (fontFamily) contentStyle.fontFamily = normalizeFontFamily4(fontFamily);
748
1133
  if (fontSize != null) contentStyle.fontSize = fontSize;
749
1134
  if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
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;
750
1139
  const renderPage = (side) => {
751
1140
  const isRight = side === "right";
1141
+ const hostsImages = side === imageSide;
752
1142
  const result = isRight ? spread.right : spread.left;
753
1143
  const header = isRight ? rightHeader : leftHeader;
754
1144
  const pageKey = `${side}-${header.pageNumber ?? "blank"}`;
@@ -756,7 +1146,7 @@ function MejiroSpread({
756
1146
  width: pageWidth,
757
1147
  height: pageHeight
758
1148
  };
759
- if (isRight && hasImages) pageStyle.overflow = "visible";
1149
+ if (hostsImages && hasImages) pageStyle.overflow = "visible";
760
1150
  return /* @__PURE__ */ jsxs7(
761
1151
  "div",
762
1152
  {
@@ -770,7 +1160,7 @@ function MejiroSpread({
770
1160
  MejiroPageView,
771
1161
  {
772
1162
  result,
773
- slotMode: useSlot,
1163
+ slotMode,
774
1164
  fontFamily,
775
1165
  lineSpacing,
776
1166
  className: "mejiro-reader-page-content",
@@ -778,12 +1168,12 @@ function MejiroSpread({
778
1168
  },
779
1169
  pageKey
780
1170
  ),
781
- selectionRects && selectionRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: selectionRects, side })
1171
+ visibleRects && visibleRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: visibleRects, side })
782
1172
  ] }) }),
783
- isRight && images.map((item) => /* @__PURE__ */ jsx11(
1173
+ hostsImages && hostedImages.map((item) => /* @__PURE__ */ jsx11(
784
1174
  MejiroImageOverlay,
785
1175
  {
786
- rect: item.rect,
1176
+ rect: isRight ? item.rect : { ...item.rect, x: item.rect.x + pageWidth },
787
1177
  onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
788
1178
  onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
789
1179
  onClose: () => onImageClose?.(item.id)
@@ -799,12 +1189,13 @@ function MejiroSpread({
799
1189
  if (gestureEnabled) handleGesturePointerDown(e);
800
1190
  if (selectionEnabled) handlePointerDown(e);
801
1191
  }
802
- function combinedPointerMove(e) {
803
- if (selectionEnabled) handlePointerMove(e);
804
- }
805
1192
  function combinedPointerUp(e) {
806
1193
  if (gestureEnabled) handleGesturePointerUp(e);
807
- if (selectionEnabled) handlePointerUp(e);
1194
+ endSelection(e);
1195
+ }
1196
+ function combinedPointerCancel(e) {
1197
+ gestureRef.current = null;
1198
+ endSelection(e);
808
1199
  }
809
1200
  const useCombined = selectionEnabled || gestureEnabled;
810
1201
  return /* @__PURE__ */ jsx11("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs7(
@@ -812,11 +1203,12 @@ function MejiroSpread({
812
1203
  {
813
1204
  className: `mejiro-reader-spread${turning ? " is-turning" : ""}${singlePage ? " mejiro-reader-spread--single" : ""}`,
814
1205
  onPointerDown: useCombined ? combinedPointerDown : void 0,
815
- onPointerMove: useCombined ? combinedPointerMove : void 0,
1206
+ onPointerMove: useCombined ? handlePointerMove : void 0,
816
1207
  onPointerUp: useCombined ? combinedPointerUp : void 0,
817
- onPointerCancel: useCombined ? combinedPointerUp : void 0,
1208
+ onPointerCancel: useCombined ? combinedPointerCancel : void 0,
1209
+ onLostPointerCapture: selectionEnabled ? endSelection : void 0,
818
1210
  children: [
819
- renderPage("right"),
1211
+ renderPage(singlePage ? singleSide : "right"),
820
1212
  !singlePage && renderPage("left"),
821
1213
  /* @__PURE__ */ jsx11(
822
1214
  "button",
@@ -855,7 +1247,10 @@ function MejiroStats({
855
1247
  readingTimeLocale = "ja"
856
1248
  }) {
857
1249
  if (!chapter) return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats" });
858
- const totalChars = chapter.paragraphs.reduce((s, p) => s + p.text.length, 0);
1250
+ const totalChars = chapter.paragraphs.reduce(
1251
+ (s, p) => p.headingLevel != null ? s : s + [...p.text].length,
1252
+ 0
1253
+ );
859
1254
  const totalRuby = chapter.paragraphs.reduce(
860
1255
  (s, p) => s + p.inlineAnnotations.filter((a) => a.kind === "ruby").length,
861
1256
  0
@@ -872,29 +1267,127 @@ function MejiroStats({
872
1267
  return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats", children: parts.filter(Boolean).join(" / ") });
873
1268
  }
874
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
+
875
1368
  // src/useChapterLayout.ts
876
1369
  import {
877
- useCallback,
878
- useEffect as useEffect2,
1370
+ useCallback as useCallback2,
1371
+ useEffect as useEffect4,
879
1372
  useLayoutEffect as useLayoutEffect2,
880
- useRef as useRef4,
881
- useState as useState2
1373
+ useRef as useRef6,
1374
+ useState as useState4
882
1375
  } from "react";
883
1376
  function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
884
1377
  const enableResize = options.enableResize ?? true;
885
1378
  const resizeDebounce = options.resizeDebounce ?? 120;
886
- const [layout, setLayout] = useState2(null);
887
- const [pageWidth, setPageWidth] = useState2(0);
888
- const [pageHeight, setPageHeight] = useState2(0);
889
- const [contentHeight, setContentHeight] = useState2(0);
890
- const [elapsedMs, setElapsedMs] = useState2(0);
891
- const layoutRef = useRef4(null);
892
- const requestIdRef = useRef4(0);
893
- 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);
894
1387
  layoutRef.current = layout;
895
- const optionsRef = useRef4(options);
1388
+ const optionsRef = useRef6(options);
896
1389
  optionsRef.current = options;
897
- const recompute = useCallback(
1390
+ const recompute = useCallback2(
898
1391
  async (opts = {}) => {
899
1392
  const blank = opts.blank ?? true;
900
1393
  const o = optionsRef.current;
@@ -933,13 +1426,20 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
933
1426
  setContentHeight(0);
934
1427
  setElapsedMs(0);
935
1428
  }, [recompute]);
936
- useEffect2(() => {
1429
+ useEffect4(() => {
937
1430
  void recompute();
938
1431
  return () => {
939
1432
  requestIdRef.current++;
940
1433
  };
941
1434
  }, [recompute]);
942
- 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(() => {
943
1443
  if (!enableResize) return;
944
1444
  const el = surface.current;
945
1445
  if (!el) return;
@@ -994,367 +1494,200 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
994
1494
 
995
1495
  // src/useEpub.ts
996
1496
  import { parseEpub } from "@libraz/mejiro/epub";
997
- 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";
998
1498
  function useEpub(options = {}) {
999
- const [epub, setEpub] = useState3(null);
1000
- const [loading, setLoading] = useState3(false);
1001
- const [error, setError] = useState3(null);
1002
- const requestIdRef = useRef5(0);
1003
- const onLoadRef = useRef5(options.onLoad);
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);
1004
1504
  onLoadRef.current = options.onLoad;
1005
- const onErrorRef = useRef5(options.onError);
1006
- onErrorRef.current = options.onError;
1007
- const fetchOptionsRef = useRef5(options.fetchOptions);
1008
- fetchOptionsRef.current = options.fetchOptions;
1009
- const fetchEpubRef = useRef5(options.fetchEpub);
1010
- fetchEpubRef.current = options.fetchEpub;
1011
- const loadBuffer = useCallback2(async (buffer) => {
1012
- const requestId = ++requestIdRef.current;
1013
- setLoading(true);
1014
- setError(null);
1015
- try {
1016
- const book = await parseEpub(buffer);
1017
- if (requestId !== requestIdRef.current) return null;
1018
- setEpub(book);
1019
- onLoadRef.current?.(book);
1020
- return book;
1021
- } catch (err) {
1022
- if (requestId === requestIdRef.current) {
1023
- const nextError = err instanceof Error ? err : new Error(String(err));
1024
- setError(nextError);
1025
- onErrorRef.current?.(nextError);
1026
- }
1027
- return null;
1028
- } finally {
1029
- if (requestId === requestIdRef.current) setLoading(false);
1030
- }
1031
- }, []);
1032
- const loadFile = useCallback2(async (file) => {
1033
- const requestId = ++requestIdRef.current;
1034
- setLoading(true);
1035
- setError(null);
1036
- try {
1037
- const book = await parseEpub(await file.arrayBuffer());
1038
- if (requestId !== requestIdRef.current) return null;
1039
- setEpub(book);
1040
- onLoadRef.current?.(book);
1041
- return book;
1042
- } catch (err) {
1043
- if (requestId === requestIdRef.current) {
1044
- const nextError = err instanceof Error ? err : new Error(String(err));
1045
- setError(nextError);
1046
- onErrorRef.current?.(nextError);
1047
- }
1048
- return null;
1049
- } finally {
1050
- if (requestId === requestIdRef.current) setLoading(false);
1051
- }
1052
- }, []);
1053
- const loadUrl = useCallback2(async (url) => {
1054
- const requestId = ++requestIdRef.current;
1055
- setLoading(true);
1056
- setError(null);
1057
- try {
1058
- let buffer;
1059
- if (fetchEpubRef.current) {
1060
- buffer = await fetchEpubRef.current(url);
1061
- } else {
1062
- const init = fetchOptionsRef.current;
1063
- const res = init ? await fetch(url, init) : await fetch(url);
1064
- if (!res.ok) return null;
1065
- buffer = await res.arrayBuffer();
1066
- }
1067
- const book = await parseEpub(buffer);
1068
- if (requestId !== requestIdRef.current) return null;
1069
- setEpub(book);
1070
- onLoadRef.current?.(book);
1071
- return book;
1072
- } catch (err) {
1073
- if (requestId === requestIdRef.current) {
1074
- const nextError = err instanceof Error ? err : new Error(String(err));
1075
- setError(nextError);
1076
- onErrorRef.current?.(nextError);
1077
- }
1078
- return null;
1079
- } finally {
1080
- if (requestId === requestIdRef.current) setLoading(false);
1081
- }
1082
- }, []);
1083
- const defaultUrl = options.defaultUrl;
1084
- useEffect3(() => {
1085
- if (defaultUrl) void loadUrl(defaultUrl);
1086
- }, [defaultUrl, loadUrl]);
1087
- const replaceEpub = useCallback2((book) => {
1088
- requestIdRef.current++;
1089
- setLoading(false);
1090
- setError(null);
1091
- setEpub(book);
1092
- }, []);
1093
- return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
1094
- }
1095
-
1096
- // src/useMejiroBook.ts
1097
- import { MejiroBook } from "@libraz/mejiro/book";
1098
- import { useCallback as useCallback3, useRef as useRef6, useState as useState4 } from "react";
1099
- function useMejiroBook(initial) {
1100
- const bookRef = useRef6(null);
1101
- if (!bookRef.current) bookRef.current = new MejiroBook(initial);
1102
- const [options, setLocal] = useState4(initial);
1103
- const setOptions = useCallback3(async (next) => {
1104
- setLocal((prev) => ({ ...prev, ...next }));
1105
- await bookRef.current?.setOptions(next);
1106
- }, []);
1107
- return { book: bookRef.current, options, setOptions };
1108
- }
1109
-
1110
- // src/useMultiImageOverlay.ts
1111
- import { moveImageOverlayRect, resizeImageOverlayRect } from "@libraz/mejiro";
1112
- import {
1113
- useCallback as useCallback4,
1114
- useEffect as useEffect4,
1115
- useMemo as useMemo3,
1116
- useRef as useRef7,
1117
- useState as useState5
1118
- } from "react";
1119
- var nextId = 0;
1120
- function useMultiImageOverlay(layout, spreadIdx, options = {}) {
1121
- const defW = options.defaultWidth ?? 120;
1122
- const defH = options.defaultHeight ?? 160;
1123
- const defX = options.defaultX ?? 80;
1124
- const defY = options.defaultY ?? 100;
1125
- const margin = options.margin;
1126
- const [images, setImages] = useState5(() => /* @__PURE__ */ new Map());
1127
- const imagesRef = useRef7(images);
1128
- imagesRef.current = images;
1129
- const layoutRef = useRef7(null);
1130
- layoutRef.current = layout;
1131
- const spreadIdxRef = useRef7(spreadIdx);
1132
- spreadIdxRef.current = spreadIdx;
1133
- const onUpdateRef = useRef7(options.onUpdate);
1134
- onUpdateRef.current = options.onUpdate;
1135
- const activeDragCleanupsRef = useRef7(/* @__PURE__ */ new Set());
1136
- useEffect4(
1137
- () => () => {
1138
- for (const cleanup of activeDragCleanupsRef.current) cleanup();
1139
- activeDragCleanupsRef.current.clear();
1140
- },
1141
- []
1142
- );
1143
- const syncSpread = useCallback4(
1144
- (si) => {
1145
- const lo = layoutRef.current;
1146
- if (!lo) return;
1147
- const items = imagesRef.current.get(si) ?? [];
1148
- const bookImages = items.map((it) => ({
1149
- x: it.rect.x,
1150
- y: it.rect.y,
1151
- w: it.rect.w,
1152
- h: it.rect.h,
1153
- margin
1154
- }));
1155
- lo.setImages(si, bookImages);
1156
- onUpdateRef.current?.(lo.getSpread(si));
1157
- },
1158
- [margin]
1159
- );
1160
- const commit = useCallback4(
1161
- (next, affectedSpread) => {
1162
- imagesRef.current = next;
1163
- setImages(next);
1164
- syncSpread(affectedSpread);
1165
- },
1166
- [syncSpread]
1167
- );
1168
- const findById = useCallback4(
1169
- (id) => {
1170
- for (const [s, list] of imagesRef.current) {
1171
- const i = list.findIndex((it) => it.id === id);
1172
- if (i >= 0) return { spread: s, index: i, item: list[i] };
1173
- }
1174
- return null;
1175
- },
1176
- []
1177
- );
1178
- const addImage = useCallback4(
1179
- (overrides) => {
1180
- const si = spreadIdxRef.current;
1181
- const list = imagesRef.current.get(si) ?? [];
1182
- const last = list[list.length - 1];
1183
- const baseX = last ? last.rect.x - 100 : defX;
1184
- const baseY = last ? last.rect.y + 40 : defY;
1185
- nextId += 1;
1186
- const item = {
1187
- id: `mejiro-img-${nextId}`,
1188
- rect: {
1189
- x: overrides?.x ?? baseX,
1190
- y: overrides?.y ?? baseY,
1191
- w: overrides?.w ?? defW,
1192
- h: overrides?.h ?? defH
1193
- }
1194
- };
1195
- const next = new Map(imagesRef.current);
1196
- next.set(si, [...list, item]);
1197
- commit(next, si);
1198
- return item;
1199
- },
1200
- [defX, defY, defW, defH, commit]
1201
- );
1202
- const removeImage = useCallback4(
1203
- (id) => {
1204
- const found = findById(id);
1205
- if (!found) return;
1206
- const next = new Map(imagesRef.current);
1207
- const list = (next.get(found.spread) ?? []).filter((it) => it.id !== id);
1208
- if (list.length === 0) next.delete(found.spread);
1209
- else next.set(found.spread, list);
1210
- commit(next, found.spread);
1211
- },
1212
- [findById, commit]
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) => {
1514
+ const requestId = ++requestIdRef.current;
1515
+ setLoading(true);
1516
+ setError(null);
1517
+ try {
1518
+ const book = await parseEpub(await read(), { limits: limitsRef.current });
1519
+ if (requestId !== requestIdRef.current) return null;
1520
+ setEpub(book);
1521
+ onLoadRef.current?.(book);
1522
+ return book;
1523
+ } catch (err) {
1524
+ if (requestId === requestIdRef.current) {
1525
+ const nextError = toError(err);
1526
+ setError(nextError);
1527
+ onErrorRef.current?.(nextError);
1528
+ }
1529
+ return null;
1530
+ } finally {
1531
+ if (requestId === requestIdRef.current) setLoading(false);
1532
+ }
1533
+ }, []);
1534
+ const loadBuffer = useCallback3((buffer) => load(async () => buffer), [load]);
1535
+ const loadFile = useCallback3(
1536
+ (file) => load(() => readEpubFile(file, limitsRef.current)),
1537
+ [load]
1213
1538
  );
1214
- const updateImage = useCallback4(
1215
- (id, rect) => {
1216
- const found = findById(id);
1217
- if (!found) return;
1218
- const next = new Map(imagesRef.current);
1219
- const list = (next.get(found.spread) ?? []).map(
1220
- (it) => it.id === id ? { ...it, rect: { ...it.rect, ...rect } } : it
1221
- );
1222
- next.set(found.spread, list);
1223
- commit(next, found.spread);
1224
- },
1225
- [findById, commit]
1539
+ const loadUrl = useCallback3(
1540
+ (url) => load(
1541
+ () => fetchEpubRef.current ? fetchEpubRef.current(url) : fetchEpubBuffer(url, fetchOptionsRef.current)
1542
+ ),
1543
+ [load]
1226
1544
  );
1227
- const clearImages = useCallback4(
1228
- (target) => {
1229
- if (target == null) {
1230
- const old = imagesRef.current;
1231
- imagesRef.current = /* @__PURE__ */ new Map();
1232
- setImages(/* @__PURE__ */ new Map());
1233
- layoutRef.current?.clearImages();
1234
- if (layoutRef.current) {
1235
- for (const si of old.keys()) onUpdateRef.current?.(layoutRef.current.getSpread(si));
1545
+ const defaultUrl = options.defaultUrl;
1546
+ useEffect5(() => {
1547
+ if (defaultUrl) void loadUrl(defaultUrl);
1548
+ }, [defaultUrl, loadUrl]);
1549
+ const replaceEpub = useCallback3((book) => {
1550
+ requestIdRef.current++;
1551
+ setLoading(false);
1552
+ setError(null);
1553
+ setEpub(book);
1554
+ }, []);
1555
+ return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
1556
+ }
1557
+
1558
+ // src/useMejiroBook.ts
1559
+ import { MejiroBook } from "@libraz/mejiro/book";
1560
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef8, useState as useState6 } from "react";
1561
+ function useMejiroBook(initial, options = {}) {
1562
+ const bookRef = useRef8(null);
1563
+ if (!bookRef.current) bookRef.current = new MejiroBook(initial);
1564
+ const [snapshot, setLocal] = useState6(initial);
1565
+ const optionsRef = useRef8(options);
1566
+ optionsRef.current = options;
1567
+ const pendingRef = useRef8(null);
1568
+ const flush = useCallback4(() => {
1569
+ const pending = pendingRef.current;
1570
+ if (!pending) return;
1571
+ pendingRef.current = null;
1572
+ if (pending.timer) clearTimeout(pending.timer);
1573
+ const { waiters } = pending;
1574
+ const applied = bookRef.current?.setOptions(pending.patch) ?? Promise.resolve();
1575
+ void applied.then(
1576
+ () => {
1577
+ for (const waiter of waiters) waiter.resolve();
1578
+ },
1579
+ (err) => {
1580
+ const error = toError(err);
1581
+ const handler = optionsRef.current.onError;
1582
+ if (handler) {
1583
+ handler(error);
1584
+ for (const waiter of waiters) waiter.resolve();
1585
+ } else {
1586
+ for (const waiter of waiters) waiter.reject(error);
1236
1587
  }
1588
+ }
1589
+ );
1590
+ }, []);
1591
+ const setOptions = useCallback4(
1592
+ (next) => {
1593
+ setLocal((prev) => mergeDefined(prev, next));
1594
+ const pending = pendingRef.current ?? { patch: {}, timer: null, waiters: [] };
1595
+ pendingRef.current = pending;
1596
+ pending.patch = mergeDefined(pending.patch, next);
1597
+ const settled = new Promise((resolve, reject) => {
1598
+ pending.waiters.push({ resolve, reject });
1599
+ });
1600
+ if (pending.timer) clearTimeout(pending.timer);
1601
+ const debounceMs = optionsRef.current.debounceMs ?? 0;
1602
+ if (debounceMs > 0) {
1603
+ pending.timer = setTimeout(() => {
1604
+ pending.timer = null;
1605
+ flush();
1606
+ }, debounceMs);
1237
1607
  } else {
1238
- const next = new Map(imagesRef.current);
1239
- next.delete(target);
1240
- commit(next, target);
1608
+ pending.timer = null;
1609
+ flush();
1241
1610
  }
1611
+ return settled;
1242
1612
  },
1243
- [commit]
1244
- );
1245
- const onOverlayPointerDown = useCallback4(
1246
- (id, e) => {
1247
- e.preventDefault();
1248
- const found = findById(id);
1249
- if (!found) return;
1250
- const startX = e.clientX;
1251
- const startY = e.clientY;
1252
- const start = { ...found.item.rect };
1253
- const target = e.currentTarget;
1254
- target.setPointerCapture(e.nativeEvent.pointerId);
1255
- target.classList.add("is-dragging");
1256
- let rafId = 0;
1257
- const onMove = (me) => {
1258
- const dx = me.clientX - startX;
1259
- const dy = me.clientY - startY;
1260
- const r = moveImageOverlayRect(start, dx, dy);
1261
- cancelAnimationFrame(rafId);
1262
- rafId = requestAnimationFrame(() => updateImage(id, r));
1263
- };
1264
- let cleanup;
1265
- const onUp = () => cleanup();
1266
- cleanup = () => {
1267
- cancelAnimationFrame(rafId);
1268
- target.classList.remove("is-dragging");
1269
- document.removeEventListener("pointermove", onMove);
1270
- document.removeEventListener("pointerup", onUp);
1271
- activeDragCleanupsRef.current.delete(cleanup);
1272
- };
1273
- document.addEventListener("pointermove", onMove);
1274
- document.addEventListener("pointerup", onUp);
1275
- activeDragCleanupsRef.current.add(cleanup);
1276
- },
1277
- [findById, updateImage]
1613
+ [flush]
1278
1614
  );
1279
- const onResizePointerDown = useCallback4(
1280
- (id, e) => {
1281
- e.preventDefault();
1282
- e.stopPropagation();
1283
- const found = findById(id);
1284
- if (!found) return;
1285
- const startX = e.clientX;
1286
- const startY = e.clientY;
1287
- const start = { ...found.item.rect };
1288
- const target = e.currentTarget;
1289
- target.setPointerCapture(e.nativeEvent.pointerId);
1290
- target.parentElement?.classList.add("is-dragging");
1291
- let rafId = 0;
1292
- const onMove = (me) => {
1293
- const dx = me.clientX - startX;
1294
- const dy = me.clientY - startY;
1295
- const r = resizeImageOverlayRect(start, dx, dy);
1296
- cancelAnimationFrame(rafId);
1297
- rafId = requestAnimationFrame(() => updateImage(id, r));
1298
- };
1299
- let cleanup;
1300
- const onUp = () => cleanup();
1301
- cleanup = () => {
1302
- cancelAnimationFrame(rafId);
1303
- target.parentElement?.classList.remove("is-dragging");
1304
- document.removeEventListener("pointermove", onMove);
1305
- document.removeEventListener("pointerup", onUp);
1306
- activeDragCleanupsRef.current.delete(cleanup);
1307
- };
1308
- document.addEventListener("pointermove", onMove);
1309
- document.addEventListener("pointerup", onUp);
1310
- activeDragCleanupsRef.current.add(cleanup);
1615
+ useEffect6(
1616
+ () => () => {
1617
+ bookRef.current?.dispose();
1618
+ const pending = pendingRef.current;
1619
+ if (!pending) return;
1620
+ pendingRef.current = null;
1621
+ if (pending.timer) clearTimeout(pending.timer);
1622
+ for (const waiter of pending.waiters) waiter.resolve();
1311
1623
  },
1312
- [findById, updateImage]
1624
+ []
1313
1625
  );
1314
- useEffect4(() => {
1315
- if (!layout) return;
1316
- for (const si of imagesRef.current.keys()) syncSpread(si);
1317
- }, [layout, syncSpread]);
1318
- const currentImages = useMemo3(() => images.get(spreadIdx) ?? [], [images, spreadIdx]);
1319
- const hasImages = useMemo3(() => [...images.values()].some((list) => list.length > 0), [images]);
1320
- return {
1321
- imagesBySpread: images,
1322
- currentImages,
1323
- hasImages,
1324
- addImage,
1325
- removeImage,
1326
- updateImage,
1327
- clearImages,
1328
- onOverlayPointerDown,
1329
- onResizePointerDown
1330
- };
1626
+ return { book: bookRef.current, options: snapshot, setOptions };
1331
1627
  }
1332
1628
 
1333
1629
  // src/useSpread.ts
1334
- import { useCallback as useCallback5, useEffect as useEffect5, 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";
1631
+ function ignoreNavigationKey(e) {
1632
+ if (e.defaultPrevented) return true;
1633
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return true;
1634
+ const target = e.target;
1635
+ if (!target) return false;
1636
+ if (target.isContentEditable) return true;
1637
+ const tag = target.tagName;
1638
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
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
+ }
1335
1656
  function useSpread(layout, options = {}) {
1336
1657
  const enableKeyboard = options.enableKeyboard ?? true;
1337
1658
  const turnDuration = options.turnDuration ?? 180;
1338
- const onChangeRef = useRef8(options.onChange);
1659
+ const single = options.single ?? false;
1660
+ const onChangeRef = useRef9(options.onChange);
1339
1661
  onChangeRef.current = options.onChange;
1340
- const [spreadIdx, setSpreadIdx] = useState6(0);
1341
- const [spread, setSpread] = useState6(null);
1342
- const [turning, setTurning] = useState6(false);
1343
- 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);
1344
1671
  layoutRef.current = layout;
1345
- const spreadIdxRef = useRef8(0);
1672
+ const spreadIdxRef = useRef9(0);
1346
1673
  spreadIdxRef.current = spreadIdx;
1347
- const turnTimerRef = useRef8(null);
1348
- const layoutGenerationRef = useRef8(0);
1349
- const lastEmittedRef = useRef8(null);
1350
- const totalPages = layout?.totalPages ?? 0;
1351
- 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;
1352
1684
  const refresh = useCallback5(() => {
1353
1685
  if (!layoutRef.current) {
1354
1686
  setSpread(null);
1355
1687
  return;
1356
1688
  }
1357
- setSpread(layoutRef.current.getSpread(spreadIdxRef.current));
1689
+ const page = firstPageOf(spreadIdxRef.current, singleRef.current);
1690
+ setSpread(layoutRef.current.getSpread(Math.floor(page / 2)));
1358
1691
  }, []);
1359
1692
  useLayoutEffect3(() => {
1360
1693
  layoutGenerationRef.current++;
@@ -1370,9 +1703,9 @@ function useSpread(layout, options = {}) {
1370
1703
  }
1371
1704
  setSpread(layout.getSpread(0));
1372
1705
  }, [layout]);
1373
- useEffect5(() => {
1706
+ useEffect7(() => {
1374
1707
  if (!layout) return;
1375
- setSpread(layout.getSpread(spreadIdx));
1708
+ setSpread(layout.getSpread(layoutSpreadIdx));
1376
1709
  const last = lastEmittedRef.current;
1377
1710
  if (!last) {
1378
1711
  lastEmittedRef.current = { layout, spreadIdx };
@@ -1382,11 +1715,12 @@ function useSpread(layout, options = {}) {
1382
1715
  onChangeRef.current?.(spreadIdx);
1383
1716
  }
1384
1717
  lastEmittedRef.current = { layout, spreadIdx };
1385
- }, [layout, spreadIdx]);
1718
+ }, [layout, spreadIdx, layoutSpreadIdx]);
1386
1719
  const goTo = useCallback5(
1387
1720
  (index) => {
1388
1721
  if (!layoutRef.current) return;
1389
- 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;
1390
1724
  const target = Math.max(0, Math.min(max, index));
1391
1725
  if (target === spreadIdxRef.current) return;
1392
1726
  if (turnDuration > 0) {
@@ -1395,7 +1729,7 @@ function useSpread(layout, options = {}) {
1395
1729
  setTurning(true);
1396
1730
  turnTimerRef.current = setTimeout(() => {
1397
1731
  if (generation !== layoutGenerationRef.current) return;
1398
- setSpreadIdx(target);
1732
+ setSpreadIdx(indexOfPageIn(firstPageOf(target, targetSingle), singleRef.current));
1399
1733
  setTurning(false);
1400
1734
  turnTimerRef.current = null;
1401
1735
  }, turnDuration);
@@ -1412,22 +1746,31 @@ function useSpread(layout, options = {}) {
1412
1746
  turnTimerRef.current = null;
1413
1747
  }
1414
1748
  setTurning(false);
1415
- const max = Math.max(1, Math.ceil(layoutRef.current.totalPages / 2)) - 1;
1749
+ const max = navigationCount(layoutRef.current.totalPages, singleRef.current) - 1;
1416
1750
  setSpreadIdx(Math.max(0, Math.min(max, index)));
1417
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
+ );
1418
1760
  const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
1419
1761
  const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
1420
- useEffect5(() => {
1762
+ useEffect7(() => {
1421
1763
  if (!enableKeyboard) return;
1422
1764
  const onKey = (e) => {
1423
1765
  if (!layoutRef.current) return;
1766
+ if (ignoreNavigationKey(e)) return;
1424
1767
  if (e.key === "ArrowLeft") next();
1425
1768
  else if (e.key === "ArrowRight") prev();
1426
1769
  };
1427
1770
  window.addEventListener("keydown", onKey);
1428
1771
  return () => window.removeEventListener("keydown", onKey);
1429
1772
  }, [enableKeyboard, next, prev]);
1430
- useEffect5(
1773
+ useEffect7(
1431
1774
  () => () => {
1432
1775
  if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
1433
1776
  },
@@ -1436,6 +1779,9 @@ function useSpread(layout, options = {}) {
1436
1779
  return {
1437
1780
  spreadIdx,
1438
1781
  spread,
1782
+ firstPage,
1783
+ layoutSpreadIdx,
1784
+ singleSide,
1439
1785
  totalPages,
1440
1786
  totalSpreads,
1441
1787
  turning,
@@ -1443,12 +1789,61 @@ function useSpread(layout, options = {}) {
1443
1789
  prev,
1444
1790
  goTo,
1445
1791
  setSpread: setSpreadIndex,
1792
+ indexOfPage,
1793
+ anchorAt,
1446
1794
  refresh
1447
1795
  };
1448
1796
  }
1449
1797
 
1450
1798
  // src/MejiroReader.tsx
1451
1799
  import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
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
+ }
1452
1847
  function MejiroReaderInner(props, ref) {
1453
1848
  const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
1454
1849
  const dialectProp = "dialect" in props ? props.dialect : void 0;
@@ -1467,6 +1862,7 @@ function MejiroReaderInner(props, ref) {
1467
1862
  enableSurfaceTap = true,
1468
1863
  fallback,
1469
1864
  fetchOptions,
1865
+ limits,
1470
1866
  fetchEpub: fetchEpubFn,
1471
1867
  locale,
1472
1868
  messages,
@@ -1494,15 +1890,18 @@ function MejiroReaderInner(props, ref) {
1494
1890
  onPageRead,
1495
1891
  onChapterCompleted
1496
1892
  } = props;
1497
- const surfaceRef = useRef9(null);
1498
- const fileRef = useRef9(null);
1499
- const [settingsOpen, setSettingsOpen] = useState7(false);
1500
- 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);
1501
1897
  const chapterIsUncontrolled = chapterProp == null;
1502
1898
  const chapter = chapterProp ?? chapterState;
1503
- const [chromeHidden, setChromeHidden] = useState7(false);
1504
- const [autoSingle, setAutoSingle] = useState7(false);
1505
- useEffect6(() => {
1899
+ const [chromeHidden, setChromeHidden] = useState8(false);
1900
+ const [userScroll, setUserScroll] = useState8(
1901
+ null
1902
+ );
1903
+ const [autoSingle, setAutoSingle] = useState8(false);
1904
+ useEffect8(() => {
1506
1905
  if (!settingsOpen) return;
1507
1906
  const onKey = (e2) => {
1508
1907
  if (e2.key === "Escape") setSettingsOpen(false);
@@ -1510,7 +1909,7 @@ function MejiroReaderInner(props, ref) {
1510
1909
  window.addEventListener("keydown", onKey);
1511
1910
  return () => window.removeEventListener("keydown", onKey);
1512
1911
  }, [settingsOpen]);
1513
- useEffect6(() => {
1912
+ useEffect8(() => {
1514
1913
  if (spreadMode !== "auto") return;
1515
1914
  const surface = surfaceRef.current;
1516
1915
  if (!surface) return;
@@ -1527,16 +1926,19 @@ function MejiroReaderInner(props, ref) {
1527
1926
  const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
1528
1927
  const resolvedGeometry = useMemo4(() => {
1529
1928
  const columns = effectiveSingle ? 1 : 2;
1530
- if (fit !== "width") return { columns, ...pageGeometryProp };
1531
- return {
1532
- columns,
1533
- gutterOffset: 0,
1534
- headerOffset: 0,
1535
- maxHeight: Number.POSITIVE_INFINITY,
1536
- minWidth: 0,
1537
- minHeight: 0,
1538
- ...pageGeometryProp
1539
- };
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
+ );
1540
1942
  }, [fit, pageGeometryProp, effectiveSingle]);
1541
1943
  const surfaceAspect = useMemo4(() => {
1542
1944
  const columns = effectiveSingle ? 1 : 2;
@@ -1544,103 +1946,135 @@ function MejiroReaderInner(props, ref) {
1544
1946
  return `${columns} / ${aspect}`;
1545
1947
  }, [effectiveSingle, resolvedGeometry]);
1546
1948
  const resolvedOptions = useMemo4(
1547
- () => ({ ...DEFAULT_BOOK_OPTIONS, ...optionsProp ?? {} }),
1949
+ () => mergeDefined(DEFAULT_BOOK_OPTIONS, optionsProp ?? {}),
1548
1950
  [optionsProp]
1549
1951
  );
1550
- const { book, options: bookOptions, setOptions } = useMejiroBook(resolvedOptions);
1551
- const didSyncOptionsPropRef = useRef9(false);
1552
- useEffect6(() => {
1553
- if (!didSyncOptionsPropRef.current) {
1554
- didSyncOptionsPropRef.current = true;
1555
- return;
1556
- }
1557
- void setOptions(resolvedOptions);
1558
- }, [resolvedOptions, setOptions]);
1952
+ const onErrorRef = useRef10(onError);
1953
+ onErrorRef.current = onError;
1954
+ const reportError = useCallback6((error) => onErrorRef.current?.(error), []);
1955
+ const {
1956
+ book,
1957
+ options: bookOptions,
1958
+ setOptions
1959
+ } = useMejiroBook(resolvedOptions, {
1960
+ debounceMs: OPTIONS_DEBOUNCE_MS,
1961
+ onError: reportError
1962
+ });
1963
+ const optionsPropKey = JSON.stringify(resolvedOptions);
1964
+ const syncedOptionsPropKeyRef = useRef10(optionsPropKey);
1965
+ const resolvedOptionsRef = useRef10(resolvedOptions);
1966
+ resolvedOptionsRef.current = resolvedOptions;
1967
+ useEffect8(() => {
1968
+ if (syncedOptionsPropKeyRef.current === optionsPropKey) return;
1969
+ syncedOptionsPropKeyRef.current = optionsPropKey;
1970
+ void setOptions(resolvedOptionsRef.current);
1971
+ }, [optionsPropKey, setOptions]);
1972
+ const manuscriptSourceKey = manuscriptKey(manuscriptProp);
1559
1973
  const synthesizedEpub = useMemo4(() => {
1560
- if (manuscriptProp === void 0) return null;
1561
- return manuscriptToEpubBook(manuscriptProp, { dialect: dialectProp });
1562
- }, [manuscriptProp, dialectProp]);
1974
+ if (manuscriptSourceKey === void 0) return null;
1975
+ const chapters = JSON.parse(manuscriptSourceKey);
1976
+ return manuscriptToEpubBook(chapters, { dialect: dialectProp });
1977
+ }, [manuscriptSourceKey, dialectProp]);
1563
1978
  const epubCtx = useEpub({
1564
1979
  // `epub` / `manuscript` take precedence: skip the URL fetch when a parsed
1565
1980
  // book (or a synthesized manuscript book) is supplied.
1566
1981
  defaultUrl: epubProp !== void 0 || manuscriptProp !== void 0 ? void 0 : epubUrl,
1567
1982
  fetchOptions,
1983
+ limits,
1568
1984
  fetchEpub: fetchEpubFn,
1569
1985
  onLoad: (b) => {
1570
1986
  if (chapterProp == null) setChapterState(0);
1987
+ clearImagesRef.current();
1571
1988
  onLoad?.(b);
1572
1989
  }
1573
1990
  });
1574
1991
  const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
1575
1992
  const isManuscriptSource = manuscriptProp !== void 0;
1576
1993
  const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
1577
- const onErrorRef = useRef9(onError);
1578
- onErrorRef.current = onError;
1579
- useEffect6(() => {
1994
+ useEffect8(() => {
1580
1995
  if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
1581
1996
  }, [epubCtx.error]);
1582
- const pageGeometryRef = useRef9(resolvedGeometry);
1997
+ const pageGeometryRef = useRef10(resolvedGeometry);
1583
1998
  pageGeometryRef.current = resolvedGeometry;
1584
- const curSpreadIdxRef = useRef9(0);
1585
- const spreadIdxPropRef = useRef9(spreadIdxProp);
1999
+ const spreadAnchorRef = useRef10(() => null);
2000
+ const spreadIdxPropRef = useRef10(spreadIdxProp);
1586
2001
  spreadIdxPropRef.current = spreadIdxProp;
2002
+ const visibleAnchorRef = useRef10(
2003
+ null
2004
+ );
1587
2005
  const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef, {
1588
2006
  pageGeometry: () => pageGeometryRef.current,
1589
2007
  // Preserve the reading position across a reflow re-layout (size / option
1590
2008
  // changes), but only in uncontrolled mode — when `spreadIdx` is controlled
1591
2009
  // the host owns the position and the controlled-restore effect handles it.
1592
- 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
+ }
1593
2016
  });
1594
- const spreadChangedRef = useRef9(void 0);
2017
+ const spreadChangedRef = useRef10(void 0);
1595
2018
  spreadChangedRef.current = (i) => {
1596
2019
  onSpreadChange?.(i);
1597
2020
  onSpreadIdxChange?.(i);
1598
2021
  };
1599
2022
  const spreadCtx = useSpread(layoutCtx.layout, {
1600
2023
  enableKeyboard,
2024
+ single: effectiveSingle,
1601
2025
  onChange: (i) => spreadChangedRef.current?.(i)
1602
2026
  });
1603
- curSpreadIdxRef.current = spreadCtx.spreadIdx;
1604
- 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);
1605
2039
  setSpreadRef.current = spreadCtx.setSpread;
1606
2040
  useLayoutEffect4(() => {
1607
2041
  const anchor = layoutCtx.pendingRestore.current;
1608
2042
  if (!(anchor && layoutCtx.layout)) return;
1609
2043
  layoutCtx.pendingRestore.current = null;
1610
2044
  const loc = layoutCtx.layout.locateAnchor(anchor);
1611
- setSpreadRef.current(loc?.spreadIdx ?? 0);
2045
+ setSpreadRef.current(loc ? indexOfPageRef.current(loc.pageIdx) : 0);
2046
+ visibleAnchorRef.current = { layout: layoutCtx.layout, anchor };
1612
2047
  }, [layoutCtx.layout]);
1613
- const optionsKey = [
1614
- bookOptions.fontFamily,
1615
- bookOptions.fontSize,
1616
- bookOptions.lineSpacing,
1617
- bookOptions.mode,
1618
- bookOptions.enableHanging
1619
- ].join("|");
1620
- const optionsKeyRef = useRef9(optionsKey);
1621
- const bookOptionsRef = useRef9(bookOptions);
2048
+ const optionsKey = reflowOptionsKey(bookOptions);
2049
+ const optionsKeyRef = useRef10(optionsKey);
2050
+ const bookOptionsRef = useRef10(bookOptions);
1622
2051
  bookOptionsRef.current = bookOptions;
1623
- const recomputeRef = useRef9(layoutCtx.recompute);
2052
+ const recomputeRef = useRef10(layoutCtx.recompute);
1624
2053
  recomputeRef.current = layoutCtx.recompute;
1625
- useEffect6(() => {
2054
+ useEffect8(() => {
1626
2055
  if (optionsKeyRef.current === optionsKey) return;
1627
2056
  optionsKeyRef.current = optionsKey;
1628
2057
  const timer = setTimeout(() => {
1629
2058
  void (async () => {
1630
- await setOptions({ ...bookOptionsRef.current });
1631
- await recomputeRef.current({ blank: false });
2059
+ try {
2060
+ await setOptions({ ...bookOptionsRef.current });
2061
+ await recomputeRef.current({ blank: false });
2062
+ } catch (err) {
2063
+ reportError(toError(err));
2064
+ }
1632
2065
  })();
1633
- }, 60);
2066
+ }, OPTIONS_DEBOUNCE_MS);
1634
2067
  return () => clearTimeout(timer);
1635
- }, [optionsKey, setOptions]);
2068
+ }, [optionsKey, setOptions, reportError]);
1636
2069
  const geometryKey = JSON.stringify(resolvedGeometry ?? null);
1637
- const geometryKeyRef = useRef9(geometryKey);
1638
- useEffect6(() => {
2070
+ const geometryKeyRef = useRef10(geometryKey);
2071
+ useEffect8(() => {
1639
2072
  if (geometryKeyRef.current === geometryKey) return;
1640
2073
  geometryKeyRef.current = geometryKey;
1641
2074
  void recomputeRef.current({ blank: false });
1642
2075
  }, [geometryKey]);
1643
- const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.spreadIdx, {
2076
+ const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.layoutSpreadIdx, {
2077
+ defaultX: IMAGE_DEFAULT_X,
1644
2078
  onUpdate: () => spreadCtx.refresh()
1645
2079
  });
1646
2080
  const annotationRects = useMemo4(() => {
@@ -1652,27 +2086,44 @@ function MejiroReaderInner(props, ref) {
1652
2086
  start: annotation.start,
1653
2087
  end: annotation.end
1654
2088
  });
1655
- for (const rect of rects) result.push(rect);
2089
+ for (const rect of rects) result.push({ ...rect, color: annotation.color });
1656
2090
  }
1657
2091
  return result;
1658
- }, [annotations, layoutCtx.layout, chapter]);
1659
- const clearImagesRef = useRef9(imageCtx.clearImages);
2092
+ }, [annotations, layoutCtx.layout, chapter, imageCtx.currentImages]);
2093
+ const clearImagesRef = useRef10(imageCtx.clearImages);
1660
2094
  clearImagesRef.current = imageCtx.clearImages;
1661
- const onLoadRef = useRef9(onLoad);
2095
+ const onLoadRef = useRef10(onLoad);
1662
2096
  onLoadRef.current = onLoad;
1663
- useEffect6(() => {
2097
+ const eRef = useRef10(e);
2098
+ eRef.current = e;
2099
+ const sourceKey = isManuscriptSource ? MANUSCRIPT_SOURCE : epubProp;
2100
+ useEffect8(() => {
1664
2101
  if (!controlled) return;
1665
2102
  if (chapterIsUncontrolled) setChapterState(0);
1666
2103
  clearImagesRef.current();
1667
2104
  book.clearCache();
1668
- if (e) onLoadRef.current?.(e);
1669
- }, [controlled, e, book, chapterIsUncontrolled]);
1670
- 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);
1671
2114
  spreadGoToRef.current = spreadCtx.goTo;
1672
- useEffect6(() => {
1673
- if (spreadIdxProp == null) return;
1674
- spreadGoToRef.current(spreadIdxProp);
1675
- }, [spreadIdxProp]);
2115
+ const appliedSpreadIdxPropRef = useRef10(spreadIdxProp);
2116
+ useEffect8(() => {
2117
+ if (spreadIdxProp == null) {
2118
+ appliedSpreadIdxPropRef.current = spreadIdxProp;
2119
+ return;
2120
+ }
2121
+ const propChanged = appliedSpreadIdxPropRef.current !== spreadIdxProp;
2122
+ appliedSpreadIdxPropRef.current = spreadIdxProp;
2123
+ if (spreadCtx.spreadIdx === spreadIdxProp) return;
2124
+ if (propChanged) spreadGoToRef.current(spreadIdxProp);
2125
+ else setSpreadRef.current(spreadIdxProp);
2126
+ }, [spreadIdxProp, spreadCtx.spreadIdx]);
1676
2127
  useLayoutEffect4(() => {
1677
2128
  const next = spreadIdxPropRef.current;
1678
2129
  if (next == null || !layoutCtx.layout) return;
@@ -1686,7 +2137,7 @@ function MejiroReaderInner(props, ref) {
1686
2137
  },
1687
2138
  [chapter, chapterProp, onChapterChange]
1688
2139
  );
1689
- const listenersRef = useRef9(/* @__PURE__ */ new Map());
2140
+ const listenersRef = useRef10(/* @__PURE__ */ new Map());
1690
2141
  const emit = useCallback6(
1691
2142
  (event, payload) => {
1692
2143
  const set = listenersRef.current.get(event);
@@ -1695,8 +2146,15 @@ function MejiroReaderInner(props, ref) {
1695
2146
  },
1696
2147
  []
1697
2148
  );
1698
- const pendingAnchorRef = useRef9(null);
1699
- 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;
1700
2158
  const tryApplyPendingAnchor = useCallback6(() => {
1701
2159
  const pending = pendingAnchorRef.current;
1702
2160
  if (!pending) return;
@@ -1708,48 +2166,45 @@ function MejiroReaderInner(props, ref) {
1708
2166
  });
1709
2167
  if (!loc) return;
1710
2168
  pendingAnchorRef.current = null;
1711
- spreadGoToRef.current(loc.spreadIdx);
2169
+ spreadGoToRef.current(indexOfPageRef.current(loc.pageIdx));
1712
2170
  pending.resolve();
1713
2171
  }, [chapter, layout]);
1714
- useEffect6(() => {
2172
+ useEffect8(() => {
1715
2173
  tryApplyPendingAnchor();
1716
2174
  }, [tryApplyPendingAnchor]);
1717
- useEffect6(
2175
+ useEffect8(
1718
2176
  () => () => {
1719
2177
  pendingAnchorRef.current?.resolve();
1720
2178
  pendingAnchorRef.current = null;
1721
2179
  },
1722
2180
  []
1723
2181
  );
1724
- const didMountRef = useRef9(false);
1725
- const dwellRef = useRef9(null);
1726
- const onPageReadRef = useRef9(onPageRead);
2182
+ const onPageReadRef = useRef10(onPageRead);
1727
2183
  onPageReadRef.current = onPageRead;
1728
- const onChapterCompletedRef = useRef9(onChapterCompleted);
2184
+ const onChapterCompletedRef = useRef10(onChapterCompleted);
1729
2185
  onChapterCompletedRef.current = onChapterCompleted;
1730
- useEffect6(() => {
1731
- const now = typeof performance !== "undefined" ? performance.now() : Date.now();
1732
- if (didMountRef.current && dwellRef.current) {
1733
- onPageReadRef.current?.(dwellRef.current.anchor, now - dwellRef.current.ts);
1734
- }
1735
- if (layout) {
1736
- const inCh = layout.anchorAt(spreadCtx.spreadIdx, "right");
1737
- dwellRef.current = inCh ? { anchor: { chapter, ...inCh }, ts: now } : null;
1738
- } else {
1739
- dwellRef.current = null;
1740
- }
1741
- if (!didMountRef.current) {
1742
- didMountRef.current = true;
1743
- return;
1744
- }
1745
- emit("spreadChanged", { chapter, spreadIdx: spreadCtx.spreadIdx });
1746
- if (spreadCtx.totalSpreads > 0 && spreadCtx.spreadIdx === spreadCtx.totalSpreads - 1) {
1747
- emit("chapterFinished", { chapter });
1748
- onChapterCompletedRef.current?.(chapter);
1749
- }
1750
- }, [chapter, spreadCtx.spreadIdx, spreadCtx.totalSpreads, layout, emit]);
1751
- const prevTurningRef = useRef9(false);
1752
- useEffect6(() => {
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(() => {
1753
2208
  if (spreadCtx.turning && !prevTurningRef.current) {
1754
2209
  emit("turnStart", { from: spreadCtx.spreadIdx });
1755
2210
  } else if (!spreadCtx.turning && prevTurningRef.current) {
@@ -1778,23 +2233,14 @@ function MejiroReaderInner(props, ref) {
1778
2233
  }),
1779
2234
  getAnchor: () => {
1780
2235
  if (!layout) return null;
1781
- const inCh = layout.anchorAt(spreadCtx.spreadIdx, "right");
2236
+ const inCh = spreadCtx.anchorAt(spreadCtx.spreadIdx);
1782
2237
  return inCh ? { chapter, ...inCh } : null;
1783
2238
  },
1784
2239
  getVisibleRange: () => {
1785
2240
  if (!layout) return null;
1786
- const start = layout.anchorAt(spreadCtx.spreadIdx, "right");
2241
+ const start = spreadCtx.anchorAt(spreadCtx.spreadIdx);
1787
2242
  if (!start) return null;
1788
- const next = layout.anchorAt(spreadCtx.spreadIdx + 1, "right");
1789
- let end;
1790
- if (next) {
1791
- end = next;
1792
- } else {
1793
- const ch = e?.chapters[chapter];
1794
- const lastP = (ch?.paragraphs.length ?? 1) - 1;
1795
- const lastText = ch?.paragraphs[lastP]?.text ?? "";
1796
- end = { paragraph: Math.max(0, lastP), charIndex: [...lastText].length };
1797
- }
2243
+ const end = spreadCtx.anchorAt(spreadCtx.spreadIdx + 1) ?? layout.endAnchor() ?? start;
1798
2244
  return {
1799
2245
  start: { chapter, ...start },
1800
2246
  end: { chapter, ...end }
@@ -1819,10 +2265,10 @@ function MejiroReaderInner(props, ref) {
1819
2265
  spreadCtx.spreadIdx,
1820
2266
  spreadCtx.totalPages,
1821
2267
  spreadCtx.totalSpreads,
2268
+ spreadCtx.anchorAt,
1822
2269
  onChapter,
1823
2270
  chapter,
1824
2271
  layout,
1825
- e,
1826
2272
  setOptions,
1827
2273
  tryApplyPendingAnchor
1828
2274
  ]
@@ -1874,7 +2320,7 @@ function MejiroReaderInner(props, ref) {
1874
2320
  MejiroStats,
1875
2321
  {
1876
2322
  chapter: e?.chapters[chapter] ?? null,
1877
- totalPages: layoutCtx.layout?.totalPages ?? 0,
2323
+ totalPages: spreadCtx.totalPages,
1878
2324
  elapsedMs: layoutCtx.elapsedMs,
1879
2325
  fontLabel
1880
2326
  }
@@ -1893,7 +2339,12 @@ function MejiroReaderInner(props, ref) {
1893
2339
  {
1894
2340
  type: "button",
1895
2341
  className: `mejiro-reader-btn${imageCtx.hasImages ? " is-active" : ""}`,
1896
- onClick: () => imageCtx.addImage(),
2342
+ onClick: () => imageCtx.addImage(
2343
+ singlePageImagePlacement(
2344
+ mode === "paginated" ? spreadCtx.singleSide : null,
2345
+ layoutCtx.pageWidth
2346
+ )
2347
+ ),
1897
2348
  children: resolvedMessages.imageButton
1898
2349
  }
1899
2350
  ),
@@ -1911,7 +2362,7 @@ function MejiroReaderInner(props, ref) {
1911
2362
  )
1912
2363
  ] })
1913
2364
  ] }) : null;
1914
- const currentSpread = spreadCtx.spreadIdx;
2365
+ const currentSpread = spreadCtx.layoutSpreadIdx;
1915
2366
  const rightPage = currentSpread * 2 + 1;
1916
2367
  const leftPage = currentSpread * 2 + 2;
1917
2368
  const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
@@ -1955,18 +2406,29 @@ function MejiroReaderInner(props, ref) {
1955
2406
  fontFamily: bookOptions.fontFamily,
1956
2407
  fontSize: bookOptions.fontSize,
1957
2408
  lineSpacing: bookOptions.lineSpacing,
1958
- scrollToPage: spreadCtx.spreadIdx * 2,
2409
+ scrollToPage: scrollTargetPage(
2410
+ spreadCtx,
2411
+ userScroll?.layout === layoutCtx.layout ? userScroll.page : null
2412
+ ),
1959
2413
  onVisiblePageChange: (pageIdx, source) => {
1960
2414
  if (source === "programmatic") return;
1961
- const target = Math.floor(pageIdx / 2);
2415
+ if (layoutCtx.layout) setUserScroll({ layout: layoutCtx.layout, page: pageIdx });
2416
+ const target = spreadCtx.indexOfPage(pageIdx);
1962
2417
  if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
1963
- }
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
1964
2425
  }
1965
2426
  ),
1966
2427
  layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
1967
2428
  MejiroSpread,
1968
2429
  {
1969
2430
  singlePage: effectiveSingle,
2431
+ singleSide: spreadCtx.singleSide ?? void 0,
1970
2432
  onSwipe: (dir) => dir === "next" ? spreadCtx.next() : spreadCtx.prev(),
1971
2433
  onSurfaceTap: enableSurfaceTap ? () => setChromeHidden((v) => !v) : void 0,
1972
2434
  spread: spreadCtx.spread,
@@ -1998,6 +2460,7 @@ function MejiroReaderInner(props, ref) {
1998
2460
  onImagePointerDown: imageCtx.onOverlayPointerDown,
1999
2461
  onImageResizePointerDown: imageCtx.onResizePointerDown,
2000
2462
  onImageClose: imageCtx.removeImage,
2463
+ spreadIdx: spreadCtx.layoutSpreadIdx,
2001
2464
  selectionRects: annotationRects.length ? annotationRects : void 0
2002
2465
  },
2003
2466
  `${chapter}-${spreadCtx.spreadIdx}-${layoutCtx.pageWidth}x${layoutCtx.pageHeight}`
@@ -2030,28 +2493,6 @@ MejiroReader.displayName = "MejiroReader";
2030
2493
 
2031
2494
  // src/MejiroEditor.tsx
2032
2495
  import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
2033
- function cloneBook(book) {
2034
- return {
2035
- ...book,
2036
- chapters: book.chapters.map((chapter) => ({
2037
- ...chapter,
2038
- blocks: chapter.blocks.map(
2039
- (block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
2040
- ),
2041
- imageAssets: new Map(chapter.imageAssets),
2042
- originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
2043
- paragraphs: chapter.paragraphs.map((paragraph) => ({
2044
- ...paragraph,
2045
- inlineAnnotations: [...paragraph.inlineAnnotations]
2046
- })),
2047
- paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
2048
- })),
2049
- packageData: {
2050
- ...book.packageData,
2051
- files: new Map(book.packageData.files)
2052
- }
2053
- };
2054
- }
2055
2496
  function utf16ToCodepoint(text, utf16Offset) {
2056
2497
  let cp = 0;
2057
2498
  let i = 0;
@@ -2063,15 +2504,29 @@ function utf16ToCodepoint(text, utf16Offset) {
2063
2504
  }
2064
2505
  return cp;
2065
2506
  }
2066
- function applyWatermark(editor, watermark) {
2067
- editor.transaction(() => {
2068
- for (let i = 0; i < editor.chapters.length; i++) {
2069
- editor.insertParagraph(i, 0, {
2070
- text: `[mejiro-watermark] ${watermark.text}`,
2071
- inlineAnnotations: []
2072
- });
2073
- }
2074
- });
2507
+ var WATERMARK_PREFIX = "[mejiro-watermark]";
2508
+ var WATERMARK_BLOCK_ID = "mejiro-watermark";
2509
+ function watermarkedBook(book, watermark) {
2510
+ const copy = cloneEditableEpubBook(book);
2511
+ const text = `${WATERMARK_PREFIX} ${watermark.text}`;
2512
+ for (const chapter of copy.chapters) {
2513
+ chapter.blocks.unshift({
2514
+ kind: "paragraph",
2515
+ id: watermarkBlockId(chapter),
2516
+ text,
2517
+ inlineAnnotations: []
2518
+ });
2519
+ chapter.paragraphs.unshift({ text, inlineAnnotations: [] });
2520
+ chapter.isDirty = true;
2521
+ }
2522
+ return copy;
2523
+ }
2524
+ function watermarkBlockId(chapter) {
2525
+ const used = new Set(chapter.blocks.map((block) => block.id));
2526
+ let id = WATERMARK_BLOCK_ID;
2527
+ let suffix = 2;
2528
+ while (used.has(id)) id = `${WATERMARK_BLOCK_ID}-${suffix++}`;
2529
+ return id;
2075
2530
  }
2076
2531
  function MejiroEditor({
2077
2532
  epubUrl,
@@ -2083,75 +2538,80 @@ function MejiroEditor({
2083
2538
  onBeforeExport,
2084
2539
  exportPolicy,
2085
2540
  assetResolver,
2541
+ limits,
2086
2542
  onLoad,
2087
2543
  onExport,
2088
2544
  onError
2089
2545
  }) {
2090
2546
  const messages = useI18n();
2091
- const imageInputRef = useRef10(null);
2092
- const textareaRef = useRef10(null);
2093
- const [editor, setEditor] = useState8(null);
2094
- const [loading, setLoading] = useState8(false);
2095
- const [error, setError] = useState8(null);
2096
- const [revision, setRevision] = useState8(0);
2097
- const [selection, setSelection] = useState8({ chapter: 0, paragraph: 0 });
2098
- const [text, setText] = useState8("");
2099
- const [rubyStart, setRubyStart] = useState8(0);
2100
- const [rubyEnd, setRubyEnd] = useState8(1);
2101
- const [rubyText, setRubyText] = useState8("");
2102
- const loadRequestIdRef = useRef10(0);
2103
- const onLoadRef = useRef10(onLoad);
2104
- const onErrorRef = useRef10(onError);
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);
2563
+ limitsRef.current = limits;
2105
2564
  const book = editor?.book ?? null;
2106
2565
  const chapter = book?.chapters[selection.chapter] ?? null;
2107
2566
  const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
2108
2567
  const textDirty = paragraph ? text !== paragraph.text : false;
2109
2568
  const previewBook = useMemo5(() => {
2110
2569
  void revision;
2111
- return book ? cloneBook(book) : null;
2570
+ return book ? cloneEditableEpubBook(book) : null;
2112
2571
  }, [book, revision]);
2113
- useEffect7(() => {
2572
+ useEffect9(() => {
2114
2573
  onLoadRef.current = onLoad;
2115
2574
  }, [onLoad]);
2116
- useEffect7(() => {
2575
+ useEffect9(() => {
2117
2576
  onErrorRef.current = onError;
2118
2577
  }, [onError]);
2119
- const loadBufferForRequest = useCallback7(async (buffer, requestId) => {
2120
- setLoading(true);
2121
- setError(null);
2122
- try {
2123
- const next = await EditableEpub.load(buffer);
2124
- if (requestId !== loadRequestIdRef.current) return;
2125
- setEditor(next);
2126
- setSelection({ chapter: 0, paragraph: 0 });
2127
- setRevision((value) => value + 1);
2128
- onLoadRef.current?.(next);
2129
- } catch (err) {
2130
- if (requestId === loadRequestIdRef.current) {
2131
- const nextError = err instanceof Error ? err : new Error(String(err));
2132
- setError(nextError);
2133
- onErrorRef.current?.(nextError);
2134
- }
2135
- } finally {
2136
- if (requestId === loadRequestIdRef.current) setLoading(false);
2137
- }
2578
+ const reportError = useCallback7((nextError) => {
2579
+ setError(nextError);
2580
+ onErrorRef.current?.(nextError);
2138
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
+ );
2139
2601
  async function loadFile(file) {
2140
2602
  const requestId = ++loadRequestIdRef.current;
2141
2603
  setLoading(true);
2142
2604
  setError(null);
2143
2605
  try {
2144
- await loadBufferForRequest(await file.arrayBuffer(), requestId);
2606
+ await loadBufferForRequest(await readEpubFile(file, limitsRef.current), requestId);
2145
2607
  } catch (err) {
2146
2608
  if (requestId === loadRequestIdRef.current) {
2147
- const nextError = err instanceof Error ? err : new Error(String(err));
2148
- setError(nextError);
2149
- onErrorRef.current?.(nextError);
2609
+ reportError(toError(err));
2150
2610
  setLoading(false);
2151
2611
  }
2152
2612
  }
2153
2613
  }
2154
- useEffect7(() => {
2614
+ useEffect9(() => {
2155
2615
  if (!epubUrl) return;
2156
2616
  let cancelled = false;
2157
2617
  const requestId = ++loadRequestIdRef.current;
@@ -2159,17 +2619,11 @@ function MejiroEditor({
2159
2619
  setLoading(true);
2160
2620
  setError(null);
2161
2621
  try {
2162
- const res = await fetch(epubUrl);
2163
- if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
2164
- const buffer = await res.arrayBuffer();
2622
+ const buffer = await fetchEpubBuffer(epubUrl);
2165
2623
  if (cancelled || requestId !== loadRequestIdRef.current) return;
2166
2624
  await loadBufferForRequest(buffer, requestId);
2167
2625
  } catch (err) {
2168
- if (!cancelled && requestId === loadRequestIdRef.current) {
2169
- const nextError = err instanceof Error ? err : new Error(String(err));
2170
- setError(nextError);
2171
- onErrorRef.current?.(nextError);
2172
- }
2626
+ if (!cancelled && requestId === loadRequestIdRef.current) reportError(toError(err));
2173
2627
  } finally {
2174
2628
  if (!cancelled && requestId === loadRequestIdRef.current) setLoading(false);
2175
2629
  }
@@ -2177,14 +2631,35 @@ function MejiroEditor({
2177
2631
  return () => {
2178
2632
  cancelled = true;
2179
2633
  };
2180
- }, [epubUrl, loadBufferForRequest]);
2181
- useEffect7(() => {
2182
- 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);
2183
2648
  setRubyStart(0);
2184
- setRubyEnd(Math.min(1, [...paragraph?.text ?? ""].length));
2649
+ setRubyEnd(Math.min(1, [...nextText].length));
2185
2650
  setRubyText("");
2186
- }, [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]);
2187
2660
  function selectParagraph(chapterIndex, paragraphIndex) {
2661
+ if (chapterIndex === selection.chapter && paragraphIndex === selection.paragraph) return;
2662
+ if (textDirty) applyText();
2188
2663
  setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
2189
2664
  }
2190
2665
  function captureRubyRange() {
@@ -2228,38 +2703,49 @@ function MejiroEditor({
2228
2703
  setRevision((value) => value + 1);
2229
2704
  }
2230
2705
  async function addImage(file) {
2231
- if (!(editor && chapter)) return;
2232
- editor.addImage(selection.chapter, {
2233
- filename: file.name,
2234
- mediaType: file.type || "application/octet-stream",
2235
- data: await file.arrayBuffer(),
2236
- alt: file.name,
2237
- afterBlockId: paragraphBlockId(chapter, selection.paragraph)
2238
- });
2239
- 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);
2240
2720
  }
2241
2721
  async function exportEpub() {
2242
- if (!editor) return;
2243
- if (exportPolicy?.watermark) applyWatermark(editor, exportPolicy.watermark);
2244
- let buffer = await editor.export(assetResolver ? { assetResolver } : void 0);
2245
- if (exportPolicy?.encrypt) buffer = await exportPolicy.encrypt(buffer);
2246
- const decision = await onBeforeExport?.(buffer);
2247
- onExport?.(buffer);
2248
- if (decision === false) return;
2249
- if (exportPolicy?.allowDownload === false) return;
2250
- const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2251
- const a = document.createElement("a");
2252
- a.href = url;
2253
- a.download = `${editor.title || "edited"}.epub`;
2254
- a.click();
2255
- 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);
2256
2739
  }
2257
2740
  return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
2258
2741
  /* @__PURE__ */ jsxs9("main", { className: "mejiro-editor-preview", children: [
2259
2742
  previewBook ? /* @__PURE__ */ jsx14(
2260
2743
  MejiroReader,
2261
2744
  {
2745
+ ref: readerRef,
2262
2746
  epub: previewBook,
2747
+ chapter: selection.chapter,
2748
+ onChapterChange: (index) => selectParagraph(index, 0),
2263
2749
  fonts,
2264
2750
  subtitle: messages.editorPreviewSubtitle,
2265
2751
  chapterNavMode: "panel",
@@ -2377,7 +2863,7 @@ function paragraphBlockId(chapter, paragraphIndex) {
2377
2863
 
2378
2864
  // src/MejiroManuscriptEditor.tsx
2379
2865
  import { EpubProject } from "@libraz/mejiro/epub";
2380
- import { useCallback as useCallback9, useEffect as useEffect9, 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";
2381
2867
 
2382
2868
  // src/MejiroNotationHighlighter.tsx
2383
2869
  import { tokenizeManuscriptSource } from "@libraz/mejiro";
@@ -2385,8 +2871,8 @@ import {
2385
2871
  forwardRef as forwardRef2,
2386
2872
  useImperativeHandle as useImperativeHandle2,
2387
2873
  useMemo as useMemo6,
2388
- useRef as useRef11,
2389
- useState as useState9
2874
+ useRef as useRef12,
2875
+ useState as useState10
2390
2876
  } from "react";
2391
2877
  import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2392
2878
  var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2({
@@ -2402,11 +2888,11 @@ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2(
2402
2888
  onCompositionEnd,
2403
2889
  ...rest
2404
2890
  }, ref) {
2405
- const textareaRef = useRef11(null);
2406
- const overlayRef = useRef11(null);
2891
+ const textareaRef = useRef12(null);
2892
+ const overlayRef = useRef12(null);
2407
2893
  useImperativeHandle2(ref, () => textareaRef.current, []);
2408
- const [isComposing, setIsComposing] = useState9(false);
2409
- const lastSegmentsRef = useRef11(null);
2894
+ const [isComposing, setIsComposing] = useState10(false);
2895
+ const lastSegmentsRef = useRef12(null);
2410
2896
  const segments = useMemo6(() => {
2411
2897
  if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
2412
2898
  const next = buildSegments(value, dialect);
@@ -2495,10 +2981,11 @@ function buildSegments(text, dialect) {
2495
2981
  }
2496
2982
 
2497
2983
  // src/useManuscriptDraft.ts
2498
- import { useCallback as useCallback8, useEffect as useEffect8, useRef as useRef12, useState as useState10 } from "react";
2984
+ import { useCallback as useCallback8, useEffect as useEffect10, useRef as useRef13, useState as useState11 } from "react";
2499
2985
  var DEFAULT_DELAY = 800;
2500
- function defaultChapter(index, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
2501
- 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) };
2502
2989
  }
2503
2990
  function useManuscriptDraft(options = {}) {
2504
2991
  const {
@@ -2509,83 +2996,104 @@ function useManuscriptDraft(options = {}) {
2509
2996
  defaultChapterTitle,
2510
2997
  defaultChapterBody
2511
2998
  } = options;
2512
- const titleForRef = useRef12(defaultChapterTitle);
2999
+ const titleForRef = useRef13(defaultChapterTitle);
2513
3000
  titleForRef.current = defaultChapterTitle;
2514
- const bodyForRef = useRef12(defaultChapterBody);
3001
+ const bodyForRef = useRef13(defaultChapterBody);
2515
3002
  bodyForRef.current = defaultChapterBody;
2516
- const [chapters, setChaptersState] = useState10(
2517
- () => options.initialChapters?.length ? options.initialChapters : [defaultChapter(0, defaultChapterTitle, defaultChapterBody)]
3003
+ const [chapters, setChaptersState] = useState11(
3004
+ () => options.initialChapters?.length ? options.initialChapters : [defaultChapter([], defaultChapterTitle, defaultChapterBody)]
2518
3005
  );
2519
- const [selected, setSelectedState] = useState10(0);
2520
- const [autosaveError, setAutosaveError] = useState10(null);
2521
- const saveRef = useRef12(onAutosave);
3006
+ const [selected, setSelectedState] = useState11(0);
3007
+ const [autosaveError, setAutosaveError] = useState11(null);
3008
+ const saveRef = useRef13(onAutosave);
2522
3009
  saveRef.current = onAutosave;
2523
- const payloadRef = useRef12(autosavePayload);
3010
+ const payloadRef = useRef13(autosavePayload);
2524
3011
  payloadRef.current = autosavePayload;
2525
- const chaptersRef = useRef12(chapters);
3012
+ const chaptersRef = useRef13(chapters);
2526
3013
  chaptersRef.current = chapters;
2527
- const didMountRef = useRef12(false);
2528
- const dirtyRef = useRef12(false);
2529
- const mountedRef = useRef12(true);
3014
+ const selectedRef = useRef13(selected);
3015
+ selectedRef.current = selected;
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);
2530
3023
  const flushAutosave = useCallback8(() => {
2531
3024
  const callback = saveRef.current;
2532
- if (!(callback && dirtyRef.current)) return;
2533
- dirtyRef.current = false;
2534
- const payload = payloadRef.current ? payloadRef.current(chaptersRef.current) : chaptersRef.current;
2535
- void Promise.resolve(callback(payload)).catch((err) => {
3025
+ if (!(callback && dirtyRef.current) || inFlightRevisionRef.current === revisionRef.current) {
3026
+ return;
3027
+ }
3028
+ const revision = revisionRef.current;
3029
+ inFlightRevisionRef.current = revision;
3030
+ const plain = snapshotChapters(chaptersRef.current);
3031
+ const payload = payloadRef.current ? payloadRef.current(plain) : plain;
3032
+ void Promise.resolve(callback(payload)).then(() => {
3033
+ if (revisionRef.current === revision) dirtyRef.current = false;
3034
+ }).catch((err) => {
2536
3035
  if (!mountedRef.current) return;
2537
- setAutosaveError(err instanceof Error ? err : new Error(String(err)));
3036
+ setAutosaveError(toError(err));
3037
+ }).finally(() => {
3038
+ if (inFlightRevisionRef.current === revision) inFlightRevisionRef.current = -1;
2538
3039
  });
2539
3040
  }, []);
2540
- useEffect8(() => {
2541
- if (!didMountRef.current) {
2542
- didMountRef.current = true;
2543
- return void 0;
3041
+ useEffect10(() => {
3042
+ if (trackerRef.current?.changed(chapters, autosaveKey ?? "")) {
3043
+ dirtyRef.current = true;
3044
+ revisionRef.current += 1;
3045
+ setAutosaveError(null);
2544
3046
  }
2545
- if (!saveRef.current) return void 0;
2546
- dirtyRef.current = true;
2547
- setAutosaveError(null);
3047
+ if (!(saveRef.current && dirtyRef.current)) return void 0;
2548
3048
  const timer = setTimeout(() => {
2549
3049
  flushAutosave();
2550
3050
  }, autosaveDelay);
2551
3051
  return () => clearTimeout(timer);
2552
3052
  }, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
2553
- useEffect8(() => {
3053
+ useEffect10(() => {
2554
3054
  mountedRef.current = true;
2555
- const handleBeforeUnload = () => flushAutosave();
2556
- window.addEventListener("beforeunload", handleBeforeUnload);
3055
+ const detachPageHide = flushOnPageHide(flushAutosave);
2557
3056
  return () => {
2558
3057
  flushAutosave();
2559
3058
  mountedRef.current = false;
2560
- window.removeEventListener("beforeunload", handleBeforeUnload);
3059
+ detachPageHide();
2561
3060
  };
2562
- }, [flushAutosave]);
2563
- const setSelected = useCallback8(
2564
- (index) => {
2565
- setSelectedState(() => Math.max(0, Math.min(index, chapters.length - 1)));
2566
- },
2567
- [chapters.length]
2568
- );
3061
+ }, [flushAutosave]);
3062
+ const commit = useCallback8((nextChapters, nextSelected) => {
3063
+ chaptersRef.current = nextChapters;
3064
+ selectedRef.current = nextSelected;
3065
+ setChaptersState(nextChapters);
3066
+ setSelectedState(nextSelected);
3067
+ }, []);
3068
+ const setSelected = useCallback8((index) => {
3069
+ const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
3070
+ selectedRef.current = next;
3071
+ setSelectedState(next);
3072
+ }, []);
2569
3073
  const setChapters = useCallback8(
2570
3074
  (next) => {
2571
- const normalized = next.length ? next : [defaultChapter(0, titleForRef.current, bodyForRef.current)];
2572
- setChaptersState(normalized);
2573
- setSelectedState((current) => {
2574
- const selectedId = chapters[current]?.id;
2575
- const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
2576
- return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
2577
- });
3075
+ const normalized = next.length ? next : [defaultChapter([], titleForRef.current, bodyForRef.current)];
3076
+ const prev = selectedRef.current;
3077
+ const selectedId = chaptersRef.current[prev]?.id;
3078
+ const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
3079
+ commit(
3080
+ normalized,
3081
+ nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(prev, normalized.length - 1))
3082
+ );
2578
3083
  },
2579
- [chapters]
3084
+ [commit]
2580
3085
  );
2581
3086
  const patchChapter = useCallback8((index, patch) => {
2582
- setChaptersState(
2583
- (current) => current.map((chapter, i) => i === index ? { ...chapter, ...patch } : chapter)
3087
+ const next = chaptersRef.current.map(
3088
+ (chapter, i) => i === index ? { ...chapter, ...patch } : chapter
2584
3089
  );
3090
+ chaptersRef.current = next;
3091
+ setChaptersState(next);
2585
3092
  }, []);
2586
- const addChapter = useCallback8((chapter = {}) => {
2587
- setChaptersState((current) => {
2588
- const generated = defaultChapter(current.length, titleForRef.current, bodyForRef.current);
3093
+ const addChapter = useCallback8(
3094
+ (chapter = {}) => {
3095
+ const current = chaptersRef.current;
3096
+ const generated = defaultChapter(current, titleForRef.current, bodyForRef.current);
2589
3097
  const next = [
2590
3098
  ...current,
2591
3099
  {
@@ -2594,38 +3102,41 @@ function useManuscriptDraft(options = {}) {
2594
3102
  body: chapter.body ?? generated.body
2595
3103
  }
2596
3104
  ];
2597
- setSelectedState(next.length - 1);
2598
- return next;
2599
- });
2600
- }, []);
2601
- const removeChapter = useCallback8((index) => {
2602
- setChaptersState((current) => {
2603
- if (current.length <= 1) return current;
3105
+ commit(next, next.length - 1);
3106
+ },
3107
+ [commit]
3108
+ );
3109
+ const removeChapter = useCallback8(
3110
+ (index) => {
3111
+ const current = chaptersRef.current;
3112
+ if (current.length <= 1) return;
2604
3113
  const next = current.filter((_, i) => i !== index);
2605
- setSelectedState((prev) => {
2606
- if (prev === index) return Math.max(0, Math.min(index, next.length - 1));
2607
- if (index < prev) return prev - 1;
2608
- return Math.max(0, Math.min(prev, next.length - 1));
2609
- });
2610
- return next;
2611
- });
2612
- }, []);
2613
- const reorderChapters = useCallback8((from, to) => {
2614
- setChaptersState((current) => {
2615
- if (from < 0 || from >= current.length) return current;
3114
+ const prev = selectedRef.current;
3115
+ let nextSelected;
3116
+ if (prev === index) nextSelected = Math.max(0, Math.min(index, next.length - 1));
3117
+ else if (index < prev) nextSelected = prev - 1;
3118
+ else nextSelected = Math.max(0, Math.min(prev, next.length - 1));
3119
+ commit(next, nextSelected);
3120
+ },
3121
+ [commit]
3122
+ );
3123
+ const reorderChapters = useCallback8(
3124
+ (from, to) => {
3125
+ const current = chaptersRef.current;
3126
+ if (from < 0 || from >= current.length) return;
2616
3127
  const next = [...current];
2617
3128
  const [moved] = next.splice(from, 1);
2618
3129
  const target = Math.max(0, Math.min(next.length, to));
2619
3130
  next.splice(target, 0, moved);
2620
- setSelectedState((prev) => {
2621
- if (prev === from) return target;
2622
- if (from < prev && target >= prev) return prev - 1;
2623
- if (from > prev && target <= prev) return prev + 1;
2624
- return prev;
2625
- });
2626
- return next;
2627
- });
2628
- }, []);
3131
+ const prev = selectedRef.current;
3132
+ let nextSelected = prev;
3133
+ if (prev === from) nextSelected = target;
3134
+ else if (from < prev && target >= prev) nextSelected = prev - 1;
3135
+ else if (from > prev && target <= prev) nextSelected = prev + 1;
3136
+ commit(next, nextSelected);
3137
+ },
3138
+ [commit]
3139
+ );
2629
3140
  return {
2630
3141
  chapters,
2631
3142
  selected,
@@ -2686,31 +3197,34 @@ function MejiroManuscriptEditor({
2686
3197
  cover: coverProp,
2687
3198
  onCoverChange,
2688
3199
  chapters: initialChapters,
3200
+ dialect = "mejiro",
2689
3201
  onAutosave,
2690
3202
  autosaveDelay,
2691
3203
  previewProps,
2692
3204
  assetResolver,
2693
- onExport
3205
+ onExport,
3206
+ onError,
3207
+ panelSide = "right"
2694
3208
  }) {
2695
3209
  const messages = useI18n();
2696
- const coverInputRef = useRef13(null);
2697
- const bodyTextareaRef = useRef13(null);
3210
+ const coverInputRef = useRef14(null);
3211
+ const bodyTextareaRef = useRef14(null);
2698
3212
  const titleControlled = onTitleChange !== void 0;
2699
3213
  const authorControlled = onAuthorChange !== void 0;
2700
3214
  const coverControlled = onCoverChange !== void 0;
2701
- const [titleState, setTitleState] = useState11(titleProp ?? messages.manuscriptDefaultTitle);
2702
- const [authorState, setAuthorState] = useState11(authorProp ?? "");
2703
- 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);
2704
3218
  const title = titleControlled ? titleProp ?? "" : titleState;
2705
3219
  const author = authorControlled ? authorProp ?? "" : authorState;
2706
3220
  const cover = coverControlled ? coverProp ?? null : coverState;
2707
- useEffect9(() => {
3221
+ useEffect11(() => {
2708
3222
  if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
2709
3223
  }, [titleControlled, titleProp]);
2710
- useEffect9(() => {
3224
+ useEffect11(() => {
2711
3225
  if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
2712
3226
  }, [authorControlled, authorProp]);
2713
- useEffect9(() => {
3227
+ useEffect11(() => {
2714
3228
  if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
2715
3229
  }, [coverControlled, coverProp]);
2716
3230
  const setTitle = useCallback9(
@@ -2752,7 +3266,8 @@ function MejiroManuscriptEditor({
2752
3266
  reorderChapters
2753
3267
  } = draft;
2754
3268
  const current = chapters[selected] ?? chapters[0];
2755
- const [draggingIndex, setDraggingIndex] = useState11(null);
3269
+ const [draggingIndex, setDraggingIndex] = useState12(null);
3270
+ const [exportError, setExportError] = useState12(null);
2756
3271
  function wrapSelection(open, close) {
2757
3272
  const el = bodyTextareaRef.current;
2758
3273
  if (!el) return;
@@ -2770,39 +3285,70 @@ function MejiroManuscriptEditor({
2770
3285
  bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
2771
3286
  });
2772
3287
  }
3288
+ const [identifier] = useState12(() => `urn:uuid:${crypto.randomUUID()}`);
2773
3289
  const exportEpub = useCallback9(async () => {
2774
- const project = EpubProject.fromManuscript({
2775
- metadata: { title, author: author || void 0 },
2776
- chapters: chapters.map((chapter) => ({
2777
- id: chapter.id,
2778
- title: chapter.title || messages.untitled,
2779
- body: chapter.body
2780
- }))
2781
- });
2782
- if (cover) {
2783
- project.setCover({
2784
- href: coverAssetHref(cover),
2785
- mediaType: cover.type || void 0,
2786
- data: await cover.arrayBuffer()
2787
- });
2788
- }
2789
- const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
2790
- onExport?.(buffer);
2791
- downloadEpub(buffer, title);
2792
- }, [assetResolver, author, chapters, cover, messages.untitled, onExport, title]);
2793
- return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor mejiro-manuscript-editor", children: [
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
+ }))
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);
3316
+ }
3317
+ );
3318
+ }, [
3319
+ assetResolver,
3320
+ author,
3321
+ chapters,
3322
+ cover,
3323
+ dialect,
3324
+ identifier,
3325
+ messages.untitled,
3326
+ onError,
3327
+ onExport,
3328
+ title
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
+ );
3338
+ return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor mejiro-manuscript-editor", "data-panel-side": panelSide, children: [
2794
3339
  /* @__PURE__ */ jsx16("main", { className: "mejiro-editor-preview", children: /* @__PURE__ */ jsx16(
2795
3340
  MejiroReader,
2796
3341
  {
2797
- subtitle: messages.manuscriptPreviewSubtitle,
2798
- chapterNavMode: "panel",
2799
- enableSurfaceTap: false,
2800
- ...previewProps,
2801
- manuscript: chapters.map((chapter) => ({
2802
- id: chapter.id,
2803
- title: chapter.title || messages.untitled,
2804
- body: chapter.body
2805
- })),
3342
+ ...mergeDefined(
3343
+ {
3344
+ subtitle: messages.manuscriptPreviewSubtitle,
3345
+ chapterNavMode: "panel",
3346
+ enableSurfaceTap: false
3347
+ },
3348
+ previewProps ?? {}
3349
+ ),
3350
+ manuscript: previewManuscript,
3351
+ dialect,
2806
3352
  fonts,
2807
3353
  chapter: selected,
2808
3354
  onChapterChange: setSelected,
@@ -2816,6 +3362,7 @@ function MejiroManuscriptEditor({
2816
3362
  /* @__PURE__ */ jsx16("small", { children: messages.manuscriptRubyHint })
2817
3363
  ] }),
2818
3364
  draft.autosaveError && /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-error", children: draft.autosaveError.message }),
3365
+ exportError && /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-error", children: exportError.message }),
2819
3366
  /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2820
3367
  /* @__PURE__ */ jsx16("span", { className: "mejiro-editor-label", children: messages.manuscriptMetadata }),
2821
3368
  /* @__PURE__ */ jsx16("input", { value: title, onChange: (event) => setTitle(event.target.value) }),
@@ -2905,6 +3452,7 @@ function MejiroManuscriptEditor({
2905
3452
  {
2906
3453
  ref: bodyTextareaRef,
2907
3454
  className: "mejiro-editor-manuscript",
3455
+ dialect,
2908
3456
  value: current.body,
2909
3457
  onChange: (body) => patchChapter(selected, { body })
2910
3458
  }
@@ -2953,12 +3501,15 @@ function MejiroShelf({
2953
3501
  }
2954
3502
 
2955
3503
  // src/MejiroToc.tsx
2956
- import { useMemo as useMemo7, useState as useState12 } from "react";
3504
+ import { useMemo as useMemo8, useState as useState13 } from "react";
2957
3505
  import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
2958
3506
  function buildEntries(epub, messages) {
2959
3507
  return epub.chapters.map((ch, i) => {
2960
3508
  const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
2961
- 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
+ });
2962
3513
  return { index: i, title, headings };
2963
3514
  });
2964
3515
  }
@@ -2973,13 +3524,13 @@ function MejiroToc({
2973
3524
  const messages = useI18n();
2974
3525
  const resolvedTitle = title ?? messages.tocTitle;
2975
3526
  const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
2976
- const [query, setQuery] = useState12("");
2977
- const entries = useMemo7(() => buildEntries(epub, messages), [epub, messages]);
2978
- const filtered = useMemo7(() => {
3527
+ const [query, setQuery] = useState13("");
3528
+ const entries = useMemo8(() => buildEntries(epub, messages), [epub, messages]);
3529
+ const filtered = useMemo8(() => {
2979
3530
  if (!query) return entries;
2980
3531
  const needle = query.toLowerCase();
2981
3532
  return entries.filter(
2982
- (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))
2983
3534
  );
2984
3535
  }, [entries, query]);
2985
3536
  const activeIndex = currentAnchor?.chapter ?? -1;
@@ -3012,7 +3563,7 @@ function MejiroToc({
3012
3563
  ]
3013
3564
  }
3014
3565
  ),
3015
- 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)) })
3016
3567
  ] }, entry.index)) }),
3017
3568
  filtered.length === 0 && /* @__PURE__ */ jsx18("div", { className: "mejiro-toc-empty", role: "status", children: format(messages.tocEmpty, { query }) })
3018
3569
  ] });
@@ -3025,57 +3576,44 @@ import {
3025
3576
  serializeAnnotations,
3026
3577
  sortAnnotations
3027
3578
  } from "@libraz/mejiro";
3028
- import { useCallback as useCallback10, useEffect as useEffect10, useRef as useRef14, useState as useState13 } from "react";
3029
- function resolveDefaultStorage() {
3030
- if (typeof globalThis === "undefined") return null;
3031
- const w = globalThis.localStorage;
3032
- 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));
3033
3582
  }
3034
3583
  function useAnnotations(options) {
3035
3584
  const { key, throttleMs = 250 } = options;
3036
3585
  const storage = options.storage ?? resolveDefaultStorage();
3037
- const [annotations, setAnnotations] = useState13(() => {
3038
- if (!storage) return [];
3039
- try {
3040
- return sortAnnotations(parseAnnotations(storage.getItem(key)));
3041
- } catch {
3042
- return [];
3043
- }
3044
- });
3045
- useEffect10(() => {
3046
- if (!storage) {
3047
- setAnnotations([]);
3048
- return;
3049
- }
3050
- try {
3051
- setAnnotations(sortAnnotations(parseAnnotations(storage.getItem(key))));
3052
- } catch {
3053
- setAnnotations([]);
3054
- }
3055
- }, [storage, key]);
3056
- const timerRef = useRef14(null);
3057
- useEffect10(
3058
- () => () => {
3059
- if (timerRef.current) clearTimeout(timerRef.current);
3060
- },
3061
- []
3586
+ const [annotations, setAnnotations] = useState14(
3587
+ () => readStorage(storage, key, parseSorted, [])
3062
3588
  );
3063
- const onChangeRef = useRef14(options.onChange);
3589
+ const storageRef = useRef15(storage);
3590
+ storageRef.current = storage;
3591
+ const annotationsRef = useRef15(annotations);
3592
+ const applyNext = useCallback10((next) => {
3593
+ annotationsRef.current = next;
3594
+ setAnnotations(next);
3595
+ }, []);
3596
+ const pendingRef = useRef15(null);
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);
3064
3605
  onChangeRef.current = options.onChange;
3065
3606
  const commit = useCallback10(
3066
3607
  (next) => {
3067
- if (storage) {
3068
- if (timerRef.current) clearTimeout(timerRef.current);
3069
- timerRef.current = setTimeout(() => {
3070
- try {
3071
- storage.setItem(key, serializeAnnotations(next));
3072
- } catch {
3073
- }
3608
+ const currentStorage = storageRef.current;
3609
+ if (currentStorage) {
3610
+ pending.schedule(() => {
3611
+ currentStorage.setItem(key, serializeAnnotations(next));
3074
3612
  }, throttleMs);
3075
3613
  }
3076
3614
  onChangeRef.current?.(next);
3077
3615
  },
3078
- [storage, key, throttleMs]
3616
+ [key, throttleMs, pending]
3079
3617
  );
3080
3618
  const add = useCallback10(
3081
3619
  (input) => {
@@ -3084,93 +3622,97 @@ function useAnnotations(options) {
3084
3622
  id: input.id ?? createAnnotationId(),
3085
3623
  createdAt: input.createdAt ?? Date.now()
3086
3624
  };
3087
- setAnnotations((current) => {
3088
- const next = sortAnnotations([...current, annotation]);
3089
- commit(next);
3090
- return next;
3091
- });
3625
+ const next = sortAnnotations([...annotationsRef.current, annotation]);
3626
+ applyNext(next);
3627
+ commit(next);
3092
3628
  return annotation;
3093
3629
  },
3094
- [commit]
3630
+ [commit, applyNext]
3095
3631
  );
3096
3632
  const remove = useCallback10(
3097
3633
  (id) => {
3098
- setAnnotations((current) => {
3099
- const next = current.filter((annotation) => annotation.id !== id);
3100
- if (next.length !== current.length) commit(next);
3101
- return next;
3102
- });
3634
+ const current = annotationsRef.current;
3635
+ const next = current.filter((annotation) => annotation.id !== id);
3636
+ if (next.length === current.length) return;
3637
+ applyNext(next);
3638
+ commit(next);
3103
3639
  },
3104
- [commit]
3640
+ [commit, applyNext]
3105
3641
  );
3106
3642
  const update = useCallback10(
3107
3643
  (id, patch) => {
3108
- setAnnotations((current) => {
3109
- let changed = false;
3110
- const next = current.map((annotation) => {
3111
- if (annotation.id !== id) return annotation;
3112
- changed = true;
3113
- return { ...annotation, ...patch, id };
3114
- });
3115
- if (!changed) return current;
3116
- const sorted = sortAnnotations(next);
3117
- commit(sorted);
3118
- return sorted;
3644
+ let changed = false;
3645
+ const next = annotationsRef.current.map((annotation) => {
3646
+ if (annotation.id !== id) return annotation;
3647
+ changed = true;
3648
+ return { ...mergeDefined(annotation, patch), id };
3119
3649
  });
3650
+ if (!changed) return;
3651
+ const sorted = sortAnnotations(next);
3652
+ applyNext(sorted);
3653
+ commit(sorted);
3120
3654
  },
3121
- [commit]
3655
+ [commit, applyNext]
3122
3656
  );
3123
3657
  const clear = useCallback10(() => {
3124
- setAnnotations([]);
3125
- if (storage) {
3126
- if (timerRef.current) clearTimeout(timerRef.current);
3127
- try {
3128
- storage.removeItem(key);
3129
- } catch {
3130
- }
3131
- }
3658
+ applyNext([]);
3659
+ pending.cancel();
3660
+ removeStorage(storageRef.current, key);
3132
3661
  onChangeRef.current?.([]);
3133
- }, [storage, key]);
3662
+ }, [key, pending, applyNext]);
3134
3663
  return { annotations, add, remove, update, clear };
3135
3664
  }
3136
3665
 
3137
3666
  // src/useEditableEpub.ts
3138
3667
  import {
3668
+ clampEditableEpubSelection,
3669
+ cloneEditableEpubBook as cloneEditableEpubBook2,
3139
3670
  EditableEpub as EditableEpub2
3140
3671
  } from "@libraz/mejiro/epub";
3141
- import { useCallback as useCallback11, useEffect as useEffect11, 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";
3142
3673
  function useEditableEpub(options = {}) {
3143
- const [editor, setEditor] = useState14(null);
3144
- const [loading, setLoading] = useState14(false);
3145
- const [exporting, setExporting] = useState14(false);
3146
- const [error, setError] = useState14(null);
3147
- const [revision, setRevision] = useState14(0);
3148
- 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({
3149
3680
  chapter: 0,
3150
3681
  paragraph: 0
3151
3682
  });
3152
- const requestIdRef = useRef15(0);
3153
- const onLoadRef = useRef15(options.onLoad);
3154
- const onErrorRef = useRef15(options.onError);
3155
- const onExportRef = useRef15(options.onExport);
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);
3156
3689
  onLoadRef.current = options.onLoad;
3157
3690
  onErrorRef.current = options.onError;
3158
3691
  onExportRef.current = options.onExport;
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;
3159
3698
  const loadBufferWithRequest = useCallback11(
3160
3699
  async (buffer, requestId) => {
3161
3700
  setLoading(true);
3162
3701
  setError(null);
3163
3702
  try {
3164
- const next = await EditableEpub2.load(buffer);
3703
+ const next = await EditableEpub2.load(buffer, { limits: limitsRef.current });
3165
3704
  if (requestId !== requestIdRef.current) return null;
3705
+ const firstParagraph = { chapter: 0, paragraph: 0 };
3706
+ editorRef.current = next;
3707
+ selectionRef.current = firstParagraph;
3166
3708
  setEditor(next);
3167
- setSelectionState({ chapter: 0, paragraph: 0 });
3709
+ setSelectionState(firstParagraph);
3168
3710
  setRevision((value) => value + 1);
3169
3711
  onLoadRef.current?.(next);
3170
3712
  return next;
3171
3713
  } catch (err) {
3172
3714
  if (requestId === requestIdRef.current) {
3173
- const nextError = err instanceof Error ? err : new Error(String(err));
3715
+ const nextError = toError(err);
3174
3716
  setError(nextError);
3175
3717
  onErrorRef.current?.(nextError);
3176
3718
  }
@@ -3194,10 +3736,10 @@ function useEditableEpub(options = {}) {
3194
3736
  setLoading(true);
3195
3737
  setError(null);
3196
3738
  try {
3197
- return await loadBufferWithRequest(await file.arrayBuffer(), requestId);
3739
+ return await loadBufferWithRequest(await readEpubFile(file, limitsRef.current), requestId);
3198
3740
  } catch (err) {
3199
3741
  if (requestId === requestIdRef.current) {
3200
- const nextError = err instanceof Error ? err : new Error(String(err));
3742
+ const nextError = toError(err);
3201
3743
  setError(nextError);
3202
3744
  onErrorRef.current?.(nextError);
3203
3745
  setLoading(false);
@@ -3213,12 +3755,10 @@ function useEditableEpub(options = {}) {
3213
3755
  setLoading(true);
3214
3756
  setError(null);
3215
3757
  try {
3216
- const res = await fetch(url);
3217
- if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
3218
- return await loadBufferWithRequest(await res.arrayBuffer(), requestId);
3758
+ return await loadBufferWithRequest(await fetchEpubBuffer(url), requestId);
3219
3759
  } catch (err) {
3220
3760
  if (requestId === requestIdRef.current) {
3221
- const nextError = err instanceof Error ? err : new Error(String(err));
3761
+ const nextError = toError(err);
3222
3762
  setError(nextError);
3223
3763
  onErrorRef.current?.(nextError);
3224
3764
  setLoading(false);
@@ -3228,74 +3768,74 @@ function useEditableEpub(options = {}) {
3228
3768
  },
3229
3769
  [loadBufferWithRequest]
3230
3770
  );
3231
- useEffect11(() => {
3771
+ useEffect13(() => {
3232
3772
  if (options.defaultUrl) void loadUrl(options.defaultUrl);
3233
3773
  }, [options.defaultUrl, loadUrl]);
3234
3774
  const book = editor?.book ?? null;
3235
3775
  const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
3236
- const previewBook = useMemo8(() => {
3776
+ const previewBook = useMemo9(() => {
3237
3777
  void revision;
3238
- return book ? cloneBook2(book) : null;
3778
+ return book ? cloneEditableEpubBook2(book) : null;
3239
3779
  }, [book, revision]);
3240
3780
  const history = editor?.history ?? null;
3241
- const setSelection = useCallback11(
3242
- (nextSelection) => {
3243
- setSelectionState(clampSelection(book, nextSelection));
3244
- },
3245
- [book]
3246
- );
3781
+ const setSelection = useCallback11((nextSelection) => {
3782
+ const next = clampEditableEpubSelection(editorRef.current?.book ?? null, nextSelection);
3783
+ selectionRef.current = next;
3784
+ setSelectionState(next);
3785
+ }, []);
3247
3786
  const updateParagraph = useCallback11(
3248
3787
  (text, inlineAnnotations) => {
3249
- if (!editor) return;
3250
- editor.updateParagraph(selection.chapter, selection.paragraph, {
3251
- text,
3252
- inlineAnnotations
3253
- });
3254
- setRevision((value) => value + 1);
3255
- },
3256
- [editor, selection.chapter, selection.paragraph]
3257
- );
3258
- const setInlineAnnotations = useCallback11(
3259
- (inlineAnnotations) => {
3260
- if (!editor) return;
3261
- editor.setInlineAnnotations(selection.chapter, selection.paragraph, inlineAnnotations);
3262
- setRevision((value) => value + 1);
3263
- },
3264
- [editor, selection.chapter, selection.paragraph]
3265
- );
3266
- const addImage = useCallback11(
3267
- (image) => {
3268
- if (!editor) return;
3269
- 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 });
3270
3792
  setRevision((value) => value + 1);
3271
3793
  },
3272
- [editor, selection.chapter]
3794
+ []
3273
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
+ }, []);
3274
3809
  const undo = useCallback11(() => {
3275
- if (!editor) return false;
3276
- const changed = editor.undo();
3810
+ const current = editorRef.current;
3811
+ if (!current) return false;
3812
+ const changed = current.undo();
3277
3813
  if (changed) setRevision((value) => value + 1);
3278
3814
  return changed;
3279
- }, [editor]);
3815
+ }, []);
3280
3816
  const redo = useCallback11(() => {
3281
- if (!editor) return false;
3282
- const changed = editor.redo();
3817
+ const current = editorRef.current;
3818
+ if (!current) return false;
3819
+ const changed = current.redo();
3283
3820
  if (changed) setRevision((value) => value + 1);
3284
3821
  return changed;
3285
- }, [editor]);
3822
+ }, []);
3286
3823
  const exportEpub = useCallback11(
3287
- async (options2) => {
3288
- if (!editor) return null;
3824
+ async (exportOptions) => {
3825
+ const current = editorRef.current;
3826
+ if (!current) return null;
3289
3827
  setExporting(true);
3290
3828
  try {
3291
- const buffer = await editor.export(options2);
3829
+ const buffer = await current.export(
3830
+ withAssetResolver(exportOptions, assetResolverRef.current)
3831
+ );
3292
3832
  onExportRef.current?.(buffer);
3293
3833
  return buffer;
3294
3834
  } finally {
3295
3835
  setExporting(false);
3296
3836
  }
3297
3837
  },
3298
- [editor]
3838
+ []
3299
3839
  );
3300
3840
  return {
3301
3841
  editor,
@@ -3320,38 +3860,9 @@ function useEditableEpub(options = {}) {
3320
3860
  exportEpub
3321
3861
  };
3322
3862
  }
3323
- function cloneBook2(book) {
3324
- return {
3325
- ...book,
3326
- chapters: book.chapters.map((chapter) => ({
3327
- ...chapter,
3328
- blocks: chapter.blocks.map(
3329
- (block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
3330
- ),
3331
- imageAssets: new Map(chapter.imageAssets),
3332
- originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
3333
- paragraphs: chapter.paragraphs.map((paragraph) => ({
3334
- ...paragraph,
3335
- inlineAnnotations: [...paragraph.inlineAnnotations]
3336
- })),
3337
- paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
3338
- })),
3339
- packageData: {
3340
- ...book.packageData,
3341
- files: new Map(book.packageData.files)
3342
- }
3343
- };
3344
- }
3345
- function clampSelection(book, selection) {
3346
- if (!book?.chapters.length) return { chapter: 0, paragraph: 0 };
3347
- const chapter = clampInteger(selection.chapter, 0, book.chapters.length - 1);
3348
- const paragraphCount = book.chapters[chapter]?.paragraphs.length ?? 0;
3349
- const paragraph = paragraphCount ? clampInteger(selection.paragraph, 0, paragraphCount - 1) : 0;
3350
- return { chapter, paragraph };
3351
- }
3352
- function clampInteger(value, min, max) {
3353
- if (!Number.isFinite(value)) return min;
3354
- return Math.min(max, Math.max(min, Math.trunc(value)));
3863
+ function withAssetResolver(options, resolver) {
3864
+ if (!resolver || options?.assetResolver) return options;
3865
+ return { ...options, assetResolver: resolver };
3355
3866
  }
3356
3867
 
3357
3868
  // src/useEpubProject.ts
@@ -3359,52 +3870,69 @@ import {
3359
3870
  EpubProject as EpubProject2,
3360
3871
  parseEpub as parseEpub2
3361
3872
  } from "@libraz/mejiro/epub";
3362
- import { useCallback as useCallback12, useEffect as useEffect12, 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";
3363
3874
  function useEpubProject(options = {}) {
3364
3875
  const defaultTitle = options.defaultChapterTitle;
3365
3876
  const defaultBody = options.defaultChapterBody;
3366
- const [metadata, setMetadataState] = useState15({
3367
- title: "\u65B0\u3057\u3044\u4F5C\u54C1",
3368
- language: "ja",
3369
- ...options.metadata
3370
- });
3371
- const [chapters, setChaptersState] = useState15(
3372
- options.chapters?.length ? options.chapters : [defaultChapter3(0, defaultTitle, defaultBody)]
3373
- );
3374
- const [selectedChapter, setSelectedChapterState] = useState15(0);
3375
- const [previewBook, setPreviewBook] = useState15(null);
3376
- const [previewError, setPreviewError] = useState15(null);
3377
- const [previewing, setPreviewing] = useState15(false);
3378
- const previewRequestIdRef = useRef16(0);
3379
- const onPreviewRef = useRef16(options.onPreview);
3380
- const onExportRef = useRef16(options.onExport);
3877
+ const [metadata, setMetadataState] = useState16(
3878
+ () => initialMetadata(options.metadata)
3879
+ );
3880
+ const [chapters, setChaptersState] = useState16(
3881
+ () => options.chapters?.length ? options.chapters : [defaultChapter3([], defaultTitle, defaultBody)]
3882
+ );
3883
+ const [selectedChapter, setSelectedChapterState] = useState16(0);
3884
+ const [cover, setCoverState] = useState16(options.cover ?? null);
3885
+ const [assets, setAssetsState] = useState16(options.assets ?? []);
3886
+ const [previewBook, setPreviewBook] = useState16(null);
3887
+ const [previewError, setPreviewError] = useState16(null);
3888
+ const [previewing, setPreviewing] = useState16(false);
3889
+ const previewRequestIdRef = useRef17(0);
3890
+ const onPreviewRef = useRef17(options.onPreview);
3891
+ const onExportRef = useRef17(options.onExport);
3381
3892
  onPreviewRef.current = options.onPreview;
3382
3893
  onExportRef.current = options.onExport;
3383
- const buildProject = useCallback12(
3384
- () => EpubProject2.fromManuscript({
3385
- metadata,
3894
+ const assetResolverRef = useRef17(options.assetResolver);
3895
+ assetResolverRef.current = options.assetResolver;
3896
+ const metadataRef = useRef17(metadata);
3897
+ metadataRef.current = metadata;
3898
+ const chaptersRef = useRef17(chapters);
3899
+ chaptersRef.current = chapters;
3900
+ const selectedRef = useRef17(selectedChapter);
3901
+ selectedRef.current = selectedChapter;
3902
+ const coverRef = useRef17(cover);
3903
+ coverRef.current = cover;
3904
+ const assetsRef = useRef17(assets);
3905
+ assetsRef.current = assets;
3906
+ const buildProject = useCallback12(() => {
3907
+ const currentCover = coverRef.current;
3908
+ const project = EpubProject2.fromManuscript({
3909
+ metadata: metadataRef.current,
3386
3910
  includeTitlePage: false,
3387
3911
  includeTitleInFirstChapter: true,
3388
- chapters: chapters.map((chapter) => ({
3912
+ chapters: chaptersRef.current.map((chapter) => ({
3389
3913
  id: chapter.id,
3390
3914
  title: chapter.title || "Untitled",
3391
3915
  body: chapter.body
3392
- }))
3393
- }),
3394
- [chapters, metadata]
3395
- );
3396
- const assetResolverRef = useRef16(options.assetResolver);
3397
- assetResolverRef.current = options.assetResolver;
3398
- useEffect12(() => {
3916
+ })),
3917
+ ...currentCover ? { cover: currentCover } : {}
3918
+ });
3919
+ for (const asset of assetsRef.current) project.addAsset(asset);
3920
+ return project;
3921
+ }, []);
3922
+ const exportOptions = useCallback12((signal) => {
3923
+ const resolver = assetResolverRef.current;
3924
+ return { ...resolver ? { assetResolver: resolver } : {}, ...signal ? { signal } : {} };
3925
+ }, []);
3926
+ useEffect14(() => {
3399
3927
  const requestId = ++previewRequestIdRef.current;
3928
+ const controller = new AbortController();
3400
3929
  let cancelled = false;
3401
3930
  setPreviewing(true);
3402
3931
  const timer = setTimeout(() => {
3403
3932
  void (async () => {
3404
3933
  try {
3405
- const resolver = assetResolverRef.current;
3406
3934
  const book = await parseEpub2(
3407
- await buildProject().export(resolver ? { assetResolver: resolver } : void 0)
3935
+ await buildProject().export(exportOptions(controller.signal))
3408
3936
  );
3409
3937
  if (cancelled || requestId !== previewRequestIdRef.current) return;
3410
3938
  setPreviewBook(book);
@@ -3412,7 +3940,7 @@ function useEpubProject(options = {}) {
3412
3940
  onPreviewRef.current?.(book);
3413
3941
  } catch (err) {
3414
3942
  if (!cancelled && requestId === previewRequestIdRef.current) {
3415
- setPreviewError(err instanceof Error ? err : new Error(String(err)));
3943
+ setPreviewError(toError(err));
3416
3944
  }
3417
3945
  } finally {
3418
3946
  if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
@@ -3423,104 +3951,122 @@ function useEpubProject(options = {}) {
3423
3951
  cancelled = true;
3424
3952
  previewRequestIdRef.current++;
3425
3953
  clearTimeout(timer);
3954
+ controller.abort();
3426
3955
  };
3427
- }, [buildProject, options.debounceMs]);
3956
+ }, [assets, buildProject, chapters, cover, exportOptions, metadata, options.debounceMs]);
3428
3957
  const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
3429
3958
  const setMetadata = useCallback12((patch) => {
3430
- 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);
3431
3968
  }, []);
3432
3969
  const setChapters = useCallback12(
3433
3970
  (next) => {
3434
- const normalized = next.length ? next : [defaultChapter3(0, defaultTitle, defaultBody)];
3435
- setChaptersState(normalized);
3436
- setSelectedChapterState((current) => {
3437
- const selectedId = chapters[current]?.id;
3438
- const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
3439
- return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
3440
- });
3441
- },
3442
- [chapters, defaultBody, defaultTitle]
3443
- );
3444
- const setSelectedChapter = useCallback12(
3445
- (index) => {
3446
- setSelectedChapterState(Math.max(0, Math.min(index, chapters.length - 1)));
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
+ );
3447
3979
  },
3448
- [chapters.length]
3980
+ [commit, defaultBody, defaultTitle]
3449
3981
  );
3982
+ const setCover = useCallback12((asset) => {
3983
+ coverRef.current = asset;
3984
+ setCoverState(asset);
3985
+ }, []);
3986
+ const setAssets = useCallback12((next) => {
3987
+ assetsRef.current = next;
3988
+ setAssetsState(next);
3989
+ }, []);
3990
+ const setSelectedChapter = useCallback12((index) => {
3991
+ const next = Math.max(0, Math.min(index, chaptersRef.current.length - 1));
3992
+ selectedRef.current = next;
3993
+ setSelectedChapterState(next);
3994
+ }, []);
3450
3995
  const patchChapter = useCallback12((index, patch) => {
3451
- setChaptersState(
3452
- (current) => current.map(
3453
- (chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
3454
- )
3996
+ const next = chaptersRef.current.map(
3997
+ (chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
3455
3998
  );
3999
+ chaptersRef.current = next;
4000
+ setChaptersState(next);
3456
4001
  }, []);
3457
4002
  const addChapter = useCallback12(
3458
4003
  (chapter = {}) => {
3459
- setChaptersState((current) => {
3460
- const generated = defaultChapter3(current.length, defaultTitle, defaultBody);
3461
- const next = [
3462
- ...current,
3463
- {
3464
- id: chapter.id ?? generated.id,
3465
- title: chapter.title ?? generated.title,
3466
- body: chapter.body ?? generated.body
3467
- }
3468
- ];
3469
- setSelectedChapterState(next.length - 1);
3470
- return next;
3471
- });
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);
3472
4015
  },
3473
- [defaultBody, defaultTitle]
4016
+ [commit, defaultBody, defaultTitle]
3474
4017
  );
3475
4018
  const removeChapter = useCallback12(
3476
- (index = selectedChapter) => {
3477
- setChaptersState((current) => {
3478
- if (current.length <= 1) return current;
3479
- const next = current.filter((_, chapterIndex) => chapterIndex !== index);
3480
- setSelectedChapterState((currentSelected) => {
3481
- if (currentSelected === index) return Math.max(0, Math.min(index, next.length - 1));
3482
- if (index < currentSelected) return currentSelected - 1;
3483
- return Math.max(0, Math.min(currentSelected, next.length - 1));
3484
- });
3485
- return next;
3486
- });
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);
3487
4029
  },
3488
- [selectedChapter]
4030
+ [commit]
3489
4031
  );
3490
- const reorderChapters = useCallback12((from, to) => {
3491
- setChaptersState((current) => {
3492
- 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;
3493
4036
  const next = [...current];
3494
4037
  const [moved] = next.splice(from, 1);
3495
4038
  const target = Math.max(0, Math.min(next.length, to));
3496
4039
  next.splice(target, 0, moved);
3497
- setSelectedChapterState((currentSelected) => {
3498
- if (currentSelected === from) return target;
3499
- if (from < currentSelected && target >= currentSelected) return currentSelected - 1;
3500
- if (from > currentSelected && target <= currentSelected) return currentSelected + 1;
3501
- return currentSelected;
3502
- });
3503
- return next;
3504
- });
3505
- }, []);
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
+ );
3506
4049
  const exportEpub = useCallback12(async () => {
3507
- const resolver = assetResolverRef.current;
3508
- const buffer = await buildProject().export(resolver ? { assetResolver: resolver } : void 0);
4050
+ const buffer = await buildProject().export(exportOptions());
3509
4051
  onExportRef.current?.(buffer);
3510
4052
  return buffer;
3511
- }, [buildProject]);
3512
- return useMemo9(
4053
+ }, [buildProject, exportOptions]);
4054
+ return useMemo10(
3513
4055
  () => ({
3514
4056
  metadata,
3515
4057
  chapters,
3516
4058
  selectedChapter,
3517
4059
  currentChapter,
4060
+ cover,
4061
+ assets,
3518
4062
  previewBook,
3519
4063
  previewError,
3520
4064
  previewing,
3521
4065
  setMetadata,
3522
4066
  setChapters,
3523
4067
  setSelectedChapter,
4068
+ setCover,
4069
+ setAssets,
3524
4070
  patchChapter,
3525
4071
  addChapter,
3526
4072
  removeChapter,
@@ -3530,8 +4076,10 @@ function useEpubProject(options = {}) {
3530
4076
  }),
3531
4077
  [
3532
4078
  addChapter,
4079
+ assets,
3533
4080
  buildProject,
3534
4081
  chapters,
4082
+ cover,
3535
4083
  currentChapter,
3536
4084
  exportEpub,
3537
4085
  metadata,
@@ -3542,27 +4090,34 @@ function useEpubProject(options = {}) {
3542
4090
  reorderChapters,
3543
4091
  removeChapter,
3544
4092
  selectedChapter,
4093
+ setAssets,
3545
4094
  setChapters,
4095
+ setCover,
3546
4096
  setMetadata,
3547
4097
  setSelectedChapter
3548
4098
  ]
3549
4099
  );
3550
4100
  }
3551
- 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" : "") {
3552
- return {
3553
- id: `chapter-${Date.now()}-${index}`,
3554
- title: titleFor(index),
3555
- body: bodyFor(index)
3556
- };
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) };
3557
4112
  }
3558
4113
 
3559
4114
  // src/useImageOverlay.ts
3560
- import { moveImageOverlayRect as moveImageOverlayRect2, resizeImageOverlayRect as resizeImageOverlayRect2 } from "@libraz/mejiro";
4115
+ import { createOverlayDragSession as createOverlayDragSession2, isPrimaryPointerPress as isPrimaryPointerPress3 } from "@libraz/mejiro/browser";
3561
4116
  import {
3562
4117
  useCallback as useCallback13,
3563
- useEffect as useEffect13,
3564
- useRef as useRef17,
3565
- useState as useState16
4118
+ useEffect as useEffect15,
4119
+ useRef as useRef18,
4120
+ useState as useState17
3566
4121
  } from "react";
3567
4122
  function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3568
4123
  const defW = options?.defaultWidth ?? 120;
@@ -3570,17 +4125,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3570
4125
  const defX = options?.defaultX ?? 80;
3571
4126
  const defY = options?.defaultY ?? 100;
3572
4127
  const margin = options?.margin;
3573
- const [imageRect, setImageRect] = useState16(null);
3574
- const rectRef = useRef17(imageRect);
4128
+ const [imageRect, setImageRect] = useState17(null);
4129
+ const rectRef = useRef18(imageRect);
3575
4130
  rectRef.current = imageRect;
3576
- const layoutRef = useRef17(layout);
4131
+ const layoutRef = useRef18(layout);
3577
4132
  layoutRef.current = layout;
3578
- const spreadRef = useRef17(spreadIdx);
4133
+ const spreadRef = useRef18(spreadIdx);
3579
4134
  spreadRef.current = spreadIdx;
3580
- const onUpdateRef = useRef17(onUpdate);
4135
+ const onUpdateRef = useRef18(onUpdate);
3581
4136
  onUpdateRef.current = onUpdate;
3582
- const activeDragCleanupsRef = useRef17(/* @__PURE__ */ new Set());
3583
- useEffect13(
4137
+ const activeDragCleanupsRef = useRef18(/* @__PURE__ */ new Set());
4138
+ useEffect15(
3584
4139
  () => () => {
3585
4140
  for (const cleanup of activeDragCleanupsRef.current) cleanup();
3586
4141
  activeDragCleanupsRef.current.clear();
@@ -3597,6 +4152,17 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3597
4152
  },
3598
4153
  [margin]
3599
4154
  );
4155
+ const syncedRef = useRef18({ layout, spreadIdx, margin });
4156
+ useEffect15(() => {
4157
+ const prev = syncedRef.current;
4158
+ syncedRef.current = { layout, spreadIdx, margin };
4159
+ if (prev.layout === layout && prev.spreadIdx === spreadIdx && prev.margin === margin) return;
4160
+ if (rectRef.current === null) return;
4161
+ if (prev.layout && prev.spreadIdx !== spreadIdx) {
4162
+ prev.layout.syncImages(prev.spreadIdx, void 0);
4163
+ }
4164
+ syncToLayout(rectRef.current);
4165
+ }, [layout, spreadIdx, margin, syncToLayout]);
3600
4166
  const toggleImage = useCallback13(() => {
3601
4167
  if (rectRef.current) {
3602
4168
  setImageRect(null);
@@ -3607,80 +4173,57 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3607
4173
  syncToLayout(r);
3608
4174
  }
3609
4175
  }, [defX, defY, defW, defH, syncToLayout]);
4176
+ const applyRect = useCallback13(
4177
+ (rect) => {
4178
+ setImageRect(rect);
4179
+ syncToLayout(rect);
4180
+ },
4181
+ [syncToLayout]
4182
+ );
3610
4183
  const onOverlayPointerDown = useCallback13(
3611
4184
  (e) => {
4185
+ if (!isPrimaryPointerPress3(e)) return;
3612
4186
  e.preventDefault();
3613
4187
  const current = rectRef.current;
3614
4188
  if (!current) return;
3615
- const startX = e.clientX;
3616
- const startY = e.clientY;
3617
- const start = { ...current };
3618
4189
  const target = e.currentTarget;
3619
- target.setPointerCapture(e.nativeEvent.pointerId);
3620
- target.classList.add("dragging");
3621
- let rafId = 0;
3622
- const onMove = (me) => {
3623
- const dx = me.clientX - startX;
3624
- const dy = me.clientY - startY;
3625
- const r = moveImageOverlayRect2(start, dx, dy);
3626
- cancelAnimationFrame(rafId);
3627
- rafId = requestAnimationFrame(() => {
3628
- setImageRect(r);
3629
- syncToLayout(r);
3630
- });
3631
- };
3632
- let cleanup;
3633
- const onUp = () => cleanup();
3634
- cleanup = () => {
3635
- cancelAnimationFrame(rafId);
3636
- target.classList.remove("dragging");
3637
- document.removeEventListener("pointermove", onMove);
3638
- document.removeEventListener("pointerup", onUp);
3639
- activeDragCleanupsRef.current.delete(cleanup);
3640
- };
3641
- document.addEventListener("pointermove", onMove);
3642
- document.addEventListener("pointerup", onUp);
3643
- activeDragCleanupsRef.current.add(cleanup);
4190
+ createOverlayDragSession2({
4191
+ mode: "move",
4192
+ rect: current,
4193
+ startX: e.clientX,
4194
+ startY: e.clientY,
4195
+ pointerId: e.nativeEvent.pointerId,
4196
+ captureElement: target,
4197
+ activeElement: target,
4198
+ dragClass: "is-dragging",
4199
+ registry: activeDragCleanupsRef.current,
4200
+ onChange: applyRect
4201
+ });
3644
4202
  },
3645
- [syncToLayout]
4203
+ [applyRect]
3646
4204
  );
3647
4205
  const onResizePointerDown = useCallback13(
3648
4206
  (e) => {
4207
+ if (!isPrimaryPointerPress3(e)) return;
3649
4208
  e.preventDefault();
3650
4209
  e.stopPropagation();
3651
4210
  const current = rectRef.current;
3652
4211
  if (!current) return;
3653
- const startX = e.clientX;
3654
- const startY = e.clientY;
3655
- const start = { ...current };
3656
4212
  const target = e.currentTarget;
3657
- target.setPointerCapture(e.nativeEvent.pointerId);
3658
- target.parentElement?.classList.add("dragging");
3659
- let rafId = 0;
3660
- const onMove = (me) => {
3661
- const dx = me.clientX - startX;
3662
- const dy = me.clientY - startY;
3663
- const r = resizeImageOverlayRect2(start, dx, dy);
3664
- cancelAnimationFrame(rafId);
3665
- rafId = requestAnimationFrame(() => {
3666
- setImageRect(r);
3667
- syncToLayout(r);
3668
- });
3669
- };
3670
- let cleanup;
3671
- const onUp = () => cleanup();
3672
- cleanup = () => {
3673
- cancelAnimationFrame(rafId);
3674
- target.parentElement?.classList.remove("dragging");
3675
- document.removeEventListener("pointermove", onMove);
3676
- document.removeEventListener("pointerup", onUp);
3677
- activeDragCleanupsRef.current.delete(cleanup);
3678
- };
3679
- document.addEventListener("pointermove", onMove);
3680
- document.addEventListener("pointerup", onUp);
3681
- activeDragCleanupsRef.current.add(cleanup);
4213
+ createOverlayDragSession2({
4214
+ mode: "resize",
4215
+ rect: current,
4216
+ startX: e.clientX,
4217
+ startY: e.clientY,
4218
+ pointerId: e.nativeEvent.pointerId,
4219
+ captureElement: target,
4220
+ activeElement: target.parentElement,
4221
+ dragClass: "is-dragging",
4222
+ registry: activeDragCleanupsRef.current,
4223
+ onChange: applyRect
4224
+ });
3682
4225
  },
3683
- [syncToLayout]
4226
+ [applyRect]
3684
4227
  );
3685
4228
  return {
3686
4229
  imageRect,
@@ -3692,122 +4235,140 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3692
4235
  }
3693
4236
 
3694
4237
  // src/useLibrary.ts
3695
- import { useCallback as useCallback14, useEffect as useEffect14, 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
+ }
3696
4245
  function useLibrary(options) {
3697
4246
  const { volumes, initialVolumeId, onChange } = options;
3698
- const initialIndex = initialVolumeId != null ? Math.max(
3699
- 0,
3700
- volumes.findIndex((v) => v.id === initialVolumeId)
3701
- ) : 0;
3702
- const [currentId, setCurrentId] = useState17(
3703
- volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
4247
+ const [currentId, setCurrentId] = useState18(
4248
+ () => resolveCurrentId(volumes, null, initialVolumeId)
3704
4249
  );
3705
- useEffect14(() => {
3706
- if (volumes.length === 0) {
3707
- if (currentId !== null) setCurrentId(null);
3708
- return;
3709
- }
3710
- if (currentId != null && volumes.some((v) => v.id === currentId)) return;
3711
- const fallback = volumes[0] ?? null;
3712
- setCurrentId(fallback?.id ?? null);
3713
- if (fallback) onChange?.(fallback);
3714
- }, [currentId, volumes, onChange]);
3715
- const index = useMemo10(() => {
3716
- if (volumes.length === 0) return -1;
3717
- if (currentId == null) return 0;
3718
- const found = volumes.findIndex((v) => v.id === currentId);
3719
- return found >= 0 ? found : 0;
3720
- }, [currentId, volumes]);
3721
- 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(
3722
4263
  () => index >= 0 && index < volumes.length ? volumes[index] : null,
3723
4264
  [index, volumes]
3724
4265
  );
3725
- const fire = useCallback14(
4266
+ const activate = useCallback14(
3726
4267
  (i) => {
3727
4268
  const v = volumes[i];
3728
- if (v) onChange?.(v);
4269
+ if (!v) return;
4270
+ setNavigated(true);
4271
+ setCurrentId(v.id);
4272
+ onChange?.(v);
3729
4273
  },
3730
4274
  [volumes, onChange]
3731
4275
  );
3732
4276
  const next = useCallback14(() => {
3733
4277
  if (index < 0) return;
3734
4278
  const ni = Math.min(volumes.length - 1, index + 1);
3735
- if (ni !== index) {
3736
- setCurrentId(volumes[ni]?.id ?? null);
3737
- fire(ni);
3738
- }
3739
- }, [index, volumes, fire]);
4279
+ if (ni !== index) activate(ni);
4280
+ }, [index, activate, volumes.length]);
3740
4281
  const prev = useCallback14(() => {
3741
4282
  if (index < 0) return;
3742
4283
  const ni = Math.max(0, index - 1);
3743
- if (ni !== index) {
3744
- setCurrentId(volumes[ni]?.id ?? null);
3745
- fire(ni);
3746
- }
3747
- }, [index, volumes, fire]);
4284
+ if (ni !== index) activate(ni);
4285
+ }, [index, activate]);
3748
4286
  const goTo = useCallback14(
3749
4287
  (id) => {
3750
4288
  const ni = volumes.findIndex((v) => v.id === id);
3751
4289
  if (ni < 0) return;
3752
- if (ni !== index) {
3753
- setCurrentId(id);
3754
- fire(ni);
3755
- }
4290
+ if (ni !== index) activate(ni);
3756
4291
  },
3757
- [index, volumes, fire]
4292
+ [index, volumes, activate]
3758
4293
  );
3759
4294
  return { list: volumes, current, currentIndex: index, next, prev, goTo };
3760
4295
  }
3761
4296
 
3762
4297
  // src/useManuscriptLayout.ts
3763
- import { useCallback as useCallback15, useEffect as useEffect15, useLayoutEffect as useLayoutEffect5, useRef as useRef18, useState as useState18 } from "react";
4298
+ import {
4299
+ useCallback as useCallback15,
4300
+ useEffect as useEffect17,
4301
+ useLayoutEffect as useLayoutEffect5,
4302
+ useRef as useRef19,
4303
+ useState as useState19
4304
+ } from "react";
3764
4305
  function useManuscriptLayout(book, chapter, surface, options = {}) {
3765
4306
  const dialect = options.dialect ?? "mejiro";
3766
4307
  const enableResize = options.enableResize ?? true;
3767
4308
  const resizeDebounce = options.resizeDebounce ?? 120;
3768
- const [layout, setLayout] = useState18(null);
3769
- const [pageWidth, setPageWidth] = useState18(0);
3770
- const [pageHeight, setPageHeight] = useState18(0);
3771
- const [contentHeight, setContentHeight] = useState18(0);
3772
- const [elapsedMs, setElapsedMs] = useState18(0);
3773
- const requestIdRef = useRef18(0);
3774
- const recompute = useCallback15(async () => {
3775
- const requestId = ++requestIdRef.current;
3776
- if (!(chapter && surface.current)) {
3777
- setLayout(null);
3778
- setPageWidth(0);
3779
- setPageHeight(0);
3780
- setContentHeight(0);
3781
- setElapsedMs(0);
3782
- return;
3783
- }
3784
- setLayout(null);
3785
- const dims = book.computePageSize(surface.current);
3786
- setPageWidth(dims.pageWidth);
3787
- setPageHeight(dims.pageHeight);
3788
- setContentHeight(dims.contentHeight);
3789
- const t0 = performance.now();
3790
- const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
3791
- if (requestId !== requestIdRef.current) return;
3792
- const next = layouts.values().next().value ?? null;
3793
- setLayout(next);
3794
- setElapsedMs(performance.now() - t0);
3795
- }, [book, chapter, dialect, surface]);
4309
+ const [layout, setLayout] = useState19(null);
4310
+ const [pageWidth, setPageWidth] = useState19(0);
4311
+ const [pageHeight, setPageHeight] = useState19(0);
4312
+ const [contentHeight, setContentHeight] = useState19(0);
4313
+ const [elapsedMs, setElapsedMs] = useState19(0);
4314
+ const requestIdRef = useRef19(0);
4315
+ const layoutRef = useRef19(null);
4316
+ layoutRef.current = layout;
4317
+ const pendingRestore = useRef19(null);
4318
+ const optionsRef = useRef19(options);
4319
+ optionsRef.current = options;
4320
+ const recompute = useCallback15(
4321
+ async (opts = {}) => {
4322
+ const blank = opts.blank ?? true;
4323
+ const requestId = ++requestIdRef.current;
4324
+ if (!(chapter && surface.current)) {
4325
+ setLayout(null);
4326
+ setPageWidth(0);
4327
+ setPageHeight(0);
4328
+ setContentHeight(0);
4329
+ setElapsedMs(0);
4330
+ return;
4331
+ }
4332
+ const captured = !blank && layoutRef.current ? optionsRef.current.capturePosition?.(layoutRef.current) ?? null : null;
4333
+ pendingRestore.current = captured;
4334
+ if (blank) setLayout(null);
4335
+ const dims = book.computePageSize(surface.current);
4336
+ setPageWidth(dims.pageWidth);
4337
+ setPageHeight(dims.pageHeight);
4338
+ setContentHeight(dims.contentHeight);
4339
+ const t0 = performance.now();
4340
+ const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
4341
+ if (requestId !== requestIdRef.current) return;
4342
+ const next = layouts.values().next().value ?? null;
4343
+ layoutRef.current = next;
4344
+ setLayout(next);
4345
+ setElapsedMs(performance.now() - t0);
4346
+ },
4347
+ [book, chapter, dialect, surface]
4348
+ );
3796
4349
  useLayoutEffect5(() => {
3797
4350
  requestIdRef.current++;
4351
+ pendingRestore.current = null;
3798
4352
  setLayout(null);
3799
4353
  setPageWidth(0);
3800
4354
  setPageHeight(0);
3801
4355
  setContentHeight(0);
3802
4356
  setElapsedMs(0);
3803
4357
  }, [recompute]);
3804
- useEffect15(() => {
4358
+ useEffect17(() => {
3805
4359
  void recompute();
3806
4360
  return () => {
3807
4361
  requestIdRef.current++;
3808
4362
  };
3809
4363
  }, [recompute]);
3810
- useEffect15(() => {
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(() => {
3811
4372
  if (!enableResize) return;
3812
4373
  const el = surface.current;
3813
4374
  if (!el) return;
@@ -3822,12 +4383,12 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
3822
4383
  const scheduleReflow = (immediate) => {
3823
4384
  clear();
3824
4385
  if (immediate) {
3825
- void recompute();
4386
+ void recompute({ blank: false });
3826
4387
  return;
3827
4388
  }
3828
4389
  timer = setTimeout(() => {
3829
4390
  timer = null;
3830
- void recompute();
4391
+ void recompute({ blank: false });
3831
4392
  }, resizeDebounce);
3832
4393
  };
3833
4394
  if (typeof ResizeObserver !== "undefined") {
@@ -3849,7 +4410,7 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
3849
4410
  clear();
3850
4411
  };
3851
4412
  }, [surface, enableResize, resizeDebounce, recompute]);
3852
- return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
4413
+ return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute, pendingRestore };
3853
4414
  }
3854
4415
 
3855
4416
  // src/useReadingPosition.ts
@@ -3857,75 +4418,44 @@ import {
3857
4418
  parseReadingPosition,
3858
4419
  serializeReadingPosition
3859
4420
  } from "@libraz/mejiro";
3860
- import { useCallback as useCallback16, useEffect as useEffect16, useRef as useRef19, useState as useState19 } from "react";
3861
- function resolveDefaultStorage2() {
3862
- if (typeof globalThis === "undefined") return null;
3863
- const w = globalThis.localStorage;
3864
- return w ?? null;
3865
- }
4421
+ import { useCallback as useCallback16, useEffect as useEffect18, useRef as useRef20, useState as useState20 } from "react";
3866
4422
  function useReadingPosition(options) {
3867
4423
  const { key, throttleMs = 250 } = options;
3868
- const storage = options.storage ?? resolveDefaultStorage2();
3869
- const [position, setPosition] = useState19(() => {
3870
- if (!storage) return null;
3871
- try {
3872
- return parseReadingPosition(storage.getItem(key));
3873
- } catch {
3874
- return null;
3875
- }
3876
- });
3877
- const storageRef = useRef19(storage);
3878
- storageRef.current = storage;
3879
- useEffect16(() => {
3880
- const currentStorage = storageRef.current;
3881
- if (!currentStorage) {
3882
- setPosition(null);
3883
- return;
3884
- }
3885
- try {
3886
- setPosition(parseReadingPosition(currentStorage.getItem(key)));
3887
- } catch {
3888
- setPosition(null);
3889
- }
3890
- }, [key]);
3891
- const timerRef = useRef19(null);
3892
- useEffect16(
3893
- () => () => {
3894
- if (timerRef.current) clearTimeout(timerRef.current);
3895
- },
3896
- []
4424
+ const storage = options.storage ?? resolveDefaultStorage();
4425
+ const [position, setPosition] = useState20(
4426
+ () => readStorage(storage, key, parseReadingPosition, null)
3897
4427
  );
3898
- const onChangeRef = useRef19(options.onChange);
4428
+ const storageRef = useRef20(storage);
4429
+ storageRef.current = storage;
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);
3899
4439
  onChangeRef.current = options.onChange;
3900
4440
  const save = useCallback16(
3901
4441
  (next) => {
3902
4442
  setPosition(next);
3903
4443
  const currentStorage = storageRef.current;
3904
4444
  if (currentStorage) {
3905
- if (timerRef.current) clearTimeout(timerRef.current);
3906
- timerRef.current = setTimeout(() => {
3907
- try {
3908
- currentStorage.setItem(key, serializeReadingPosition(next));
3909
- } catch {
3910
- }
4445
+ pending.schedule(() => {
4446
+ currentStorage.setItem(key, serializeReadingPosition(next));
3911
4447
  }, throttleMs);
3912
4448
  }
3913
4449
  onChangeRef.current?.(next);
3914
4450
  },
3915
- [key, throttleMs]
4451
+ [key, throttleMs, pending]
3916
4452
  );
3917
4453
  const clear = useCallback16(() => {
3918
4454
  setPosition(null);
3919
- if (timerRef.current) clearTimeout(timerRef.current);
3920
- const currentStorage = storageRef.current;
3921
- if (currentStorage) {
3922
- try {
3923
- currentStorage.removeItem(key);
3924
- } catch {
3925
- }
3926
- }
4455
+ pending.cancel();
4456
+ removeStorage(storageRef.current, key);
3927
4457
  onChangeRef.current?.(null);
3928
- }, [key]);
4458
+ }, [key, pending]);
3929
4459
  return { position, save, clear };
3930
4460
  }
3931
4461