@libraz/mejiro-react 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,134 +1,13 @@
1
1
  // src/i18n.tsx
2
+ import {
3
+ enMessages,
4
+ formatMessage,
5
+ messageCatalogs,
6
+ resolveMessages as resolveCoreMessages
7
+ } from "@libraz/mejiro";
2
8
  import { createContext, useContext, useMemo } from "react";
9
+ import { enMessages as enMessages2, jaMessages } from "@libraz/mejiro";
3
10
  import { jsx } from "react/jsx-runtime";
4
- var enMessages = {
5
- logoSubtitle: "Vertical Reader",
6
- openButton: "Open",
7
- settingsButton: "Settings",
8
- imageButton: "Image",
9
- imageRemoveButton: "Remove image",
10
- loading: "Loading...",
11
- prevSpread: "Previous spread",
12
- nextSpread: "Next spread",
13
- settingsFont: "Font",
14
- settingsSize: "Size",
15
- settingsSizeDown: "Decrease font size",
16
- settingsSizeUp: "Increase font size",
17
- settingsLayout: "Layout",
18
- settingsKinsoku: "Kinsoku",
19
- settingsHanging: "Hanging",
20
- settingsLineSpacing: "Line spacing",
21
- settingsStrict: "Strict",
22
- settingsLoose: "Loose",
23
- toggleOn: "On",
24
- toggleOff: "Off",
25
- dropZoneTitle: "Drop an EPUB here or click to open",
26
- dropZoneHint: ".epub files only",
27
- tocTitle: "Contents",
28
- tocSearchPlaceholder: "Search chapters\u2026",
29
- tocEmpty: 'No chapters match "{query}".',
30
- shelfTitle: "Library",
31
- chapterN: "Chapter {n}",
32
- spreadAnnouncement: "Spread {spread} of {total}",
33
- editorPreviewSubtitle: "Editor Preview",
34
- editorTitle: "EPUB Editor",
35
- editorNoBookLoaded: "No book loaded",
36
- editorParagraphs: "Paragraphs",
37
- editorProofread: "Proofread",
38
- editorApplyText: "Apply text",
39
- editorRuby: "Ruby",
40
- editorRubyHint: "Select base text in the textarea, then type the ruby reading below.",
41
- editorRubyRange: "Base: {start}-{end} ({count} chars)",
42
- editorRubyPlaceholder: "furigana",
43
- editorApplyRuby: "Apply ruby",
44
- editorImages: "Images",
45
- editorInsertImageAfterParagraph: "Insert image after paragraph",
46
- editorExportEpub: "Export EPUB",
47
- manuscriptPreviewSubtitle: "Manuscript Preview",
48
- manuscriptTitle: "Manuscript EPUB",
49
- manuscriptRubyHint: "Use \uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B or \u6F22\u5B57\u300A\u304B\u3093\u3058\u300B for ruby.",
50
- manuscriptMetadata: "Metadata",
51
- manuscriptChapters: "Chapters",
52
- manuscriptDraft: "Draft",
53
- manuscriptChooseCoverImage: "Choose cover image",
54
- manuscriptDefaultTitle: "New work",
55
- manuscriptDefaultChapterTitle: "Chapter {n}",
56
- manuscriptDefaultBody: "This is a ruby example: \uFF5Ckanji\u300Areading\u300B.\n\nPaste your draft here.",
57
- untitled: "Untitled",
58
- manuscriptAddChapter: "Add chapter",
59
- manuscriptRemove: "Remove",
60
- manuscriptEmphasisDots: "Emphasis dots",
61
- manuscriptTcy: "Tate-chu-yoko",
62
- manuscriptEm: "Em",
63
- manuscriptStrong: "Strong",
64
- manuscriptReorderHandle: "Reorder {title}"
65
- };
66
- var jaMessages = {
67
- logoSubtitle: "\u7E26\u66F8\u304D\u30EA\u30FC\u30C0\u30FC",
68
- openButton: "\u958B\u304F",
69
- settingsButton: "\u8A2D\u5B9A",
70
- imageButton: "\u753B\u50CF",
71
- imageRemoveButton: "\u753B\u50CF\u3092\u524A\u9664",
72
- loading: "\u8AAD\u307F\u8FBC\u307F\u4E2D\u2026",
73
- prevSpread: "\u524D\u306E\u898B\u958B\u304D",
74
- nextSpread: "\u6B21\u306E\u898B\u958B\u304D",
75
- settingsFont: "\u30D5\u30A9\u30F3\u30C8",
76
- settingsSize: "\u30B5\u30A4\u30BA",
77
- settingsSizeDown: "\u30D5\u30A9\u30F3\u30C8\u30B5\u30A4\u30BA\u3092\u4E0B\u3052\u308B",
78
- settingsSizeUp: "\u30D5\u30A9\u30F3\u30C8\u30B5\u30A4\u30BA\u3092\u4E0A\u3052\u308B",
79
- settingsLayout: "\u30EC\u30A4\u30A2\u30A6\u30C8",
80
- settingsKinsoku: "\u7981\u5247",
81
- settingsHanging: "\u3076\u3089\u4E0B\u3052",
82
- settingsLineSpacing: "\u884C\u9593",
83
- settingsStrict: "\u53B3\u683C",
84
- settingsLoose: "\u7DE9\u548C",
85
- toggleOn: "\u30AA\u30F3",
86
- toggleOff: "\u30AA\u30D5",
87
- dropZoneTitle: "EPUB \u3092\u30C9\u30ED\u30C3\u30D7\u307E\u305F\u306F\u30AF\u30EA\u30C3\u30AF\u3067\u958B\u304F",
88
- dropZoneHint: ".epub \u30D5\u30A1\u30A4\u30EB\u306E\u307F",
89
- tocTitle: "\u76EE\u6B21",
90
- tocSearchPlaceholder: "\u7AE0\u3092\u691C\u7D22\u2026",
91
- tocEmpty: "\u300C{query}\u300D\u306B\u4E00\u81F4\u3059\u308B\u7AE0\u306F\u3042\u308A\u307E\u305B\u3093\u3002",
92
- shelfTitle: "\u672C\u68DA",
93
- chapterN: "\u7B2C{n}\u7AE0",
94
- spreadAnnouncement: "{total}\u898B\u958B\u304D\u4E2D{spread}\u898B\u958B\u304D\u76EE",
95
- editorPreviewSubtitle: "\u7DE8\u96C6\u30D7\u30EC\u30D3\u30E5\u30FC",
96
- editorTitle: "EPUB \u30A8\u30C7\u30A3\u30BF\u30FC",
97
- editorNoBookLoaded: "\u672C\u304C\u8AAD\u307F\u8FBC\u307E\u308C\u3066\u3044\u307E\u305B\u3093",
98
- editorParagraphs: "\u6BB5\u843D",
99
- editorProofread: "\u6821\u6B63",
100
- editorApplyText: "\u672C\u6587\u3092\u9069\u7528",
101
- editorRuby: "\u30EB\u30D3",
102
- editorRubyHint: "\u30C6\u30AD\u30B9\u30C8\u30A8\u30EA\u30A2\u3067\u89AA\u6587\u5B57\u3092\u9078\u629E\u3057\u3066\u304B\u3089\u3001\u4E0B\u306B\u30EB\u30D3\u3092\u5165\u529B\u3057\u307E\u3059\u3002",
103
- editorRubyRange: "\u89AA\u6587\u5B57: {start}-{end}\uFF08{count}\u6587\u5B57\uFF09",
104
- editorRubyPlaceholder: "\u3075\u308A\u304C\u306A",
105
- editorApplyRuby: "\u30EB\u30D3\u3092\u9069\u7528",
106
- editorImages: "\u753B\u50CF",
107
- editorInsertImageAfterParagraph: "\u6BB5\u843D\u306E\u5F8C\u306B\u753B\u50CF\u3092\u633F\u5165",
108
- editorExportEpub: "EPUB \u3092\u66F8\u304D\u51FA\u3059",
109
- manuscriptPreviewSubtitle: "\u539F\u7A3F\u30D7\u30EC\u30D3\u30E5\u30FC",
110
- manuscriptTitle: "\u539F\u7A3F EPUB",
111
- manuscriptRubyHint: "\u30EB\u30D3\u306B\u306F \uFF5C\u6F22\u5B57\u300A\u304B\u3093\u3058\u300B \u307E\u305F\u306F \u6F22\u5B57\u300A\u304B\u3093\u3058\u300B \u3092\u4F7F\u3044\u307E\u3059\u3002",
112
- manuscriptMetadata: "\u30E1\u30BF\u30C7\u30FC\u30BF",
113
- manuscriptChapters: "\u7AE0",
114
- manuscriptDraft: "\u539F\u7A3F",
115
- manuscriptChooseCoverImage: "\u8868\u7D19\u753B\u50CF\u3092\u9078\u629E",
116
- manuscriptDefaultTitle: "\u65B0\u3057\u3044\u4F5C\u54C1",
117
- manuscriptDefaultChapterTitle: "\u7B2C{n}\u8A71",
118
- manuscriptDefaultBody: "\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",
119
- untitled: "\u7121\u984C",
120
- manuscriptAddChapter: "\u7AE0\u3092\u8FFD\u52A0",
121
- manuscriptRemove: "\u524A\u9664",
122
- manuscriptEmphasisDots: "\u508D\u70B9",
123
- manuscriptTcy: "\u7E26\u4E2D\u6A2A",
124
- manuscriptEm: "\u5F37\u8ABF",
125
- manuscriptStrong: "\u592A\u5B57",
126
- manuscriptReorderHandle: "{title} \u306E\u9806\u756A\u3092\u5909\u66F4"
127
- };
128
- var CATALOGS = {
129
- en: enMessages,
130
- ja: jaMessages
131
- };
132
11
  var I18nContext = createContext(enMessages);
133
12
  function MejiroI18nProvider({
134
13
  locale,
@@ -136,10 +15,10 @@ function MejiroI18nProvider({
136
15
  children
137
16
  }) {
138
17
  const fromContext = useContext(I18nContext);
139
- const value = useMemo(() => {
140
- const base = locale != null ? CATALOGS[locale] : fromContext;
141
- return messages ? { ...base, ...messages } : base;
142
- }, [locale, messages, fromContext]);
18
+ const value = useMemo(
19
+ () => resolveCoreMessages(locale, messages, locale != null ? enMessages : fromContext),
20
+ [locale, messages, fromContext]
21
+ );
143
22
  return /* @__PURE__ */ jsx(I18nContext.Provider, { value, children });
144
23
  }
145
24
  function useI18n(options = {}) {
@@ -147,19 +26,14 @@ function useI18n(options = {}) {
147
26
  const { locale, messages } = options;
148
27
  return useMemo(() => {
149
28
  if (locale == null && messages == null) return fromContext;
150
- const base = locale != null ? CATALOGS[locale] : fromContext;
151
- return messages ? { ...base, ...messages } : base;
29
+ return resolveCoreMessages(locale, messages, fromContext);
152
30
  }, [locale, messages, fromContext]);
153
31
  }
154
32
  function resolveMessages(locale, overrides) {
155
- const base = locale != null ? CATALOGS[locale] : enMessages;
156
- return overrides ? { ...base, ...overrides } : base;
33
+ return resolveCoreMessages(locale, overrides);
157
34
  }
158
35
  function format(template, vars) {
159
- return template.replace(
160
- /\{(\w+)\}/g,
161
- (_, k) => Object.hasOwn(vars, k) ? String(vars[k]) : `{${k}}`
162
- );
36
+ return formatMessage(template, vars);
163
37
  }
164
38
 
165
39
  // src/MejiroChapterNav.tsx
@@ -208,7 +82,10 @@ function MejiroChapterNav({
208
82
  }) })
209
83
  ] });
210
84
  }
211
- return /* @__PURE__ */ jsx2("div", { className: "mejiro-reader-chapter-nav", children: /* @__PURE__ */ jsx2("select", { value: chapter, onChange: (e) => onChange(Number(e.target.value)), children: epub.chapters.map((ch, i) => /* @__PURE__ */ jsx2("option", { value: i, children: ch.title ?? format(messages.chapterN, { n: i + 1 }) }, ch.title ?? `chapter-${i}`)) }) });
85
+ return /* @__PURE__ */ jsx2("div", { className: "mejiro-reader-chapter-nav", children: /* @__PURE__ */ jsx2("select", { value: chapter, onChange: (e) => onChange(Number(e.target.value)), children: epub.chapters.map((ch, i) => (
86
+ // biome-ignore lint/suspicious/noArrayIndexKey: chapter titles are not guaranteed unique
87
+ /* @__PURE__ */ jsx2("option", { value: i, children: ch.title ?? format(messages.chapterN, { n: i + 1 }) }, i)
88
+ )) }) });
212
89
  }
213
90
 
214
91
  // src/MejiroDropZone.tsx
@@ -274,10 +151,10 @@ function MejiroDropZone({
274
151
 
275
152
  // src/MejiroEditor.tsx
276
153
  import { EditableEpub } from "@libraz/mejiro/epub";
277
- import { useCallback as useCallback7, useEffect as useEffect7, useRef as useRef10, useState as useState8 } from "react";
154
+ import { useCallback as useCallback7, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef10, useState as useState8 } from "react";
278
155
 
279
156
  // src/MejiroReader.tsx
280
- import { DEFAULT_BOOK_OPTIONS } from "@libraz/mejiro/book";
157
+ import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
281
158
  import { normalizeFontFamily as normalizeFontFamily5 } from "@libraz/mejiro/browser";
282
159
  import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
283
160
  import {
@@ -285,6 +162,7 @@ import {
285
162
  useCallback as useCallback6,
286
163
  useEffect as useEffect6,
287
164
  useImperativeHandle,
165
+ useLayoutEffect as useLayoutEffect4,
288
166
  useMemo as useMemo4,
289
167
  useRef as useRef9,
290
168
  useState as useState7
@@ -311,38 +189,36 @@ import {
311
189
 
312
190
  // src/MejiroPageView.tsx
313
191
  import { normalizeFontFamily } from "@libraz/mejiro/browser";
314
- import { Fragment as Fragment3 } from "react";
315
192
 
316
- // src/MejiroPage.tsx
317
- import { Fragment as Fragment2 } from "react";
318
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
193
+ // src/renderInlineNode.tsx
194
+ import {
195
+ segmentToInlineNode
196
+ } from "@libraz/mejiro/render";
197
+ import { createElement, Fragment as Fragment2 } from "react";
198
+ import { jsx as jsx4 } from "react/jsx-runtime";
319
199
  function renderSegment(segment, key) {
320
- switch (segment.type) {
321
- case "text":
322
- return /* @__PURE__ */ jsx4(Fragment2, { children: segment.text }, key);
323
- case "ruby":
324
- return /* @__PURE__ */ jsxs4("ruby", { children: [
325
- segment.base,
326
- /* @__PURE__ */ jsx4("rt", { children: segment.rubyText })
327
- ] }, key);
328
- case "emphasis":
329
- return /* @__PURE__ */ jsx4("span", { className: `mejiro-emphasis mejiro-emphasis--${segment.style}`, children: segment.text }, key);
330
- case "tcy":
331
- return /* @__PURE__ */ jsx4("span", { className: "mejiro-tcy", children: segment.text }, key);
332
- case "em":
333
- return /* @__PURE__ */ jsx4("em", { children: segment.text }, key);
334
- case "strong":
335
- return /* @__PURE__ */ jsx4("strong", { children: segment.text }, key);
336
- case "link":
337
- return /* @__PURE__ */ jsx4("a", { href: segment.href, title: segment.title, children: segment.text }, key);
338
- case "footnote-ref":
339
- return /* @__PURE__ */ jsx4("a", { className: "mejiro-footnote-ref", href: `#${segment.noteId}`, children: segment.text }, key);
340
- }
200
+ return renderInlineNode(segmentToInlineNode(segment), key);
341
201
  }
202
+ function renderInlineNode(node, key) {
203
+ if (node.type === "text") return /* @__PURE__ */ jsx4(Fragment2, { children: node.text }, key);
204
+ return createElement(
205
+ node.tag,
206
+ {
207
+ key,
208
+ className: node.className,
209
+ href: node.href,
210
+ title: node.title
211
+ },
212
+ node.children.map((child, index) => renderInlineNode(child, `${key}-${index}`))
213
+ );
214
+ }
215
+
216
+ // src/MejiroPage.tsx
217
+ import { jsx as jsx5 } from "react/jsx-runtime";
342
218
  function renderLine(line, lineIndex) {
343
219
  const nodes = [];
344
220
  if (lineIndex > 0) {
345
- nodes.push(/* @__PURE__ */ jsx4("br", {}, `br-${lineIndex}`));
221
+ nodes.push(/* @__PURE__ */ jsx5("br", {}, `br-${lineIndex}`));
346
222
  }
347
223
  for (let i = 0; i < line.segments.length; i++) {
348
224
  nodes.push(renderSegment(line.segments[i], `${lineIndex}-${i}`));
@@ -351,7 +227,7 @@ function renderLine(line, lineIndex) {
351
227
  }
352
228
  function MejiroPage({ page, className, style }) {
353
229
  const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
354
- return /* @__PURE__ */ jsx4("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
230
+ return /* @__PURE__ */ jsx5("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
355
231
  let paraClass = "mejiro-paragraph";
356
232
  if (paragraph.headingLevel != null) {
357
233
  paraClass += ` mejiro-paragraph--h${paragraph.headingLevel}`;
@@ -360,36 +236,13 @@ function MejiroPage({ page, className, style }) {
360
236
  }
361
237
  return (
362
238
  // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
363
- /* @__PURE__ */ jsx4("div", { className: paraClass, children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
239
+ /* @__PURE__ */ jsx5("div", { className: paraClass, children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
364
240
  );
365
241
  }) });
366
242
  }
367
243
 
368
244
  // src/MejiroPageView.tsx
369
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
370
- function renderSlotSegment(segment, key) {
371
- switch (segment.type) {
372
- case "text":
373
- return /* @__PURE__ */ jsx5(Fragment3, { children: segment.text }, key);
374
- case "ruby":
375
- return /* @__PURE__ */ jsxs5("ruby", { children: [
376
- segment.base,
377
- /* @__PURE__ */ jsx5("rt", { children: segment.rubyText })
378
- ] }, key);
379
- case "emphasis":
380
- return /* @__PURE__ */ jsx5("span", { className: `mejiro-emphasis mejiro-emphasis--${segment.style}`, children: segment.text }, key);
381
- case "tcy":
382
- return /* @__PURE__ */ jsx5("span", { className: "mejiro-tcy", children: segment.text }, key);
383
- case "em":
384
- return /* @__PURE__ */ jsx5("em", { children: segment.text }, key);
385
- case "strong":
386
- return /* @__PURE__ */ jsx5("strong", { children: segment.text }, key);
387
- case "link":
388
- return /* @__PURE__ */ jsx5("a", { href: segment.href, title: segment.title, children: segment.text }, key);
389
- case "footnote-ref":
390
- return /* @__PURE__ */ jsx5("a", { className: "mejiro-footnote-ref", href: `#${segment.noteId}`, children: segment.text }, key);
391
- }
392
- }
245
+ import { jsx as jsx6 } from "react/jsx-runtime";
393
246
  function MejiroPageView({
394
247
  result,
395
248
  fontFamily,
@@ -401,7 +254,7 @@ function MejiroPageView({
401
254
  if (result.hasImages || slotMode) {
402
255
  const rootClass = className ? `mejiro-page-slots ${className}` : "mejiro-page-slots";
403
256
  const fontFamilyCss = fontFamily != null ? normalizeFontFamily(fontFamily) : void 0;
404
- return /* @__PURE__ */ jsx5("div", { className: rootClass, style: { position: "relative", ...style }, children: result.lines.map((line, i) => {
257
+ return /* @__PURE__ */ jsx6("div", { className: rootClass, style: { position: "relative", ...style }, children: result.lines.map((line, i) => {
405
258
  const slot = result.slots[i];
406
259
  if (!slot || slot.height <= 0) return null;
407
260
  const colStyle = {
@@ -419,15 +272,15 @@ function MejiroPageView({
419
272
  };
420
273
  return (
421
274
  // biome-ignore lint/suspicious/noArrayIndexKey: lines have no stable ID
422
- /* @__PURE__ */ jsx5("div", { style: colStyle, children: line.segments.map((seg, si) => renderSlotSegment(seg, `${i}-${si}`)) }, i)
275
+ /* @__PURE__ */ jsx6("div", { style: colStyle, children: line.segments.map((seg, si) => renderSegment(seg, `${i}-${si}`)) }, i)
423
276
  );
424
277
  }) });
425
278
  }
426
- return /* @__PURE__ */ jsx5(MejiroPage, { page: result.page, className, style });
279
+ return /* @__PURE__ */ jsx6(MejiroPage, { page: result.page, className, style });
427
280
  }
428
281
 
429
282
  // src/MejiroScrollView.tsx
430
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
283
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
431
284
  function MejiroScrollView({
432
285
  layout,
433
286
  pageWidth,
@@ -444,10 +297,12 @@ function MejiroScrollView({
444
297
  const pageRefs = useRef2([]);
445
298
  const onVisiblePageChangeRef = useRef2(onVisiblePageChange);
446
299
  onVisiblePageChangeRef.current = onVisiblePageChange;
300
+ const programmaticScrollRef = useRef2(false);
447
301
  const pages = useMemo2(() => {
448
302
  const total = layout.totalPages;
449
303
  return Array.from({ length: total }, (_, i) => layout.getPage(i));
450
304
  }, [layout]);
305
+ const pageCount = pages.length;
451
306
  const useSlot = true;
452
307
  const contentStyle = { height: contentHeight };
453
308
  if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
@@ -456,6 +311,7 @@ function MejiroScrollView({
456
311
  useEffect(() => {
457
312
  const container = containerRef.current;
458
313
  if (!container) return;
314
+ if (pageCount === 0) return;
459
315
  if (typeof IntersectionObserver === "undefined") return;
460
316
  let mostVisibleIdx = -1;
461
317
  let mostVisibleRatio = 0;
@@ -474,7 +330,12 @@ function MejiroScrollView({
474
330
  }
475
331
  }
476
332
  }
477
- if (mostVisibleIdx >= 0) onVisiblePageChangeRef.current?.(mostVisibleIdx);
333
+ if (mostVisibleIdx >= 0) {
334
+ onVisiblePageChangeRef.current?.(
335
+ mostVisibleIdx,
336
+ programmaticScrollRef.current ? "programmatic" : "user"
337
+ );
338
+ }
478
339
  },
479
340
  { root: container, threshold: [0.25, 0.5, 0.75] }
480
341
  );
@@ -482,22 +343,28 @@ function MejiroScrollView({
482
343
  if (el) observer.observe(el);
483
344
  }
484
345
  return () => observer.disconnect();
485
- }, []);
346
+ }, [pageCount]);
486
347
  useLayoutEffect(() => {
487
348
  if (scrollToPage == null) return;
349
+ if (pageCount === 0) return;
488
350
  const el = pageRefs.current[scrollToPage];
489
351
  if (!(el && containerRef.current)) return;
352
+ programmaticScrollRef.current = true;
490
353
  containerRef.current.scrollTo({
491
354
  top: el.offsetTop,
492
355
  behavior: "auto"
493
356
  });
494
- }, [scrollToPage]);
495
- return /* @__PURE__ */ jsx6("div", { ref: containerRef, className: "mejiro-reader-scroll", children: /* @__PURE__ */ jsx6(
357
+ const timer = setTimeout(() => {
358
+ programmaticScrollRef.current = false;
359
+ }, 0);
360
+ return () => clearTimeout(timer);
361
+ }, [scrollToPage, pageCount]);
362
+ return /* @__PURE__ */ jsx7("div", { ref: containerRef, className: "mejiro-reader-scroll", children: /* @__PURE__ */ jsx7(
496
363
  "div",
497
364
  {
498
365
  className: "mejiro-reader-scroll-track",
499
366
  style: { display: "flex", flexDirection: "column", gap: pageGap },
500
- children: pages.map((result, i) => /* @__PURE__ */ jsxs6(
367
+ children: pages.map((result, i) => /* @__PURE__ */ jsxs4(
501
368
  "div",
502
369
  {
503
370
  ref: (el) => {
@@ -507,12 +374,12 @@ function MejiroScrollView({
507
374
  className: "mejiro-reader-page",
508
375
  style: { width: pageWidth, height: pageHeight, flexShrink: 0 },
509
376
  children: [
510
- /* @__PURE__ */ jsx6("div", { className: "mejiro-reader-page-rule" }),
511
- /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-page-header", children: [
512
- /* @__PURE__ */ jsx6("span", { className: "mejiro-reader-page-header-title" }),
513
- /* @__PURE__ */ jsx6("span", { className: "mejiro-reader-page-header-num", children: i + 1 })
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 })
514
381
  ] }),
515
- /* @__PURE__ */ jsx6("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsx6("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: /* @__PURE__ */ jsx6(
382
+ /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: /* @__PURE__ */ jsx7(
516
383
  MejiroPageView,
517
384
  {
518
385
  result,
@@ -533,11 +400,23 @@ function MejiroScrollView({
533
400
 
534
401
  // src/MejiroSettingsPanel.tsx
535
402
  import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
536
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
403
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
537
404
  var DEFAULT_FONTS = [
538
405
  { value: "serif", label: "System Serif" },
539
406
  { value: "sans-serif", label: "System Sans" }
540
407
  ];
408
+ function fontLabelFromCss(css) {
409
+ const first = css.split(",")[0]?.trim() ?? css;
410
+ return first.replace(/^["']|["']$/g, "") || css;
411
+ }
412
+ function clampNumber(value, min, max, fallback) {
413
+ if (!Number.isFinite(value)) return fallback;
414
+ return Math.max(min, Math.min(max, value));
415
+ }
416
+ function parseClampedInput(value, min, max, fallback) {
417
+ if (value.trim() === "") return fallback;
418
+ return clampNumber(Number(value), min, max, fallback);
419
+ }
541
420
  function MejiroSettingsPanel({
542
421
  open,
543
422
  settings,
@@ -548,20 +427,15 @@ function MejiroSettingsPanel({
548
427
  }) {
549
428
  const messages = useI18n();
550
429
  const patch = (next) => onChange({ ...settings, ...next });
551
- return /* @__PURE__ */ jsx7("div", { className: `mejiro-reader-settings-panel${open ? " is-open" : ""}`, children: /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-settings-inner", children: [
552
- /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-settings-group", children: [
553
- /* @__PURE__ */ jsx7("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
554
- /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx7(
555
- "select",
556
- {
557
- value: normalizeFontFamily3(settings.fontFamily),
558
- onChange: (e) => patch({ fontFamily: e.target.value }),
559
- children: fonts.map((f) => /* @__PURE__ */ jsx7("option", { value: f.value, children: f.label }, f.value))
560
- }
561
- ) }),
562
- /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-control", children: [
563
- /* @__PURE__ */ jsx7("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-font-size", children: messages.settingsSize }),
564
- /* @__PURE__ */ jsx7(
430
+ const currentFont = normalizeFontFamily3(settings.fontFamily);
431
+ 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(
565
439
  "button",
566
440
  {
567
441
  type: "button",
@@ -571,7 +445,7 @@ function MejiroSettingsPanel({
571
445
  children: "A\u2212"
572
446
  }
573
447
  ),
574
- /* @__PURE__ */ jsx7(
448
+ /* @__PURE__ */ jsx8(
575
449
  "input",
576
450
  {
577
451
  id: "mejiro-reader-font-size",
@@ -579,10 +453,17 @@ function MejiroSettingsPanel({
579
453
  value: settings.fontSize,
580
454
  min: minFontSize,
581
455
  max: maxFontSize,
582
- onChange: (e) => patch({ fontSize: Number(e.target.value) })
456
+ onChange: (e) => patch({
457
+ fontSize: parseClampedInput(
458
+ e.target.value,
459
+ minFontSize,
460
+ maxFontSize,
461
+ settings.fontSize
462
+ )
463
+ })
583
464
  }
584
465
  ),
585
- /* @__PURE__ */ jsx7(
466
+ /* @__PURE__ */ jsx8(
586
467
  "button",
587
468
  {
588
469
  type: "button",
@@ -594,41 +475,41 @@ function MejiroSettingsPanel({
594
475
  )
595
476
  ] })
596
477
  ] }),
597
- /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-settings-group", children: [
598
- /* @__PURE__ */ jsx7("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsLayout }),
599
- /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-control", children: [
600
- /* @__PURE__ */ jsx7("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-kinsoku", children: messages.settingsKinsoku }),
601
- /* @__PURE__ */ jsxs7(
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(
602
483
  "select",
603
484
  {
604
485
  id: "mejiro-reader-kinsoku",
605
486
  value: settings.mode ?? "strict",
606
487
  onChange: (e) => patch({ mode: e.target.value }),
607
488
  children: [
608
- /* @__PURE__ */ jsx7("option", { value: "strict", children: messages.settingsStrict }),
609
- /* @__PURE__ */ jsx7("option", { value: "loose", children: messages.settingsLoose })
489
+ /* @__PURE__ */ jsx8("option", { value: "strict", children: messages.settingsStrict }),
490
+ /* @__PURE__ */ jsx8("option", { value: "loose", children: messages.settingsLoose })
610
491
  ]
611
492
  }
612
493
  )
613
494
  ] }),
614
- /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-control", children: [
615
- /* @__PURE__ */ jsx7("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-hanging", children: messages.settingsHanging }),
616
- /* @__PURE__ */ jsxs7(
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(
617
498
  "select",
618
499
  {
619
500
  id: "mejiro-reader-hanging",
620
501
  value: String(settings.enableHanging ?? true),
621
502
  onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
622
503
  children: [
623
- /* @__PURE__ */ jsx7("option", { value: "true", children: messages.toggleOn }),
624
- /* @__PURE__ */ jsx7("option", { value: "false", children: messages.toggleOff })
504
+ /* @__PURE__ */ jsx8("option", { value: "true", children: messages.toggleOn }),
505
+ /* @__PURE__ */ jsx8("option", { value: "false", children: messages.toggleOff })
625
506
  ]
626
507
  }
627
508
  )
628
509
  ] }),
629
- /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-control", children: [
630
- /* @__PURE__ */ jsx7("label", { className: "mejiro-reader-control-label", htmlFor: "mejiro-reader-line-spacing", children: messages.settingsLineSpacing }),
631
- /* @__PURE__ */ jsx7(
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(
632
513
  "input",
633
514
  {
634
515
  id: "mejiro-reader-line-spacing",
@@ -638,12 +519,19 @@ function MejiroSettingsPanel({
638
519
  min: 1,
639
520
  max: 3,
640
521
  step: 0.1,
641
- onChange: (e) => patch({ lineSpacing: Number(e.target.value) })
522
+ onChange: (e) => patch({
523
+ lineSpacing: parseClampedInput(
524
+ e.target.value,
525
+ 1,
526
+ 3,
527
+ settings.lineSpacing ?? 1.8
528
+ )
529
+ })
642
530
  }
643
531
  )
644
532
  ] })
645
533
  ] })
646
- ] }) });
534
+ ] }) }) });
647
535
  }
648
536
 
649
537
  // src/MejiroSpread.tsx
@@ -653,7 +541,7 @@ import {
653
541
  } from "react";
654
542
 
655
543
  // src/MejiroImageOverlay.tsx
656
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
544
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
657
545
  function MejiroImageOverlay({
658
546
  rect,
659
547
  label,
@@ -663,7 +551,7 @@ function MejiroImageOverlay({
663
551
  }) {
664
552
  const messages = useI18n();
665
553
  const resolvedLabel = label ?? messages.imageButton;
666
- return /* @__PURE__ */ jsxs8(
554
+ return /* @__PURE__ */ jsxs6(
667
555
  "div",
668
556
  {
669
557
  className: "mejiro-reader-image-overlay",
@@ -675,21 +563,28 @@ function MejiroImageOverlay({
675
563
  },
676
564
  onPointerDown: onOverlayPointerDown,
677
565
  children: [
678
- /* @__PURE__ */ jsxs8("div", { className: "mejiro-reader-image-overlay-label", children: [
679
- /* @__PURE__ */ jsx8("div", { className: "mejiro-reader-image-overlay-icon" }),
680
- /* @__PURE__ */ jsx8("span", { children: resolvedLabel })
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 })
681
569
  ] }),
682
- /* @__PURE__ */ jsx8("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
683
- /* @__PURE__ */ jsx8(
570
+ /* @__PURE__ */ jsx9("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
571
+ /* @__PURE__ */ jsx9(
684
572
  "button",
685
573
  {
686
574
  type: "button",
687
575
  "aria-label": messages.imageRemoveButton,
576
+ title: messages.imageRemoveButton,
688
577
  className: "mejiro-reader-image-overlay-close",
689
578
  onPointerDown: (e) => {
690
579
  e.stopPropagation();
691
580
  e.preventDefault();
692
581
  onClose?.();
582
+ },
583
+ onKeyDown: (e) => {
584
+ if (e.key !== "Enter" && e.key !== " ") return;
585
+ e.stopPropagation();
586
+ e.preventDefault();
587
+ onClose?.();
693
588
  }
694
589
  }
695
590
  )
@@ -699,7 +594,7 @@ function MejiroImageOverlay({
699
594
  }
700
595
 
701
596
  // src/MejiroSelectionLayer.tsx
702
- import { jsx as jsx9 } from "react/jsx-runtime";
597
+ import { jsx as jsx10 } from "react/jsx-runtime";
703
598
  function MejiroSelectionLayer({
704
599
  rects,
705
600
  side,
@@ -710,7 +605,7 @@ function MejiroSelectionLayer({
710
605
  }) {
711
606
  const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
712
607
  const rectClass = rectClassName ?? "mejiro-selection-rect";
713
- return /* @__PURE__ */ jsx9(
608
+ return /* @__PURE__ */ jsx10(
714
609
  "div",
715
610
  {
716
611
  className: wrapperClass,
@@ -723,7 +618,7 @@ function MejiroSelectionLayer({
723
618
  children: rects.map((r, i) => {
724
619
  if (r.side !== side) return null;
725
620
  const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
726
- return /* @__PURE__ */ jsx9(
621
+ return /* @__PURE__ */ jsx10(
727
622
  "div",
728
623
  {
729
624
  className: rectClass,
@@ -743,11 +638,11 @@ function MejiroSelectionLayer({
743
638
  }
744
639
 
745
640
  // src/MejiroSpread.tsx
746
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
641
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
747
642
  function defaultHeader(data) {
748
- return /* @__PURE__ */ jsxs9("div", { className: "mejiro-reader-page-header", children: [
749
- /* @__PURE__ */ jsx10("span", { className: "mejiro-reader-page-header-title", children: data.title ?? "" }),
750
- /* @__PURE__ */ jsx10("span", { className: "mejiro-reader-page-header-num", children: data.pageNumber != null ? String(data.pageNumber) : "" })
643
+ return /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-page-header", children: [
644
+ /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-page-header-title", children: data.title ?? "" }),
645
+ /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-page-header-num", children: data.pageNumber != null ? String(data.pageNumber) : "" })
751
646
  ] });
752
647
  }
753
648
  function MejiroSpread({
@@ -862,16 +757,16 @@ function MejiroSpread({
862
757
  height: pageHeight
863
758
  };
864
759
  if (isRight && hasImages) pageStyle.overflow = "visible";
865
- return /* @__PURE__ */ jsxs9(
760
+ return /* @__PURE__ */ jsxs7(
866
761
  "div",
867
762
  {
868
763
  className: `mejiro-reader-page mejiro-reader-page--${side}`,
869
764
  style: pageStyle,
870
765
  children: [
871
- /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-page-rule" }),
766
+ /* @__PURE__ */ jsx11("div", { className: "mejiro-reader-page-rule" }),
872
767
  renderPageHeader ? renderPageHeader(side, header) : defaultHeader(header),
873
- /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs9("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
874
- /* @__PURE__ */ jsx10(
768
+ /* @__PURE__ */ jsx11("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
769
+ /* @__PURE__ */ jsx11(
875
770
  MejiroPageView,
876
771
  {
877
772
  result,
@@ -883,9 +778,9 @@ function MejiroSpread({
883
778
  },
884
779
  pageKey
885
780
  ),
886
- selectionRects && selectionRects.length > 0 && /* @__PURE__ */ jsx10(MejiroSelectionLayer, { rects: selectionRects, side })
781
+ selectionRects && selectionRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: selectionRects, side })
887
782
  ] }) }),
888
- isRight && images.map((item) => /* @__PURE__ */ jsx10(
783
+ isRight && images.map((item) => /* @__PURE__ */ jsx11(
889
784
  MejiroImageOverlay,
890
785
  {
891
786
  rect: item.rect,
@@ -912,7 +807,7 @@ function MejiroSpread({
912
807
  if (selectionEnabled) handlePointerUp(e);
913
808
  }
914
809
  const useCombined = selectionEnabled || gestureEnabled;
915
- return /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs9(
810
+ return /* @__PURE__ */ jsx11("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs7(
916
811
  "div",
917
812
  {
918
813
  className: `mejiro-reader-spread${turning ? " is-turning" : ""}${singlePage ? " mejiro-reader-spread--single" : ""}`,
@@ -923,7 +818,7 @@ function MejiroSpread({
923
818
  children: [
924
819
  renderPage("right"),
925
820
  !singlePage && renderPage("left"),
926
- /* @__PURE__ */ jsx10(
821
+ /* @__PURE__ */ jsx11(
927
822
  "button",
928
823
  {
929
824
  type: "button",
@@ -932,7 +827,7 @@ function MejiroSpread({
932
827
  onClick: onPrev
933
828
  }
934
829
  ),
935
- /* @__PURE__ */ jsx10(
830
+ /* @__PURE__ */ jsx11(
936
831
  "button",
937
832
  {
938
833
  type: "button",
@@ -949,7 +844,7 @@ function MejiroSpread({
949
844
 
950
845
  // src/MejiroStats.tsx
951
846
  import { estimateReadingTime, formatReadingTime } from "@libraz/mejiro/book";
952
- import { jsx as jsx11 } from "react/jsx-runtime";
847
+ import { jsx as jsx12 } from "react/jsx-runtime";
953
848
  function MejiroStats({
954
849
  chapter,
955
850
  totalPages,
@@ -959,7 +854,7 @@ function MejiroStats({
959
854
  cpm = 600,
960
855
  readingTimeLocale = "ja"
961
856
  }) {
962
- if (!chapter) return /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-stats" });
857
+ if (!chapter) return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats" });
963
858
  const totalChars = chapter.paragraphs.reduce((s, p) => s + p.text.length, 0);
964
859
  const totalRuby = chapter.paragraphs.reduce(
965
860
  (s, p) => s + p.inlineAnnotations.filter((a) => a.kind === "ruby").length,
@@ -974,11 +869,17 @@ function MejiroStats({
974
869
  fontLabel || null,
975
870
  `${elapsedMs.toFixed(0)}ms`
976
871
  ];
977
- return /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-stats", children: parts.filter(Boolean).join(" / ") });
872
+ return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats", children: parts.filter(Boolean).join(" / ") });
978
873
  }
979
874
 
980
875
  // src/useChapterLayout.ts
981
- import { useCallback, useEffect as useEffect2, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState2 } from "react";
876
+ import {
877
+ useCallback,
878
+ useEffect as useEffect2,
879
+ useLayoutEffect as useLayoutEffect2,
880
+ useRef as useRef4,
881
+ useState as useState2
882
+ } from "react";
982
883
  function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
983
884
  const enableResize = options.enableResize ?? true;
984
885
  const resizeDebounce = options.resizeDebounce ?? 120;
@@ -989,31 +890,43 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
989
890
  const [elapsedMs, setElapsedMs] = useState2(0);
990
891
  const layoutRef = useRef4(null);
991
892
  const requestIdRef = useRef4(0);
893
+ const pendingRestore = useRef4(null);
992
894
  layoutRef.current = layout;
993
- const recompute = useCallback(async () => {
994
- const requestId = ++requestIdRef.current;
995
- const chapter = epub?.chapters[chapterIndex];
996
- if (!(chapter && surface.current)) {
997
- setLayout(null);
998
- setPageWidth(0);
999
- setPageHeight(0);
1000
- setContentHeight(0);
1001
- setElapsedMs(0);
1002
- return;
1003
- }
1004
- setLayout(null);
1005
- const dims = book.computePageSize(surface.current);
1006
- setPageWidth(dims.pageWidth);
1007
- setPageHeight(dims.pageHeight);
1008
- setContentHeight(dims.contentHeight);
1009
- const t0 = performance.now();
1010
- const l = await book.layoutChapter(chapter);
1011
- if (requestId !== requestIdRef.current) return;
1012
- setLayout(l);
1013
- setElapsedMs(performance.now() - t0);
1014
- }, [book, epub, chapterIndex, surface]);
895
+ const optionsRef = useRef4(options);
896
+ optionsRef.current = options;
897
+ const recompute = useCallback(
898
+ async (opts = {}) => {
899
+ const blank = opts.blank ?? true;
900
+ const o = optionsRef.current;
901
+ const requestId = ++requestIdRef.current;
902
+ const chapter = epub?.chapters[chapterIndex];
903
+ if (!(chapter && surface.current)) {
904
+ setLayout(null);
905
+ setPageWidth(0);
906
+ setPageHeight(0);
907
+ setContentHeight(0);
908
+ setElapsedMs(0);
909
+ return;
910
+ }
911
+ const captured = !blank && layoutRef.current ? o.capturePosition?.(layoutRef.current) ?? null : null;
912
+ pendingRestore.current = captured;
913
+ if (blank) setLayout(null);
914
+ const dims = book.computePageSize(surface.current, o.pageGeometry?.());
915
+ setPageWidth(dims.pageWidth);
916
+ setPageHeight(dims.pageHeight);
917
+ setContentHeight(dims.contentHeight);
918
+ const t0 = performance.now();
919
+ const l = await book.layoutChapter(chapter);
920
+ if (requestId !== requestIdRef.current) return;
921
+ layoutRef.current = l;
922
+ setLayout(l);
923
+ setElapsedMs(performance.now() - t0);
924
+ },
925
+ [book, epub, chapterIndex, surface]
926
+ );
1015
927
  useLayoutEffect2(() => {
1016
928
  requestIdRef.current++;
929
+ pendingRestore.current = null;
1017
930
  setLayout(null);
1018
931
  setPageWidth(0);
1019
932
  setPageHeight(0);
@@ -1028,29 +941,55 @@ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
1028
941
  }, [recompute]);
1029
942
  useEffect2(() => {
1030
943
  if (!enableResize) return;
944
+ const el = surface.current;
945
+ if (!el) return;
1031
946
  let timer = null;
1032
- const onResize = () => {
1033
- if (!(surface.current && layoutRef.current)) return;
1034
- if (timer) clearTimeout(timer);
947
+ let observed = false;
948
+ const clear = () => {
949
+ if (timer) {
950
+ clearTimeout(timer);
951
+ timer = null;
952
+ }
953
+ };
954
+ const scheduleReflow = (immediate) => {
955
+ clear();
956
+ if (immediate) {
957
+ void recompute({ blank: false });
958
+ return;
959
+ }
1035
960
  timer = setTimeout(() => {
1036
- if (!(surface.current && layoutRef.current)) return;
1037
- const dims = book.computePageSize(surface.current);
1038
- setPageWidth(dims.pageWidth);
1039
- setPageHeight(dims.pageHeight);
1040
- setContentHeight(dims.contentHeight);
1041
- layoutRef.current.resize({
1042
- pageWidth: dims.pageWidth,
1043
- lineWidth: dims.contentHeight - book.getOptions().fontSize * 0.5
1044
- });
961
+ timer = null;
962
+ void recompute({ blank: false });
1045
963
  }, resizeDebounce);
1046
964
  };
1047
- window.addEventListener("resize", onResize);
965
+ if (typeof ResizeObserver !== "undefined") {
966
+ const observer = new ResizeObserver(() => {
967
+ const immediate = !observed;
968
+ observed = true;
969
+ scheduleReflow(immediate);
970
+ });
971
+ observer.observe(el);
972
+ return () => {
973
+ observer.disconnect();
974
+ clear();
975
+ };
976
+ }
977
+ const onWindowResize = () => scheduleReflow(false);
978
+ window.addEventListener("resize", onWindowResize);
1048
979
  return () => {
1049
- window.removeEventListener("resize", onResize);
1050
- if (timer) clearTimeout(timer);
980
+ window.removeEventListener("resize", onWindowResize);
981
+ clear();
1051
982
  };
1052
- }, [book, surface, enableResize, resizeDebounce]);
1053
- return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
983
+ }, [surface, enableResize, resizeDebounce, recompute]);
984
+ return {
985
+ layout,
986
+ pageWidth,
987
+ pageHeight,
988
+ contentHeight,
989
+ elapsedMs,
990
+ recompute,
991
+ pendingRestore
992
+ };
1054
993
  }
1055
994
 
1056
995
  // src/useEpub.ts
@@ -1407,6 +1346,7 @@ function useSpread(layout, options = {}) {
1407
1346
  spreadIdxRef.current = spreadIdx;
1408
1347
  const turnTimerRef = useRef8(null);
1409
1348
  const layoutGenerationRef = useRef8(0);
1349
+ const lastEmittedRef = useRef8(null);
1410
1350
  const totalPages = layout?.totalPages ?? 0;
1411
1351
  const totalSpreads = Math.max(1, Math.ceil(totalPages / 2));
1412
1352
  const refresh = useCallback5(() => {
@@ -1433,7 +1373,15 @@ function useSpread(layout, options = {}) {
1433
1373
  useEffect5(() => {
1434
1374
  if (!layout) return;
1435
1375
  setSpread(layout.getSpread(spreadIdx));
1436
- onChangeRef.current?.(spreadIdx);
1376
+ const last = lastEmittedRef.current;
1377
+ if (!last) {
1378
+ lastEmittedRef.current = { layout, spreadIdx };
1379
+ return;
1380
+ }
1381
+ if (last.spreadIdx !== spreadIdx) {
1382
+ onChangeRef.current?.(spreadIdx);
1383
+ }
1384
+ lastEmittedRef.current = { layout, spreadIdx };
1437
1385
  }, [layout, spreadIdx]);
1438
1386
  const goTo = useCallback5(
1439
1387
  (index) => {
@@ -1457,6 +1405,16 @@ function useSpread(layout, options = {}) {
1457
1405
  },
1458
1406
  [turnDuration]
1459
1407
  );
1408
+ const setSpreadIndex = useCallback5((index) => {
1409
+ if (!layoutRef.current) return;
1410
+ if (turnTimerRef.current) {
1411
+ clearTimeout(turnTimerRef.current);
1412
+ turnTimerRef.current = null;
1413
+ }
1414
+ setTurning(false);
1415
+ const max = Math.max(1, Math.ceil(layoutRef.current.totalPages / 2)) - 1;
1416
+ setSpreadIdx(Math.max(0, Math.min(max, index)));
1417
+ }, []);
1460
1418
  const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
1461
1419
  const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
1462
1420
  useEffect5(() => {
@@ -1475,16 +1433,28 @@ function useSpread(layout, options = {}) {
1475
1433
  },
1476
1434
  []
1477
1435
  );
1478
- return { spreadIdx, spread, totalPages, totalSpreads, turning, next, prev, goTo, refresh };
1436
+ return {
1437
+ spreadIdx,
1438
+ spread,
1439
+ totalPages,
1440
+ totalSpreads,
1441
+ turning,
1442
+ next,
1443
+ prev,
1444
+ goTo,
1445
+ setSpread: setSpreadIndex,
1446
+ refresh
1447
+ };
1479
1448
  }
1480
1449
 
1481
1450
  // src/MejiroReader.tsx
1482
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1451
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
1483
1452
  function MejiroReaderInner(props, ref) {
1484
1453
  const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
1485
1454
  const dialectProp = "dialect" in props ? props.dialect : void 0;
1486
1455
  const {
1487
- options = DEFAULT_BOOK_OPTIONS,
1456
+ options: optionsProp,
1457
+ pageGeometry: pageGeometryProp,
1488
1458
  fonts,
1489
1459
  epub: epubProp,
1490
1460
  epubUrl,
@@ -1493,6 +1463,7 @@ function MejiroReaderInner(props, ref) {
1493
1463
  theme = "light",
1494
1464
  mode = "paginated",
1495
1465
  spreadMode = "double",
1466
+ fit = "fill",
1496
1467
  enableSurfaceTap = true,
1497
1468
  fallback,
1498
1469
  fetchOptions,
@@ -1508,10 +1479,12 @@ function MejiroReaderInner(props, ref) {
1508
1479
  enableChapterNav = !bare,
1509
1480
  chapterNavMode = "select",
1510
1481
  enableSettings = !bare,
1482
+ renderSettings,
1511
1483
  enableImageOverlay = false,
1512
1484
  enableStats = !bare,
1513
1485
  enableKeyboard = true,
1514
1486
  enablePageIndicator = !bare,
1487
+ pageNumbers = "both",
1515
1488
  annotations,
1516
1489
  onLoad,
1517
1490
  onError,
@@ -1525,6 +1498,7 @@ function MejiroReaderInner(props, ref) {
1525
1498
  const fileRef = useRef9(null);
1526
1499
  const [settingsOpen, setSettingsOpen] = useState7(false);
1527
1500
  const [chapterState, setChapterState] = useState7(chapterProp ?? 0);
1501
+ const chapterIsUncontrolled = chapterProp == null;
1528
1502
  const chapter = chapterProp ?? chapterState;
1529
1503
  const [chromeHidden, setChromeHidden] = useState7(false);
1530
1504
  const [autoSingle, setAutoSingle] = useState7(false);
@@ -1551,7 +1525,37 @@ function MejiroReaderInner(props, ref) {
1551
1525
  return () => observer.disconnect();
1552
1526
  }, [spreadMode]);
1553
1527
  const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
1554
- const { book, options: bookOptions, setOptions } = useMejiroBook(options);
1528
+ const resolvedGeometry = useMemo4(() => {
1529
+ 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
+ };
1540
+ }, [fit, pageGeometryProp, effectiveSingle]);
1541
+ const surfaceAspect = useMemo4(() => {
1542
+ const columns = effectiveSingle ? 1 : 2;
1543
+ const aspect = resolvedGeometry?.aspect ?? DEFAULT_PAGE_GEOMETRY.aspect;
1544
+ return `${columns} / ${aspect}`;
1545
+ }, [effectiveSingle, resolvedGeometry]);
1546
+ const resolvedOptions = useMemo4(
1547
+ () => ({ ...DEFAULT_BOOK_OPTIONS, ...optionsProp ?? {} }),
1548
+ [optionsProp]
1549
+ );
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]);
1555
1559
  const synthesizedEpub = useMemo4(() => {
1556
1560
  if (manuscriptProp === void 0) return null;
1557
1561
  return manuscriptToEpubBook(manuscriptProp, { dialect: dialectProp });
@@ -1575,7 +1579,18 @@ function MejiroReaderInner(props, ref) {
1575
1579
  useEffect6(() => {
1576
1580
  if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
1577
1581
  }, [epubCtx.error]);
1578
- const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef);
1582
+ const pageGeometryRef = useRef9(resolvedGeometry);
1583
+ pageGeometryRef.current = resolvedGeometry;
1584
+ const curSpreadIdxRef = useRef9(0);
1585
+ const spreadIdxPropRef = useRef9(spreadIdxProp);
1586
+ spreadIdxPropRef.current = spreadIdxProp;
1587
+ const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef, {
1588
+ pageGeometry: () => pageGeometryRef.current,
1589
+ // Preserve the reading position across a reflow re-layout (size / option
1590
+ // changes), but only in uncontrolled mode — when `spreadIdx` is controlled
1591
+ // the host owns the position and the controlled-restore effect handles it.
1592
+ capturePosition: (layout2) => spreadIdxPropRef.current != null ? null : layout2.anchorAt(curSpreadIdxRef.current, "right")
1593
+ });
1579
1594
  const spreadChangedRef = useRef9(void 0);
1580
1595
  spreadChangedRef.current = (i) => {
1581
1596
  onSpreadChange?.(i);
@@ -1585,6 +1600,46 @@ function MejiroReaderInner(props, ref) {
1585
1600
  enableKeyboard,
1586
1601
  onChange: (i) => spreadChangedRef.current?.(i)
1587
1602
  });
1603
+ curSpreadIdxRef.current = spreadCtx.spreadIdx;
1604
+ const setSpreadRef = useRef9(spreadCtx.setSpread);
1605
+ setSpreadRef.current = spreadCtx.setSpread;
1606
+ useLayoutEffect4(() => {
1607
+ const anchor = layoutCtx.pendingRestore.current;
1608
+ if (!(anchor && layoutCtx.layout)) return;
1609
+ layoutCtx.pendingRestore.current = null;
1610
+ const loc = layoutCtx.layout.locateAnchor(anchor);
1611
+ setSpreadRef.current(loc?.spreadIdx ?? 0);
1612
+ }, [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);
1622
+ bookOptionsRef.current = bookOptions;
1623
+ const recomputeRef = useRef9(layoutCtx.recompute);
1624
+ recomputeRef.current = layoutCtx.recompute;
1625
+ useEffect6(() => {
1626
+ if (optionsKeyRef.current === optionsKey) return;
1627
+ optionsKeyRef.current = optionsKey;
1628
+ const timer = setTimeout(() => {
1629
+ void (async () => {
1630
+ await setOptions({ ...bookOptionsRef.current });
1631
+ await recomputeRef.current({ blank: false });
1632
+ })();
1633
+ }, 60);
1634
+ return () => clearTimeout(timer);
1635
+ }, [optionsKey, setOptions]);
1636
+ const geometryKey = JSON.stringify(resolvedGeometry ?? null);
1637
+ const geometryKeyRef = useRef9(geometryKey);
1638
+ useEffect6(() => {
1639
+ if (geometryKeyRef.current === geometryKey) return;
1640
+ geometryKeyRef.current = geometryKey;
1641
+ void recomputeRef.current({ blank: false });
1642
+ }, [geometryKey]);
1588
1643
  const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.spreadIdx, {
1589
1644
  onUpdate: () => spreadCtx.refresh()
1590
1645
  });
@@ -1607,18 +1662,22 @@ function MejiroReaderInner(props, ref) {
1607
1662
  onLoadRef.current = onLoad;
1608
1663
  useEffect6(() => {
1609
1664
  if (!controlled) return;
1610
- if (chapterProp == null) setChapterState(0);
1665
+ if (chapterIsUncontrolled) setChapterState(0);
1611
1666
  clearImagesRef.current();
1612
1667
  book.clearCache();
1613
- if (epubProp) onLoadRef.current?.(epubProp);
1614
- }, [controlled, epubProp, book, chapterProp]);
1668
+ if (e) onLoadRef.current?.(e);
1669
+ }, [controlled, e, book, chapterIsUncontrolled]);
1615
1670
  const spreadGoToRef = useRef9(spreadCtx.goTo);
1616
1671
  spreadGoToRef.current = spreadCtx.goTo;
1617
1672
  useEffect6(() => {
1618
1673
  if (spreadIdxProp == null) return;
1619
- if (spreadIdxProp === spreadCtx.spreadIdx) return;
1620
1674
  spreadGoToRef.current(spreadIdxProp);
1621
- }, [spreadIdxProp, spreadCtx.spreadIdx]);
1675
+ }, [spreadIdxProp]);
1676
+ useLayoutEffect4(() => {
1677
+ const next = spreadIdxPropRef.current;
1678
+ if (next == null || !layoutCtx.layout) return;
1679
+ setSpreadRef.current(next);
1680
+ }, [layoutCtx.layout]);
1622
1681
  const onChapter = useCallback6(
1623
1682
  (i) => {
1624
1683
  if (i === chapter) return;
@@ -1788,20 +1847,30 @@ function MejiroReaderInner(props, ref) {
1788
1847
  const layoutReady = e && spreadCtx.spread && layoutCtx.layout && layoutCtx.pageWidth > 0;
1789
1848
  const themeName = typeof theme === "string" ? theme : theme.name;
1790
1849
  const themeOverride = typeof theme === "string" ? void 0 : theme.override;
1791
- const themeStyle = themeOverride;
1850
+ const themeStyle = useMemo4(() => {
1851
+ const pad = pageGeometryProp?.padding;
1852
+ const styleVars = {};
1853
+ if (pad?.x != null) styleVars["--mejiro-page-pad-x"] = `${pad.x}px`;
1854
+ if (pad?.y != null) styleVars["--mejiro-page-pad-y"] = `${pad.y}px`;
1855
+ if (pad?.bottom != null) styleVars["--mejiro-page-pad-bottom"] = `${pad.bottom}px`;
1856
+ if (fit === "width") styleVars["--mejiro-surface-aspect"] = surfaceAspect;
1857
+ const hasVars = Object.keys(styleVars).length > 0;
1858
+ if (!(themeOverride || hasVars)) return void 0;
1859
+ return { ...themeOverride, ...styleVars };
1860
+ }, [themeOverride, pageGeometryProp, fit, surfaceAspect]);
1792
1861
  const resolvedMessages = useI18n({ locale, messages });
1793
1862
  const effectiveSubtitle = subtitle ?? resolvedMessages.logoSubtitle;
1794
- const defaultLogo = /* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-logo", children: [
1795
- /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-logo-mark", children: title }),
1796
- effectiveSubtitle && /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-logo-sub", children: effectiveSubtitle })
1863
+ const defaultLogo = /* @__PURE__ */ jsxs8("div", { className: "mejiro-reader-logo", children: [
1864
+ /* @__PURE__ */ jsx13("span", { className: "mejiro-reader-logo-mark", children: title }),
1865
+ effectiveSubtitle && /* @__PURE__ */ jsx13("span", { className: "mejiro-reader-logo-sub", children: effectiveSubtitle })
1797
1866
  ] });
1798
- const header = enableHeader ? /* @__PURE__ */ jsxs10("header", { className: "mejiro-reader-header", children: [
1799
- /* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-header-left", children: [
1867
+ const header = enableHeader ? /* @__PURE__ */ jsxs8("header", { className: "mejiro-reader-header", children: [
1868
+ /* @__PURE__ */ jsxs8("div", { className: "mejiro-reader-header-left", children: [
1800
1869
  logo === void 0 ? defaultLogo : logo,
1801
- showChapterSelect && /* @__PURE__ */ jsx12(MejiroChapterNav, { epub: e, chapter, onChange: onChapter })
1870
+ showChapterSelect && /* @__PURE__ */ jsx13(MejiroChapterNav, { epub: e, chapter, onChange: onChapter })
1802
1871
  ] }),
1803
- /* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-header-actions", children: [
1804
- enableStats && /* @__PURE__ */ jsx12(
1872
+ /* @__PURE__ */ jsxs8("div", { className: "mejiro-reader-header-actions", children: [
1873
+ enableStats && /* @__PURE__ */ jsx13(
1805
1874
  MejiroStats,
1806
1875
  {
1807
1876
  chapter: e?.chapters[chapter] ?? null,
@@ -1810,7 +1879,7 @@ function MejiroReaderInner(props, ref) {
1810
1879
  fontLabel
1811
1880
  }
1812
1881
  ),
1813
- enableDropZone && /* @__PURE__ */ jsx12(
1882
+ enableDropZone && /* @__PURE__ */ jsx13(
1814
1883
  "button",
1815
1884
  {
1816
1885
  type: "button",
@@ -1819,7 +1888,7 @@ function MejiroReaderInner(props, ref) {
1819
1888
  children: resolvedMessages.openButton
1820
1889
  }
1821
1890
  ),
1822
- enableImageOverlay && e && /* @__PURE__ */ jsx12(
1891
+ enableImageOverlay && e && /* @__PURE__ */ jsx13(
1823
1892
  "button",
1824
1893
  {
1825
1894
  type: "button",
@@ -1828,7 +1897,7 @@ function MejiroReaderInner(props, ref) {
1828
1897
  children: resolvedMessages.imageButton
1829
1898
  }
1830
1899
  ),
1831
- enableSettings && /* @__PURE__ */ jsxs10(
1900
+ enableSettings && /* @__PURE__ */ jsxs8(
1832
1901
  "button",
1833
1902
  {
1834
1903
  type: "button",
@@ -1836,7 +1905,7 @@ function MejiroReaderInner(props, ref) {
1836
1905
  onClick: () => setSettingsOpen((v) => !v),
1837
1906
  children: [
1838
1907
  resolvedMessages.settingsButton,
1839
- /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-btn-arrow", children: "\u25BE" })
1908
+ /* @__PURE__ */ jsx13("span", { className: "mejiro-reader-btn-arrow", children: "\u25BE" })
1840
1909
  ]
1841
1910
  }
1842
1911
  )
@@ -1846,15 +1915,22 @@ function MejiroReaderInner(props, ref) {
1846
1915
  const rightPage = currentSpread * 2 + 1;
1847
1916
  const leftPage = currentSpread * 2 + 2;
1848
1917
  const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
1849
- return /* @__PURE__ */ jsx12(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs10(
1918
+ const showRightNum = pageNumbers === "both" || pageNumbers === "right";
1919
+ const showLeftNum = pageNumbers === "both" || pageNumbers === "left";
1920
+ return /* @__PURE__ */ jsx13(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs8(
1850
1921
  "div",
1851
1922
  {
1852
- className: `mejiro-reader${chromeHidden ? " mejiro-reader--chrome-hidden" : ""}`,
1923
+ className: `mejiro-reader${chromeHidden ? " mejiro-reader--chrome-hidden" : ""}${fit === "width" ? " mejiro-reader--fit-width" : ""}`,
1853
1924
  "data-mejiro-theme": themeName,
1854
1925
  style: themeStyle,
1855
1926
  children: [
1856
1927
  header,
1857
- enableSettings && /* @__PURE__ */ jsx12(
1928
+ enableSettings && (renderSettings ? /* @__PURE__ */ jsx13("div", { className: `mejiro-reader-settings-panel${settingsOpen ? " is-open" : ""}`, children: /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-settings-inner", children: /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-settings-content", children: renderSettings({
1929
+ settings: editable,
1930
+ update: setOptions,
1931
+ open: settingsOpen,
1932
+ toggle: () => setSettingsOpen((v) => !v)
1933
+ }) }) }) }) : /* @__PURE__ */ jsx13(
1858
1934
  MejiroSettingsPanel,
1859
1935
  {
1860
1936
  open: settingsOpen,
@@ -1862,14 +1938,14 @@ function MejiroReaderInner(props, ref) {
1862
1938
  fonts,
1863
1939
  onChange: setOptions
1864
1940
  }
1865
- ),
1866
- /* @__PURE__ */ jsxs10("div", { className: `mejiro-reader-body${showChapterPanel ? " has-chapter-panel" : ""}`, children: [
1867
- showChapterPanel && /* @__PURE__ */ jsx12(MejiroChapterNav, { epub: e, chapter, onChange: onChapter, variant: "panel" }),
1868
- /* @__PURE__ */ jsxs10("div", { ref: surfaceRef, className: "mejiro-reader-surface", children: [
1869
- !(e || epubCtx.loading) && enableDropZone && /* @__PURE__ */ jsx12(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
1870
- epubCtx.loading && /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-loading", children: resolvedMessages.loading }),
1871
- !layoutReady && fallback && /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-fallback", children: fallback }),
1872
- layoutReady && mode === "scroll" && layoutCtx.layout && /* @__PURE__ */ jsx12(
1941
+ )),
1942
+ /* @__PURE__ */ jsxs8("div", { className: `mejiro-reader-body${showChapterPanel ? " has-chapter-panel" : ""}`, children: [
1943
+ showChapterPanel && /* @__PURE__ */ jsx13(MejiroChapterNav, { epub: e, chapter, onChange: onChapter, variant: "panel" }),
1944
+ /* @__PURE__ */ jsxs8("div", { ref: surfaceRef, className: "mejiro-reader-surface", children: [
1945
+ !(e || epubCtx.loading) && enableDropZone && /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
1946
+ epubCtx.loading && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-loading", children: resolvedMessages.loading }),
1947
+ !layoutReady && fallback && /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-fallback", children: fallback }),
1948
+ layoutReady && mode === "scroll" && layoutCtx.layout && /* @__PURE__ */ jsx13(
1873
1949
  MejiroScrollView,
1874
1950
  {
1875
1951
  layout: layoutCtx.layout,
@@ -1880,13 +1956,14 @@ function MejiroReaderInner(props, ref) {
1880
1956
  fontSize: bookOptions.fontSize,
1881
1957
  lineSpacing: bookOptions.lineSpacing,
1882
1958
  scrollToPage: spreadCtx.spreadIdx * 2,
1883
- onVisiblePageChange: (pageIdx) => {
1959
+ onVisiblePageChange: (pageIdx, source) => {
1960
+ if (source === "programmatic") return;
1884
1961
  const target = Math.floor(pageIdx / 2);
1885
- if (target !== spreadCtx.spreadIdx) spreadGoToRef.current(target);
1962
+ if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
1886
1963
  }
1887
1964
  }
1888
1965
  ),
1889
- layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx12(
1966
+ layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
1890
1967
  MejiroSpread,
1891
1968
  {
1892
1969
  singlePage: effectiveSingle,
@@ -1900,13 +1977,16 @@ function MejiroReaderInner(props, ref) {
1900
1977
  fontSize: bookOptions.fontSize,
1901
1978
  lineSpacing: bookOptions.lineSpacing,
1902
1979
  turning: spreadCtx.turning,
1903
- rightHeader: { title: runningTitleRight, pageNumber: rightPage },
1980
+ rightHeader: {
1981
+ title: runningTitleRight,
1982
+ pageNumber: showRightNum ? rightPage : null
1983
+ },
1904
1984
  leftHeader: {
1905
1985
  title: runningTitleLeft,
1906
- pageNumber: showLeft ? leftPage : null
1986
+ pageNumber: showLeft && showLeftNum ? leftPage : null
1907
1987
  },
1908
1988
  images: imageCtx.currentImages,
1909
- indicator: enablePageIndicator ? /* @__PURE__ */ jsx12(
1989
+ indicator: enablePageIndicator ? /* @__PURE__ */ jsx13(
1910
1990
  MejiroPageIndicator,
1911
1991
  {
1912
1992
  current: spreadCtx.spreadIdx + 1,
@@ -1924,7 +2004,7 @@ function MejiroReaderInner(props, ref) {
1924
2004
  )
1925
2005
  ] })
1926
2006
  ] }),
1927
- /* @__PURE__ */ jsx12(
2007
+ /* @__PURE__ */ jsx13(
1928
2008
  "input",
1929
2009
  {
1930
2010
  ref: fileRef,
@@ -1937,7 +2017,7 @@ function MejiroReaderInner(props, ref) {
1937
2017
  }
1938
2018
  }
1939
2019
  ),
1940
- /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-sr-only", role: "status", "aria-live": "polite", children: spreadCtx.totalSpreads > 0 ? format(resolvedMessages.spreadAnnouncement, {
2020
+ /* @__PURE__ */ jsx13("div", { className: "mejiro-reader-sr-only", role: "status", "aria-live": "polite", children: spreadCtx.totalSpreads > 0 ? format(resolvedMessages.spreadAnnouncement, {
1941
2021
  spread: spreadCtx.spreadIdx + 1,
1942
2022
  total: spreadCtx.totalSpreads
1943
2023
  }) : "" })
@@ -1949,7 +2029,7 @@ var MejiroReader = forwardRef(MejiroReaderInner);
1949
2029
  MejiroReader.displayName = "MejiroReader";
1950
2030
 
1951
2031
  // src/MejiroEditor.tsx
1952
- import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2032
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1953
2033
  function cloneBook(book) {
1954
2034
  return {
1955
2035
  ...book,
@@ -2025,8 +2105,11 @@ function MejiroEditor({
2025
2105
  const book = editor?.book ?? null;
2026
2106
  const chapter = book?.chapters[selection.chapter] ?? null;
2027
2107
  const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
2028
- const previewBook = book ? cloneBook(book) : null;
2029
- void revision;
2108
+ const textDirty = paragraph ? text !== paragraph.text : false;
2109
+ const previewBook = useMemo5(() => {
2110
+ void revision;
2111
+ return book ? cloneBook(book) : null;
2112
+ }, [book, revision]);
2030
2113
  useEffect7(() => {
2031
2114
  onLoadRef.current = onLoad;
2032
2115
  }, [onLoad]);
@@ -2120,7 +2203,7 @@ function MejiroEditor({
2120
2203
  setRevision((value) => value + 1);
2121
2204
  }
2122
2205
  function applyRuby() {
2123
- if (!(editor && paragraph && rubyText.trim())) return;
2206
+ if (!(editor && paragraph && rubyText.trim()) || textDirty) return;
2124
2207
  const len = [...text].length;
2125
2208
  const start = Math.max(0, Math.min(rubyStart, len));
2126
2209
  const end = Math.max(start + 1, Math.min(rubyEnd, len));
@@ -2171,9 +2254,9 @@ function MejiroEditor({
2171
2254
  a.click();
2172
2255
  URL.revokeObjectURL(url);
2173
2256
  }
2174
- return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor", children: [
2175
- /* @__PURE__ */ jsxs11("main", { className: "mejiro-editor-preview", children: [
2176
- previewBook ? /* @__PURE__ */ jsx13(
2257
+ return /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor", children: [
2258
+ /* @__PURE__ */ jsxs9("main", { className: "mejiro-editor-preview", children: [
2259
+ previewBook ? /* @__PURE__ */ jsx14(
2177
2260
  MejiroReader,
2178
2261
  {
2179
2262
  epub: previewBook,
@@ -2183,38 +2266,38 @@ function MejiroEditor({
2183
2266
  enableImageOverlay: false,
2184
2267
  enableSurfaceTap: false
2185
2268
  }
2186
- ) : /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (file) => void loadFile(file) }),
2187
- loading && /* @__PURE__ */ jsx13("div", { className: "mejiro-editor-loading", children: messages.loading }),
2188
- error && /* @__PURE__ */ jsx13("div", { className: "mejiro-editor-error", children: error.message })
2269
+ ) : /* @__PURE__ */ jsx14(MejiroDropZone, { onFile: (file) => void loadFile(file) }),
2270
+ loading && /* @__PURE__ */ jsx14("div", { className: "mejiro-editor-loading", children: messages.loading }),
2271
+ error && /* @__PURE__ */ jsx14("div", { className: "mejiro-editor-error", children: error.message })
2189
2272
  ] }),
2190
- /* @__PURE__ */ jsxs11("aside", { className: "mejiro-editor-panel", children: [
2191
- /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-head", children: [
2192
- /* @__PURE__ */ jsx13("span", { children: messages.editorTitle }),
2193
- /* @__PURE__ */ jsx13("strong", { children: editor?.title ?? messages.editorNoBookLoaded }),
2194
- editor?.author && /* @__PURE__ */ jsx13("small", { children: editor.author })
2273
+ /* @__PURE__ */ jsxs9("aside", { className: "mejiro-editor-panel", children: [
2274
+ /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-head", children: [
2275
+ /* @__PURE__ */ jsx14("span", { children: messages.editorTitle }),
2276
+ /* @__PURE__ */ jsx14("strong", { children: editor?.title ?? messages.editorNoBookLoaded }),
2277
+ editor?.author && /* @__PURE__ */ jsx14("small", { children: editor.author })
2195
2278
  ] }),
2196
- book && /* @__PURE__ */ jsxs11(Fragment4, { children: [
2197
- /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2198
- /* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorParagraphs }),
2199
- /* @__PURE__ */ jsx13("div", { className: "mejiro-editor-paragraphs", children: book.chapters.map(
2200
- (ch, ci) => ch.blocks.filter((b) => b.kind === "paragraph").map((block, pi) => /* @__PURE__ */ jsxs11(
2279
+ book && /* @__PURE__ */ jsxs9(Fragment3, { children: [
2280
+ /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
2281
+ /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorParagraphs }),
2282
+ /* @__PURE__ */ jsx14("div", { className: "mejiro-editor-paragraphs", children: book.chapters.map(
2283
+ (ch, ci) => ch.blocks.filter((b) => b.kind === "paragraph").map((block, pi) => /* @__PURE__ */ jsxs9(
2201
2284
  "button",
2202
2285
  {
2203
2286
  type: "button",
2204
2287
  className: selection.chapter === ci && selection.paragraph === pi ? "is-active" : "",
2205
2288
  onClick: () => selectParagraph(ci, pi),
2206
2289
  children: [
2207
- /* @__PURE__ */ jsx13("span", { children: ch.title ?? format(messages.chapterN, { n: ci + 1 }) }),
2208
- /* @__PURE__ */ jsx13("strong", { children: block.text.slice(0, 42) })
2290
+ /* @__PURE__ */ jsx14("span", { children: ch.title ?? format(messages.chapterN, { n: ci + 1 }) }),
2291
+ /* @__PURE__ */ jsx14("strong", { children: block.text.slice(0, 42) })
2209
2292
  ]
2210
2293
  },
2211
2294
  `${ch.href}-${block.id}`
2212
2295
  ))
2213
2296
  ) })
2214
2297
  ] }),
2215
- enableProofread && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2216
- /* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorProofread }),
2217
- /* @__PURE__ */ jsx13(
2298
+ enableProofread && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
2299
+ /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorProofread }),
2300
+ /* @__PURE__ */ jsx14(
2218
2301
  "textarea",
2219
2302
  {
2220
2303
  ref: textareaRef,
@@ -2223,17 +2306,17 @@ function MejiroEditor({
2223
2306
  onSelect: captureRubyRange
2224
2307
  }
2225
2308
  ),
2226
- /* @__PURE__ */ jsx13("button", { type: "button", className: "mejiro-editor-primary", onClick: applyText, children: messages.editorApplyText })
2309
+ /* @__PURE__ */ jsx14("button", { type: "button", className: "mejiro-editor-primary", onClick: applyText, children: messages.editorApplyText })
2227
2310
  ] }),
2228
- enableRuby && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2229
- /* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
2230
- /* @__PURE__ */ jsx13("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
2231
- /* @__PURE__ */ jsx13("p", { className: "mejiro-editor-range", children: format(messages.editorRubyRange, {
2311
+ enableRuby && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
2312
+ /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
2313
+ /* @__PURE__ */ jsx14("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
2314
+ /* @__PURE__ */ jsx14("p", { className: "mejiro-editor-range", children: format(messages.editorRubyRange, {
2232
2315
  start: rubyStart,
2233
2316
  end: rubyEnd,
2234
2317
  count: Math.max(0, rubyEnd - rubyStart)
2235
2318
  }) }),
2236
- /* @__PURE__ */ jsx13(
2319
+ /* @__PURE__ */ jsx14(
2237
2320
  "input",
2238
2321
  {
2239
2322
  value: rubyText,
@@ -2241,12 +2324,21 @@ function MejiroEditor({
2241
2324
  onChange: (event) => setRubyText(event.target.value)
2242
2325
  }
2243
2326
  ),
2244
- /* @__PURE__ */ jsx13("button", { type: "button", className: "mejiro-editor-primary", onClick: applyRuby, children: messages.editorApplyRuby })
2327
+ /* @__PURE__ */ jsx14(
2328
+ "button",
2329
+ {
2330
+ type: "button",
2331
+ className: "mejiro-editor-primary",
2332
+ disabled: textDirty || !rubyText.trim(),
2333
+ onClick: applyRuby,
2334
+ children: messages.editorApplyRuby
2335
+ }
2336
+ )
2245
2337
  ] }),
2246
- enableImages && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2247
- /* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorImages }),
2248
- /* @__PURE__ */ jsx13("button", { type: "button", onClick: () => imageInputRef.current?.click(), children: messages.editorInsertImageAfterParagraph }),
2249
- /* @__PURE__ */ jsx13(
2338
+ enableImages && /* @__PURE__ */ jsxs9("div", { className: "mejiro-editor-section", children: [
2339
+ /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.editorImages }),
2340
+ /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => imageInputRef.current?.click(), children: messages.editorInsertImageAfterParagraph }),
2341
+ /* @__PURE__ */ jsx14(
2250
2342
  "input",
2251
2343
  {
2252
2344
  ref: imageInputRef,
@@ -2260,7 +2352,7 @@ function MejiroEditor({
2260
2352
  }
2261
2353
  )
2262
2354
  ] }),
2263
- enableExport && /* @__PURE__ */ jsx13(
2355
+ enableExport && /* @__PURE__ */ jsx14(
2264
2356
  "button",
2265
2357
  {
2266
2358
  type: "button",
@@ -2285,35 +2377,189 @@ function paragraphBlockId(chapter, paragraphIndex) {
2285
2377
 
2286
2378
  // src/MejiroManuscriptEditor.tsx
2287
2379
  import { EpubProject } from "@libraz/mejiro/epub";
2288
- import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef12, useState as useState10 } from "react";
2380
+ import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef13, useState as useState11 } from "react";
2381
+
2382
+ // src/MejiroNotationHighlighter.tsx
2383
+ import { tokenizeManuscriptSource } from "@libraz/mejiro";
2384
+ import {
2385
+ forwardRef as forwardRef2,
2386
+ useImperativeHandle as useImperativeHandle2,
2387
+ useMemo as useMemo6,
2388
+ useRef as useRef11,
2389
+ useState as useState9
2390
+ } from "react";
2391
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
2392
+ var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2({
2393
+ value,
2394
+ onChange,
2395
+ dialect = "mejiro",
2396
+ wrapperClassName,
2397
+ wrapperStyle,
2398
+ className,
2399
+ style,
2400
+ onScroll,
2401
+ onCompositionStart,
2402
+ onCompositionEnd,
2403
+ ...rest
2404
+ }, ref) {
2405
+ const textareaRef = useRef11(null);
2406
+ const overlayRef = useRef11(null);
2407
+ useImperativeHandle2(ref, () => textareaRef.current, []);
2408
+ const [isComposing, setIsComposing] = useState9(false);
2409
+ const lastSegmentsRef = useRef11(null);
2410
+ const segments = useMemo6(() => {
2411
+ if (isComposing && lastSegmentsRef.current) return lastSegmentsRef.current;
2412
+ const next = buildSegments(value, dialect);
2413
+ lastSegmentsRef.current = next;
2414
+ return next;
2415
+ }, [value, dialect, isComposing]);
2416
+ return /* @__PURE__ */ jsxs10(
2417
+ "div",
2418
+ {
2419
+ className: `mejiro-notation-highlighter${wrapperClassName ? ` ${wrapperClassName}` : ""}`,
2420
+ style: wrapperStyle,
2421
+ children: [
2422
+ /* @__PURE__ */ jsxs10(
2423
+ "div",
2424
+ {
2425
+ ref: overlayRef,
2426
+ className: "mejiro-notation-overlay",
2427
+ "aria-hidden": "true",
2428
+ children: [
2429
+ segments.map((segment) => /* @__PURE__ */ jsx15(
2430
+ "span",
2431
+ {
2432
+ className: segment.kind ? `mejiro-notation-token mejiro-notation-${segment.kind}` : void 0,
2433
+ "data-token": segment.kind,
2434
+ children: segment.text
2435
+ },
2436
+ segment.key
2437
+ )),
2438
+ "\u200B"
2439
+ ]
2440
+ }
2441
+ ),
2442
+ /* @__PURE__ */ jsx15(
2443
+ "textarea",
2444
+ {
2445
+ ...rest,
2446
+ ref: textareaRef,
2447
+ className: `mejiro-notation-textarea${className ? ` ${className}` : ""}`,
2448
+ style,
2449
+ value,
2450
+ onChange: (event) => onChange(event.target.value),
2451
+ onCompositionStart: (event) => {
2452
+ setIsComposing(true);
2453
+ onCompositionStart?.(event);
2454
+ },
2455
+ onCompositionEnd: (event) => {
2456
+ setIsComposing(false);
2457
+ onCompositionEnd?.(event);
2458
+ },
2459
+ onScroll: (event) => {
2460
+ if (overlayRef.current) {
2461
+ overlayRef.current.scrollTop = event.currentTarget.scrollTop;
2462
+ overlayRef.current.scrollLeft = event.currentTarget.scrollLeft;
2463
+ }
2464
+ onScroll?.(event);
2465
+ },
2466
+ spellCheck: false
2467
+ }
2468
+ )
2469
+ ]
2470
+ }
2471
+ );
2472
+ });
2473
+ function buildSegments(text, dialect) {
2474
+ const tokens = tokenizeManuscriptSource(text, dialect);
2475
+ const segments = [];
2476
+ let cursor = 0;
2477
+ for (const [i, token] of tokens.entries()) {
2478
+ if (token.start > cursor) {
2479
+ segments.push({ key: `t-${i}-pre`, text: text.slice(cursor, token.start) });
2480
+ }
2481
+ segments.push({
2482
+ key: `t-${i}`,
2483
+ text: text.slice(token.start, token.end),
2484
+ kind: token.kind
2485
+ });
2486
+ cursor = token.end;
2487
+ }
2488
+ if (cursor < text.length) {
2489
+ segments.push({ key: "tail", text: text.slice(cursor) });
2490
+ }
2491
+ if (segments.length === 0) {
2492
+ segments.push({ key: "empty", text });
2493
+ }
2494
+ return segments;
2495
+ }
2289
2496
 
2290
2497
  // src/useManuscriptDraft.ts
2291
- import { useCallback as useCallback8, useEffect as useEffect8, useRef as useRef11, useState as useState9 } from "react";
2498
+ import { useCallback as useCallback8, useEffect as useEffect8, useRef as useRef12, useState as useState10 } from "react";
2292
2499
  var DEFAULT_DELAY = 800;
2293
- function defaultChapter(index) {
2294
- return { id: `chapter-${Date.now()}-${index}`, title: `\u7B2C${index + 1}\u8A71`, body: "" };
2500
+ function defaultChapter(index, titleFor = (i) => `\u7B2C${i + 1}\u8A71`, bodyFor = () => "") {
2501
+ return { id: `chapter-${Date.now()}-${index}`, title: titleFor(index), body: bodyFor(index) };
2295
2502
  }
2296
2503
  function useManuscriptDraft(options = {}) {
2297
- const { onAutosave, autosaveDelay = DEFAULT_DELAY } = options;
2298
- const [chapters, setChaptersState] = useState9(
2299
- () => options.initialChapters?.length ? options.initialChapters : [defaultChapter(0)]
2504
+ const {
2505
+ onAutosave,
2506
+ autosaveDelay = DEFAULT_DELAY,
2507
+ autosavePayload,
2508
+ autosaveKey,
2509
+ defaultChapterTitle,
2510
+ defaultChapterBody
2511
+ } = options;
2512
+ const titleForRef = useRef12(defaultChapterTitle);
2513
+ titleForRef.current = defaultChapterTitle;
2514
+ const bodyForRef = useRef12(defaultChapterBody);
2515
+ bodyForRef.current = defaultChapterBody;
2516
+ const [chapters, setChaptersState] = useState10(
2517
+ () => options.initialChapters?.length ? options.initialChapters : [defaultChapter(0, defaultChapterTitle, defaultChapterBody)]
2300
2518
  );
2301
- const [selected, setSelectedState] = useState9(0);
2302
- const saveRef = useRef11(onAutosave);
2519
+ const [selected, setSelectedState] = useState10(0);
2520
+ const [autosaveError, setAutosaveError] = useState10(null);
2521
+ const saveRef = useRef12(onAutosave);
2303
2522
  saveRef.current = onAutosave;
2304
- const didMountRef = useRef11(false);
2523
+ const payloadRef = useRef12(autosavePayload);
2524
+ payloadRef.current = autosavePayload;
2525
+ const chaptersRef = useRef12(chapters);
2526
+ chaptersRef.current = chapters;
2527
+ const didMountRef = useRef12(false);
2528
+ const dirtyRef = useRef12(false);
2529
+ const mountedRef = useRef12(true);
2530
+ const flushAutosave = useCallback8(() => {
2531
+ 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) => {
2536
+ if (!mountedRef.current) return;
2537
+ setAutosaveError(err instanceof Error ? err : new Error(String(err)));
2538
+ });
2539
+ }, []);
2305
2540
  useEffect8(() => {
2306
2541
  if (!didMountRef.current) {
2307
2542
  didMountRef.current = true;
2308
2543
  return void 0;
2309
2544
  }
2310
- const callback = saveRef.current;
2311
- if (!callback) return void 0;
2545
+ if (!saveRef.current) return void 0;
2546
+ dirtyRef.current = true;
2547
+ setAutosaveError(null);
2312
2548
  const timer = setTimeout(() => {
2313
- void callback(chapters);
2549
+ flushAutosave();
2314
2550
  }, autosaveDelay);
2315
2551
  return () => clearTimeout(timer);
2316
- }, [chapters, autosaveDelay]);
2552
+ }, [chapters, autosaveKey, autosaveDelay, flushAutosave]);
2553
+ useEffect8(() => {
2554
+ mountedRef.current = true;
2555
+ const handleBeforeUnload = () => flushAutosave();
2556
+ window.addEventListener("beforeunload", handleBeforeUnload);
2557
+ return () => {
2558
+ flushAutosave();
2559
+ mountedRef.current = false;
2560
+ window.removeEventListener("beforeunload", handleBeforeUnload);
2561
+ };
2562
+ }, [flushAutosave]);
2317
2563
  const setSelected = useCallback8(
2318
2564
  (index) => {
2319
2565
  setSelectedState(() => Math.max(0, Math.min(index, chapters.length - 1)));
@@ -2322,7 +2568,7 @@ function useManuscriptDraft(options = {}) {
2322
2568
  );
2323
2569
  const setChapters = useCallback8(
2324
2570
  (next) => {
2325
- const normalized = next.length ? next : [defaultChapter(0)];
2571
+ const normalized = next.length ? next : [defaultChapter(0, titleForRef.current, bodyForRef.current)];
2326
2572
  setChaptersState(normalized);
2327
2573
  setSelectedState((current) => {
2328
2574
  const selectedId = chapters[current]?.id;
@@ -2339,7 +2585,7 @@ function useManuscriptDraft(options = {}) {
2339
2585
  }, []);
2340
2586
  const addChapter = useCallback8((chapter = {}) => {
2341
2587
  setChaptersState((current) => {
2342
- const generated = defaultChapter(current.length);
2588
+ const generated = defaultChapter(current.length, titleForRef.current, bodyForRef.current);
2343
2589
  const next = [
2344
2590
  ...current,
2345
2591
  {
@@ -2388,12 +2634,14 @@ function useManuscriptDraft(options = {}) {
2388
2634
  patchChapter,
2389
2635
  addChapter,
2390
2636
  removeChapter,
2391
- reorderChapters
2637
+ reorderChapters,
2638
+ autosaveError,
2639
+ flushAutosave
2392
2640
  };
2393
2641
  }
2394
2642
 
2395
2643
  // src/MejiroManuscriptEditor.tsx
2396
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2644
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
2397
2645
  function downloadEpub(buffer, title) {
2398
2646
  const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2399
2647
  const a = document.createElement("a");
@@ -2445,14 +2693,14 @@ function MejiroManuscriptEditor({
2445
2693
  onExport
2446
2694
  }) {
2447
2695
  const messages = useI18n();
2448
- const coverInputRef = useRef12(null);
2449
- const bodyTextareaRef = useRef12(null);
2696
+ const coverInputRef = useRef13(null);
2697
+ const bodyTextareaRef = useRef13(null);
2450
2698
  const titleControlled = onTitleChange !== void 0;
2451
2699
  const authorControlled = onAuthorChange !== void 0;
2452
2700
  const coverControlled = onCoverChange !== void 0;
2453
- const [titleState, setTitleState] = useState10(titleProp ?? messages.manuscriptDefaultTitle);
2454
- const [authorState, setAuthorState] = useState10(authorProp ?? "");
2455
- const [coverState, setCoverState] = useState10(coverProp ?? null);
2701
+ const [titleState, setTitleState] = useState11(titleProp ?? messages.manuscriptDefaultTitle);
2702
+ const [authorState, setAuthorState] = useState11(authorProp ?? "");
2703
+ const [coverState, setCoverState] = useState11(coverProp ?? null);
2456
2704
  const title = titleControlled ? titleProp ?? "" : titleState;
2457
2705
  const author = authorControlled ? authorProp ?? "" : authorState;
2458
2706
  const cover = coverControlled ? coverProp ?? null : coverState;
@@ -2489,7 +2737,10 @@ function MejiroManuscriptEditor({
2489
2737
  const draft = useManuscriptDraft({
2490
2738
  initialChapters: initialChapters?.length ? initialChapters : [defaultChapter2(messages)],
2491
2739
  onAutosave,
2492
- autosaveDelay
2740
+ autosavePayload: (savedChapters) => ({ title, author, cover, chapters: savedChapters }),
2741
+ autosaveKey: `${title}\0${author}\0${cover?.name ?? ""}\0${cover?.size ?? 0}\0${cover?.lastModified ?? 0}`,
2742
+ autosaveDelay,
2743
+ defaultChapterTitle: (index) => format(messages.manuscriptDefaultChapterTitle, { n: index + 1 })
2493
2744
  });
2494
2745
  const {
2495
2746
  chapters,
@@ -2501,7 +2752,7 @@ function MejiroManuscriptEditor({
2501
2752
  reorderChapters
2502
2753
  } = draft;
2503
2754
  const current = chapters[selected] ?? chapters[0];
2504
- const [draggingIndex, setDraggingIndex] = useState10(null);
2755
+ const [draggingIndex, setDraggingIndex] = useState11(null);
2505
2756
  function wrapSelection(open, close) {
2506
2757
  const el = bodyTextareaRef.current;
2507
2758
  if (!el) return;
@@ -2539,8 +2790,8 @@ function MejiroManuscriptEditor({
2539
2790
  onExport?.(buffer);
2540
2791
  downloadEpub(buffer, title);
2541
2792
  }, [assetResolver, author, chapters, cover, messages.untitled, onExport, title]);
2542
- return /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor mejiro-manuscript-editor", children: [
2543
- /* @__PURE__ */ jsx14("main", { className: "mejiro-editor-preview", children: /* @__PURE__ */ jsx14(
2793
+ return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor mejiro-manuscript-editor", children: [
2794
+ /* @__PURE__ */ jsx16("main", { className: "mejiro-editor-preview", children: /* @__PURE__ */ jsx16(
2544
2795
  MejiroReader,
2545
2796
  {
2546
2797
  subtitle: messages.manuscriptPreviewSubtitle,
@@ -2558,18 +2809,19 @@ function MejiroManuscriptEditor({
2558
2809
  enableImageOverlay: false
2559
2810
  }
2560
2811
  ) }),
2561
- /* @__PURE__ */ jsxs12("aside", { className: "mejiro-editor-panel", children: [
2562
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-head", children: [
2563
- /* @__PURE__ */ jsx14("span", { children: messages.manuscriptTitle }),
2564
- /* @__PURE__ */ jsx14("strong", { children: title }),
2565
- /* @__PURE__ */ jsx14("small", { children: messages.manuscriptRubyHint })
2812
+ /* @__PURE__ */ jsxs11("aside", { className: "mejiro-editor-panel", children: [
2813
+ /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-head", children: [
2814
+ /* @__PURE__ */ jsx16("span", { children: messages.manuscriptTitle }),
2815
+ /* @__PURE__ */ jsx16("strong", { children: title }),
2816
+ /* @__PURE__ */ jsx16("small", { children: messages.manuscriptRubyHint })
2566
2817
  ] }),
2567
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
2568
- /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptMetadata }),
2569
- /* @__PURE__ */ jsx14("input", { value: title, onChange: (event) => setTitle(event.target.value) }),
2570
- /* @__PURE__ */ jsx14("input", { value: author, onChange: (event) => setAuthor(event.target.value) }),
2571
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => coverInputRef.current?.click(), children: cover ? cover.name : messages.manuscriptChooseCoverImage }),
2572
- /* @__PURE__ */ jsx14(
2818
+ draft.autosaveError && /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-error", children: draft.autosaveError.message }),
2819
+ /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2820
+ /* @__PURE__ */ jsx16("span", { className: "mejiro-editor-label", children: messages.manuscriptMetadata }),
2821
+ /* @__PURE__ */ jsx16("input", { value: title, onChange: (event) => setTitle(event.target.value) }),
2822
+ /* @__PURE__ */ jsx16("input", { value: author, onChange: (event) => setAuthor(event.target.value) }),
2823
+ /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => coverInputRef.current?.click(), children: cover ? cover.name : messages.manuscriptChooseCoverImage }),
2824
+ /* @__PURE__ */ jsx16(
2573
2825
  "input",
2574
2826
  {
2575
2827
  ref: coverInputRef,
@@ -2580,9 +2832,9 @@ function MejiroManuscriptEditor({
2580
2832
  }
2581
2833
  )
2582
2834
  ] }),
2583
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
2584
- /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptChapters }),
2585
- /* @__PURE__ */ jsx14("div", { className: "mejiro-editor-paragraphs", children: chapters.map((chapter, index) => /* @__PURE__ */ jsxs12(
2835
+ /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2836
+ /* @__PURE__ */ jsx16("span", { className: "mejiro-editor-label", children: messages.manuscriptChapters }),
2837
+ /* @__PURE__ */ jsx16("div", { className: "mejiro-editor-paragraphs", children: chapters.map((chapter, index) => /* @__PURE__ */ jsxs11(
2586
2838
  "button",
2587
2839
  {
2588
2840
  type: "button",
@@ -2611,14 +2863,14 @@ function MejiroManuscriptEditor({
2611
2863
  },
2612
2864
  "data-dragging": draggingIndex === index ? "" : void 0,
2613
2865
  children: [
2614
- /* @__PURE__ */ jsx14("span", { children: format(messages.chapterN, { n: index + 1 }) }),
2615
- /* @__PURE__ */ jsx14("strong", { children: chapter.title || messages.untitled })
2866
+ /* @__PURE__ */ jsx16("span", { children: format(messages.chapterN, { n: index + 1 }) }),
2867
+ /* @__PURE__ */ jsx16("strong", { children: chapter.title || messages.untitled })
2616
2868
  ]
2617
2869
  },
2618
2870
  chapter.id
2619
2871
  )) }),
2620
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-grid", children: [
2621
- /* @__PURE__ */ jsx14(
2872
+ /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid", children: [
2873
+ /* @__PURE__ */ jsx16(
2622
2874
  "button",
2623
2875
  {
2624
2876
  type: "button",
@@ -2630,138 +2882,41 @@ function MejiroManuscriptEditor({
2630
2882
  children: messages.manuscriptAddChapter
2631
2883
  }
2632
2884
  ),
2633
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => removeChapter(selected), children: messages.manuscriptRemove })
2885
+ /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => removeChapter(selected), children: messages.manuscriptRemove })
2634
2886
  ] })
2635
2887
  ] }),
2636
- current && /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
2637
- /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptDraft }),
2638
- /* @__PURE__ */ jsx14(
2888
+ current && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2889
+ /* @__PURE__ */ jsx16("span", { className: "mejiro-editor-label", children: messages.manuscriptDraft }),
2890
+ /* @__PURE__ */ jsx16(
2639
2891
  "input",
2640
2892
  {
2641
2893
  value: current.title,
2642
2894
  onChange: (event) => patchChapter(selected, { title: event.target.value })
2643
2895
  }
2644
2896
  ),
2645
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
2646
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("\u300A\u300A", "\u300B\u300B"), children: messages.manuscriptEmphasisDots }),
2647
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("\u3014", "\u3015"), children: messages.manuscriptTcy }),
2648
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("*", "*"), children: messages.manuscriptEm }),
2649
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("**", "**"), children: messages.manuscriptStrong })
2897
+ /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
2898
+ /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u300A\u300A", "\u300B\u300B"), children: messages.manuscriptEmphasisDots }),
2899
+ /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("\u3014", "\u3015"), children: messages.manuscriptTcy }),
2900
+ /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("*", "*"), children: messages.manuscriptEm }),
2901
+ /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => wrapSelection("**", "**"), children: messages.manuscriptStrong })
2650
2902
  ] }),
2651
- /* @__PURE__ */ jsx14(
2652
- "textarea",
2903
+ /* @__PURE__ */ jsx16(
2904
+ MejiroNotationHighlighter,
2653
2905
  {
2654
2906
  ref: bodyTextareaRef,
2655
2907
  className: "mejiro-editor-manuscript",
2656
2908
  value: current.body,
2657
- onChange: (event) => patchChapter(selected, { body: event.target.value })
2909
+ onChange: (body) => patchChapter(selected, { body })
2658
2910
  }
2659
2911
  )
2660
2912
  ] }),
2661
- /* @__PURE__ */ jsx14("button", { type: "button", className: "mejiro-editor-export", onClick: () => void exportEpub(), children: messages.editorExportEpub })
2913
+ /* @__PURE__ */ jsx16("button", { type: "button", className: "mejiro-editor-export", onClick: () => void exportEpub(), children: messages.editorExportEpub })
2662
2914
  ] })
2663
2915
  ] });
2664
2916
  }
2665
2917
 
2666
- // src/MejiroNotationHighlighter.tsx
2667
- import { tokenizeManuscriptSource } from "@libraz/mejiro";
2668
- import {
2669
- forwardRef as forwardRef2,
2670
- useImperativeHandle as useImperativeHandle2,
2671
- useMemo as useMemo5,
2672
- useRef as useRef13
2673
- } from "react";
2674
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2675
- var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2({
2676
- value,
2677
- onChange,
2678
- dialect = "mejiro",
2679
- wrapperClassName,
2680
- wrapperStyle,
2681
- className,
2682
- style,
2683
- onScroll,
2684
- ...rest
2685
- }, ref) {
2686
- const textareaRef = useRef13(null);
2687
- const overlayRef = useRef13(null);
2688
- useImperativeHandle2(ref, () => textareaRef.current, []);
2689
- const segments = useMemo5(() => buildSegments(value, dialect), [value, dialect]);
2690
- return /* @__PURE__ */ jsxs13(
2691
- "div",
2692
- {
2693
- className: `mejiro-notation-highlighter${wrapperClassName ? ` ${wrapperClassName}` : ""}`,
2694
- style: wrapperStyle,
2695
- children: [
2696
- /* @__PURE__ */ jsxs13(
2697
- "div",
2698
- {
2699
- ref: overlayRef,
2700
- className: "mejiro-notation-overlay",
2701
- "aria-hidden": "true",
2702
- children: [
2703
- segments.map((segment) => /* @__PURE__ */ jsx15(
2704
- "span",
2705
- {
2706
- className: segment.kind ? `mejiro-notation-token mejiro-notation-${segment.kind}` : void 0,
2707
- "data-token": segment.kind,
2708
- children: segment.text
2709
- },
2710
- segment.key
2711
- )),
2712
- "\u200B"
2713
- ]
2714
- }
2715
- ),
2716
- /* @__PURE__ */ jsx15(
2717
- "textarea",
2718
- {
2719
- ...rest,
2720
- ref: textareaRef,
2721
- className: `mejiro-notation-textarea${className ? ` ${className}` : ""}`,
2722
- style,
2723
- value,
2724
- onChange: (event) => onChange(event.target.value),
2725
- onScroll: (event) => {
2726
- if (overlayRef.current) {
2727
- overlayRef.current.scrollTop = event.currentTarget.scrollTop;
2728
- overlayRef.current.scrollLeft = event.currentTarget.scrollLeft;
2729
- }
2730
- onScroll?.(event);
2731
- },
2732
- spellCheck: false
2733
- }
2734
- )
2735
- ]
2736
- }
2737
- );
2738
- });
2739
- function buildSegments(text, dialect) {
2740
- const tokens = tokenizeManuscriptSource(text, dialect);
2741
- const segments = [];
2742
- let cursor = 0;
2743
- for (const [i, token] of tokens.entries()) {
2744
- if (token.start > cursor) {
2745
- segments.push({ key: `t-${i}-pre`, text: text.slice(cursor, token.start) });
2746
- }
2747
- segments.push({
2748
- key: `t-${i}`,
2749
- text: text.slice(token.start, token.end),
2750
- kind: token.kind
2751
- });
2752
- cursor = token.end;
2753
- }
2754
- if (cursor < text.length) {
2755
- segments.push({ key: "tail", text: text.slice(cursor) });
2756
- }
2757
- if (segments.length === 0) {
2758
- segments.push({ key: "empty", text });
2759
- }
2760
- return segments;
2761
- }
2762
-
2763
2918
  // src/MejiroShelf.tsx
2764
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2919
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
2765
2920
  function MejiroShelf({
2766
2921
  volumes,
2767
2922
  currentId,
@@ -2770,26 +2925,26 @@ function MejiroShelf({
2770
2925
  }) {
2771
2926
  const messages = useI18n();
2772
2927
  const resolvedTitle = title ?? messages.shelfTitle;
2773
- return /* @__PURE__ */ jsxs14("section", { className: "mejiro-shelf", "aria-label": resolvedTitle, children: [
2774
- /* @__PURE__ */ jsx16("header", { className: "mejiro-shelf-header", children: /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-title", children: resolvedTitle }) }),
2775
- /* @__PURE__ */ jsx16("ul", { className: "mejiro-shelf-grid", children: volumes.map((v) => /* @__PURE__ */ jsx16("li", { className: "mejiro-shelf-item", children: /* @__PURE__ */ jsxs14(
2928
+ return /* @__PURE__ */ jsxs12("section", { className: "mejiro-shelf", "aria-label": resolvedTitle, children: [
2929
+ /* @__PURE__ */ jsx17("header", { className: "mejiro-shelf-header", children: /* @__PURE__ */ jsx17("span", { className: "mejiro-shelf-title", children: resolvedTitle }) }),
2930
+ /* @__PURE__ */ jsx17("ul", { className: "mejiro-shelf-grid", children: volumes.map((v) => /* @__PURE__ */ jsx17("li", { className: "mejiro-shelf-item", children: /* @__PURE__ */ jsxs12(
2776
2931
  "button",
2777
2932
  {
2778
2933
  type: "button",
2779
2934
  className: `mejiro-shelf-card${v.id === currentId ? " is-active" : ""}`,
2780
2935
  onClick: () => onSelect?.(v),
2781
2936
  children: [
2782
- v.cover ? /* @__PURE__ */ jsx16(
2937
+ v.cover ? /* @__PURE__ */ jsx17(
2783
2938
  "span",
2784
2939
  {
2785
2940
  className: "mejiro-shelf-cover",
2786
2941
  style: { backgroundImage: `url(${JSON.stringify(v.cover)})` },
2787
2942
  "aria-hidden": "true"
2788
2943
  }
2789
- ) : /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-cover mejiro-shelf-cover--blank", "aria-hidden": "true" }),
2790
- /* @__PURE__ */ jsxs14("span", { className: "mejiro-shelf-meta", children: [
2791
- /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-label", children: v.label }),
2792
- v.author && /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-author", children: v.author })
2944
+ ) : /* @__PURE__ */ jsx17("span", { className: "mejiro-shelf-cover mejiro-shelf-cover--blank", "aria-hidden": "true" }),
2945
+ /* @__PURE__ */ jsxs12("span", { className: "mejiro-shelf-meta", children: [
2946
+ /* @__PURE__ */ jsx17("span", { className: "mejiro-shelf-label", children: v.label }),
2947
+ v.author && /* @__PURE__ */ jsx17("span", { className: "mejiro-shelf-author", children: v.author })
2793
2948
  ] })
2794
2949
  ]
2795
2950
  }
@@ -2798,8 +2953,8 @@ function MejiroShelf({
2798
2953
  }
2799
2954
 
2800
2955
  // src/MejiroToc.tsx
2801
- import { useMemo as useMemo6, useState as useState11 } from "react";
2802
- import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2956
+ import { useMemo as useMemo7, useState as useState12 } from "react";
2957
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
2803
2958
  function buildEntries(epub, messages) {
2804
2959
  return epub.chapters.map((ch, i) => {
2805
2960
  const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
@@ -2818,9 +2973,9 @@ function MejiroToc({
2818
2973
  const messages = useI18n();
2819
2974
  const resolvedTitle = title ?? messages.tocTitle;
2820
2975
  const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
2821
- const [query, setQuery] = useState11("");
2822
- const entries = useMemo6(() => buildEntries(epub, messages), [epub, messages]);
2823
- const filtered = useMemo6(() => {
2976
+ const [query, setQuery] = useState12("");
2977
+ const entries = useMemo7(() => buildEntries(epub, messages), [epub, messages]);
2978
+ const filtered = useMemo7(() => {
2824
2979
  if (!query) return entries;
2825
2980
  const needle = query.toLowerCase();
2826
2981
  return entries.filter(
@@ -2828,12 +2983,12 @@ function MejiroToc({
2828
2983
  );
2829
2984
  }, [entries, query]);
2830
2985
  const activeIndex = currentAnchor?.chapter ?? -1;
2831
- return /* @__PURE__ */ jsxs15("nav", { className: "mejiro-toc", "aria-label": resolvedTitle, children: [
2832
- /* @__PURE__ */ jsxs15("header", { className: "mejiro-toc-header", children: [
2833
- /* @__PURE__ */ jsx17("span", { className: "mejiro-toc-title", children: resolvedTitle }),
2834
- epub.author && /* @__PURE__ */ jsx17("span", { className: "mejiro-toc-subtitle", children: epub.author })
2986
+ return /* @__PURE__ */ jsxs13("nav", { className: "mejiro-toc", "aria-label": resolvedTitle, children: [
2987
+ /* @__PURE__ */ jsxs13("header", { className: "mejiro-toc-header", children: [
2988
+ /* @__PURE__ */ jsx18("span", { className: "mejiro-toc-title", children: resolvedTitle }),
2989
+ epub.author && /* @__PURE__ */ jsx18("span", { className: "mejiro-toc-subtitle", children: epub.author })
2835
2990
  ] }),
2836
- searchable && /* @__PURE__ */ jsx17("div", { className: "mejiro-toc-search", children: /* @__PURE__ */ jsx17(
2991
+ searchable && /* @__PURE__ */ jsx18("div", { className: "mejiro-toc-search", children: /* @__PURE__ */ jsx18(
2837
2992
  "input",
2838
2993
  {
2839
2994
  type: "search",
@@ -2843,8 +2998,8 @@ function MejiroToc({
2843
2998
  "aria-label": resolvedSearchPlaceholder
2844
2999
  }
2845
3000
  ) }),
2846
- /* @__PURE__ */ jsx17("ol", { className: "mejiro-toc-list", children: filtered.map((entry) => /* @__PURE__ */ jsxs15("li", { className: "mejiro-toc-item", children: [
2847
- /* @__PURE__ */ jsxs15(
3001
+ /* @__PURE__ */ jsx18("ol", { className: "mejiro-toc-list", children: filtered.map((entry) => /* @__PURE__ */ jsxs13("li", { className: "mejiro-toc-item", children: [
3002
+ /* @__PURE__ */ jsxs13(
2848
3003
  "button",
2849
3004
  {
2850
3005
  type: "button",
@@ -2852,58 +3007,37 @@ function MejiroToc({
2852
3007
  "aria-current": entry.index === activeIndex ? "true" : void 0,
2853
3008
  onClick: () => onSelect?.(entry.index),
2854
3009
  children: [
2855
- /* @__PURE__ */ jsx17("span", { className: "mejiro-toc-num", children: String(entry.index + 1).padStart(2, "0") }),
2856
- /* @__PURE__ */ jsx17("span", { className: "mejiro-toc-label", children: entry.title })
3010
+ /* @__PURE__ */ jsx18("span", { className: "mejiro-toc-num", children: String(entry.index + 1).padStart(2, "0") }),
3011
+ /* @__PURE__ */ jsx18("span", { className: "mejiro-toc-label", children: entry.title })
2857
3012
  ]
2858
3013
  }
2859
3014
  ),
2860
- entry.headings.length > 0 && /* @__PURE__ */ jsx17("ul", { className: "mejiro-toc-subheads", children: entry.headings.slice(0, 5).map((h) => /* @__PURE__ */ jsx17("li", { className: "mejiro-toc-subhead", children: h }, h)) })
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)) })
2861
3016
  ] }, entry.index)) }),
2862
- filtered.length === 0 && /* @__PURE__ */ jsx17("div", { className: "mejiro-toc-empty", role: "status", children: format(messages.tocEmpty, { query }) })
3017
+ filtered.length === 0 && /* @__PURE__ */ jsx18("div", { className: "mejiro-toc-empty", role: "status", children: format(messages.tocEmpty, { query }) })
2863
3018
  ] });
2864
3019
  }
2865
3020
 
2866
3021
  // src/useAnnotations.ts
2867
- import { useCallback as useCallback10, useEffect as useEffect10, useRef as useRef14, useState as useState12 } from "react";
2868
- var STORAGE_VERSION = 1;
3022
+ import {
3023
+ createAnnotationId,
3024
+ parseAnnotations,
3025
+ serializeAnnotations,
3026
+ sortAnnotations
3027
+ } from "@libraz/mejiro";
3028
+ import { useCallback as useCallback10, useEffect as useEffect10, useRef as useRef14, useState as useState13 } from "react";
2869
3029
  function resolveDefaultStorage() {
2870
3030
  if (typeof globalThis === "undefined") return null;
2871
3031
  const w = globalThis.localStorage;
2872
3032
  return w ?? null;
2873
3033
  }
2874
- function parse(raw) {
2875
- if (!raw) return [];
2876
- try {
2877
- const parsed = JSON.parse(raw);
2878
- if (parsed && Array.isArray(parsed.annotations)) return parsed.annotations;
2879
- } catch {
2880
- }
2881
- return [];
2882
- }
2883
- function serialize(annotations) {
2884
- const payload = { version: STORAGE_VERSION, annotations: [...annotations] };
2885
- return JSON.stringify(payload);
2886
- }
2887
- function sort(annotations) {
2888
- return [...annotations].sort((a, b) => {
2889
- if (a.chapter !== b.chapter) return a.chapter - b.chapter;
2890
- if (a.start.paragraph !== b.start.paragraph) return a.start.paragraph - b.start.paragraph;
2891
- return a.start.charIndex - b.start.charIndex;
2892
- });
2893
- }
2894
- function makeId() {
2895
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
2896
- return crypto.randomUUID();
2897
- }
2898
- return `ann-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2899
- }
2900
3034
  function useAnnotations(options) {
2901
3035
  const { key, throttleMs = 250 } = options;
2902
3036
  const storage = options.storage ?? resolveDefaultStorage();
2903
- const [annotations, setAnnotations] = useState12(() => {
3037
+ const [annotations, setAnnotations] = useState13(() => {
2904
3038
  if (!storage) return [];
2905
3039
  try {
2906
- return sort(parse(storage.getItem(key)));
3040
+ return sortAnnotations(parseAnnotations(storage.getItem(key)));
2907
3041
  } catch {
2908
3042
  return [];
2909
3043
  }
@@ -2914,7 +3048,7 @@ function useAnnotations(options) {
2914
3048
  return;
2915
3049
  }
2916
3050
  try {
2917
- setAnnotations(sort(parse(storage.getItem(key))));
3051
+ setAnnotations(sortAnnotations(parseAnnotations(storage.getItem(key))));
2918
3052
  } catch {
2919
3053
  setAnnotations([]);
2920
3054
  }
@@ -2934,7 +3068,7 @@ function useAnnotations(options) {
2934
3068
  if (timerRef.current) clearTimeout(timerRef.current);
2935
3069
  timerRef.current = setTimeout(() => {
2936
3070
  try {
2937
- storage.setItem(key, serialize(next));
3071
+ storage.setItem(key, serializeAnnotations(next));
2938
3072
  } catch {
2939
3073
  }
2940
3074
  }, throttleMs);
@@ -2947,11 +3081,11 @@ function useAnnotations(options) {
2947
3081
  (input) => {
2948
3082
  const annotation = {
2949
3083
  ...input,
2950
- id: input.id ?? makeId(),
3084
+ id: input.id ?? createAnnotationId(),
2951
3085
  createdAt: input.createdAt ?? Date.now()
2952
3086
  };
2953
3087
  setAnnotations((current) => {
2954
- const next = sort([...current, annotation]);
3088
+ const next = sortAnnotations([...current, annotation]);
2955
3089
  commit(next);
2956
3090
  return next;
2957
3091
  });
@@ -2979,7 +3113,7 @@ function useAnnotations(options) {
2979
3113
  return { ...annotation, ...patch, id };
2980
3114
  });
2981
3115
  if (!changed) return current;
2982
- const sorted = sort(next);
3116
+ const sorted = sortAnnotations(next);
2983
3117
  commit(sorted);
2984
3118
  return sorted;
2985
3119
  });
@@ -3004,14 +3138,14 @@ function useAnnotations(options) {
3004
3138
  import {
3005
3139
  EditableEpub as EditableEpub2
3006
3140
  } from "@libraz/mejiro/epub";
3007
- import { useCallback as useCallback11, useEffect as useEffect11, useRef as useRef15, useState as useState13 } from "react";
3141
+ import { useCallback as useCallback11, useEffect as useEffect11, useMemo as useMemo8, useRef as useRef15, useState as useState14 } from "react";
3008
3142
  function useEditableEpub(options = {}) {
3009
- const [editor, setEditor] = useState13(null);
3010
- const [loading, setLoading] = useState13(false);
3011
- const [exporting, setExporting] = useState13(false);
3012
- const [error, setError] = useState13(null);
3013
- const [revision, setRevision] = useState13(0);
3014
- const [selection, setSelectionState] = useState13({
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({
3015
3149
  chapter: 0,
3016
3150
  paragraph: 0
3017
3151
  });
@@ -3099,7 +3233,10 @@ function useEditableEpub(options = {}) {
3099
3233
  }, [options.defaultUrl, loadUrl]);
3100
3234
  const book = editor?.book ?? null;
3101
3235
  const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
3102
- const previewBook = book ? cloneBook2(book) : null;
3236
+ const previewBook = useMemo8(() => {
3237
+ void revision;
3238
+ return book ? cloneBook2(book) : null;
3239
+ }, [book, revision]);
3103
3240
  const history = editor?.history ?? null;
3104
3241
  const setSelection = useCallback11(
3105
3242
  (nextSelection) => {
@@ -3222,20 +3359,22 @@ import {
3222
3359
  EpubProject as EpubProject2,
3223
3360
  parseEpub as parseEpub2
3224
3361
  } from "@libraz/mejiro/epub";
3225
- import { useCallback as useCallback12, useEffect as useEffect12, useMemo as useMemo7, useRef as useRef16, useState as useState14 } from "react";
3362
+ import { useCallback as useCallback12, useEffect as useEffect12, useMemo as useMemo9, useRef as useRef16, useState as useState15 } from "react";
3226
3363
  function useEpubProject(options = {}) {
3227
- const [metadata, setMetadataState] = useState14({
3364
+ const defaultTitle = options.defaultChapterTitle;
3365
+ const defaultBody = options.defaultChapterBody;
3366
+ const [metadata, setMetadataState] = useState15({
3228
3367
  title: "\u65B0\u3057\u3044\u4F5C\u54C1",
3229
3368
  language: "ja",
3230
3369
  ...options.metadata
3231
3370
  });
3232
- const [chapters, setChaptersState] = useState14(
3233
- options.chapters?.length ? options.chapters : [defaultChapter3(0)]
3371
+ const [chapters, setChaptersState] = useState15(
3372
+ options.chapters?.length ? options.chapters : [defaultChapter3(0, defaultTitle, defaultBody)]
3234
3373
  );
3235
- const [selectedChapter, setSelectedChapterState] = useState14(0);
3236
- const [previewBook, setPreviewBook] = useState14(null);
3237
- const [previewError, setPreviewError] = useState14(null);
3238
- const [previewing, setPreviewing] = useState14(false);
3374
+ const [selectedChapter, setSelectedChapterState] = useState15(0);
3375
+ const [previewBook, setPreviewBook] = useState15(null);
3376
+ const [previewError, setPreviewError] = useState15(null);
3377
+ const [previewing, setPreviewing] = useState15(false);
3239
3378
  const previewRequestIdRef = useRef16(0);
3240
3379
  const onPreviewRef = useRef16(options.onPreview);
3241
3380
  const onExportRef = useRef16(options.onExport);
@@ -3292,7 +3431,7 @@ function useEpubProject(options = {}) {
3292
3431
  }, []);
3293
3432
  const setChapters = useCallback12(
3294
3433
  (next) => {
3295
- const normalized = next.length ? next : [defaultChapter3(0)];
3434
+ const normalized = next.length ? next : [defaultChapter3(0, defaultTitle, defaultBody)];
3296
3435
  setChaptersState(normalized);
3297
3436
  setSelectedChapterState((current) => {
3298
3437
  const selectedId = chapters[current]?.id;
@@ -3300,7 +3439,7 @@ function useEpubProject(options = {}) {
3300
3439
  return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
3301
3440
  });
3302
3441
  },
3303
- [chapters]
3442
+ [chapters, defaultBody, defaultTitle]
3304
3443
  );
3305
3444
  const setSelectedChapter = useCallback12(
3306
3445
  (index) => {
@@ -3315,21 +3454,24 @@ function useEpubProject(options = {}) {
3315
3454
  )
3316
3455
  );
3317
3456
  }, []);
3318
- const addChapter = useCallback12((chapter = {}) => {
3319
- setChaptersState((current) => {
3320
- const generated = defaultChapter3(current.length);
3321
- const next = [
3322
- ...current,
3323
- {
3324
- id: chapter.id ?? generated.id,
3325
- title: chapter.title ?? generated.title,
3326
- body: chapter.body ?? generated.body
3327
- }
3328
- ];
3329
- setSelectedChapterState(next.length - 1);
3330
- return next;
3331
- });
3332
- }, []);
3457
+ const addChapter = useCallback12(
3458
+ (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
+ });
3472
+ },
3473
+ [defaultBody, defaultTitle]
3474
+ );
3333
3475
  const removeChapter = useCallback12(
3334
3476
  (index = selectedChapter) => {
3335
3477
  setChaptersState((current) => {
@@ -3367,7 +3509,7 @@ function useEpubProject(options = {}) {
3367
3509
  onExportRef.current?.(buffer);
3368
3510
  return buffer;
3369
3511
  }, [buildProject]);
3370
- return useMemo7(
3512
+ return useMemo9(
3371
3513
  () => ({
3372
3514
  metadata,
3373
3515
  chapters,
@@ -3406,11 +3548,11 @@ function useEpubProject(options = {}) {
3406
3548
  ]
3407
3549
  );
3408
3550
  }
3409
- function defaultChapter3(index) {
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" : "") {
3410
3552
  return {
3411
3553
  id: `chapter-${Date.now()}-${index}`,
3412
- title: index === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${index + 1}\u8A71`,
3413
- body: index === 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" : ""
3554
+ title: titleFor(index),
3555
+ body: bodyFor(index)
3414
3556
  };
3415
3557
  }
3416
3558
 
@@ -3420,7 +3562,7 @@ import {
3420
3562
  useCallback as useCallback13,
3421
3563
  useEffect as useEffect13,
3422
3564
  useRef as useRef17,
3423
- useState as useState15
3565
+ useState as useState16
3424
3566
  } from "react";
3425
3567
  function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3426
3568
  const defW = options?.defaultWidth ?? 120;
@@ -3428,7 +3570,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3428
3570
  const defX = options?.defaultX ?? 80;
3429
3571
  const defY = options?.defaultY ?? 100;
3430
3572
  const margin = options?.margin;
3431
- const [imageRect, setImageRect] = useState15(null);
3573
+ const [imageRect, setImageRect] = useState16(null);
3432
3574
  const rectRef = useRef17(imageRect);
3433
3575
  rectRef.current = imageRect;
3434
3576
  const layoutRef = useRef17(layout);
@@ -3550,14 +3692,14 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3550
3692
  }
3551
3693
 
3552
3694
  // src/useLibrary.ts
3553
- import { useCallback as useCallback14, useEffect as useEffect14, useMemo as useMemo8, useState as useState16 } from "react";
3695
+ import { useCallback as useCallback14, useEffect as useEffect14, useMemo as useMemo10, useState as useState17 } from "react";
3554
3696
  function useLibrary(options) {
3555
3697
  const { volumes, initialVolumeId, onChange } = options;
3556
3698
  const initialIndex = initialVolumeId != null ? Math.max(
3557
3699
  0,
3558
3700
  volumes.findIndex((v) => v.id === initialVolumeId)
3559
3701
  ) : 0;
3560
- const [currentId, setCurrentId] = useState16(
3702
+ const [currentId, setCurrentId] = useState17(
3561
3703
  volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
3562
3704
  );
3563
3705
  useEffect14(() => {
@@ -3570,13 +3712,13 @@ function useLibrary(options) {
3570
3712
  setCurrentId(fallback?.id ?? null);
3571
3713
  if (fallback) onChange?.(fallback);
3572
3714
  }, [currentId, volumes, onChange]);
3573
- const index = useMemo8(() => {
3715
+ const index = useMemo10(() => {
3574
3716
  if (volumes.length === 0) return -1;
3575
3717
  if (currentId == null) return 0;
3576
3718
  const found = volumes.findIndex((v) => v.id === currentId);
3577
3719
  return found >= 0 ? found : 0;
3578
3720
  }, [currentId, volumes]);
3579
- const current = useMemo8(
3721
+ const current = useMemo10(
3580
3722
  () => index >= 0 && index < volumes.length ? volumes[index] : null,
3581
3723
  [index, volumes]
3582
3724
  );
@@ -3618,19 +3760,17 @@ function useLibrary(options) {
3618
3760
  }
3619
3761
 
3620
3762
  // src/useManuscriptLayout.ts
3621
- import { useCallback as useCallback15, useEffect as useEffect15, useLayoutEffect as useLayoutEffect4, useRef as useRef18, useState as useState17 } from "react";
3763
+ import { useCallback as useCallback15, useEffect as useEffect15, useLayoutEffect as useLayoutEffect5, useRef as useRef18, useState as useState18 } from "react";
3622
3764
  function useManuscriptLayout(book, chapter, surface, options = {}) {
3623
3765
  const dialect = options.dialect ?? "mejiro";
3624
3766
  const enableResize = options.enableResize ?? true;
3625
3767
  const resizeDebounce = options.resizeDebounce ?? 120;
3626
- const [layout, setLayout] = useState17(null);
3627
- const [pageWidth, setPageWidth] = useState17(0);
3628
- const [pageHeight, setPageHeight] = useState17(0);
3629
- const [contentHeight, setContentHeight] = useState17(0);
3630
- const [elapsedMs, setElapsedMs] = useState17(0);
3631
- const layoutRef = useRef18(null);
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);
3632
3773
  const requestIdRef = useRef18(0);
3633
- layoutRef.current = layout;
3634
3774
  const recompute = useCallback15(async () => {
3635
3775
  const requestId = ++requestIdRef.current;
3636
3776
  if (!(chapter && surface.current)) {
@@ -3653,7 +3793,7 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
3653
3793
  setLayout(next);
3654
3794
  setElapsedMs(performance.now() - t0);
3655
3795
  }, [book, chapter, dialect, surface]);
3656
- useLayoutEffect4(() => {
3796
+ useLayoutEffect5(() => {
3657
3797
  requestIdRef.current++;
3658
3798
  setLayout(null);
3659
3799
  setPageWidth(0);
@@ -3669,95 +3809,85 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
3669
3809
  }, [recompute]);
3670
3810
  useEffect15(() => {
3671
3811
  if (!enableResize) return;
3812
+ const el = surface.current;
3813
+ if (!el) return;
3672
3814
  let timer = null;
3673
- const onResize = () => {
3674
- if (!(surface.current && layoutRef.current)) return;
3675
- if (timer) clearTimeout(timer);
3815
+ let observed = false;
3816
+ const clear = () => {
3817
+ if (timer) {
3818
+ clearTimeout(timer);
3819
+ timer = null;
3820
+ }
3821
+ };
3822
+ const scheduleReflow = (immediate) => {
3823
+ clear();
3824
+ if (immediate) {
3825
+ void recompute();
3826
+ return;
3827
+ }
3676
3828
  timer = setTimeout(() => {
3677
- if (!(surface.current && layoutRef.current)) return;
3678
- const dims = book.computePageSize(surface.current);
3679
- setPageWidth(dims.pageWidth);
3680
- setPageHeight(dims.pageHeight);
3681
- setContentHeight(dims.contentHeight);
3682
- layoutRef.current.resize({
3683
- pageWidth: dims.pageWidth,
3684
- lineWidth: dims.contentHeight - book.getOptions().fontSize * 0.5
3685
- });
3829
+ timer = null;
3830
+ void recompute();
3686
3831
  }, resizeDebounce);
3687
3832
  };
3688
- window.addEventListener("resize", onResize);
3833
+ if (typeof ResizeObserver !== "undefined") {
3834
+ const observer = new ResizeObserver(() => {
3835
+ const immediate = !observed;
3836
+ observed = true;
3837
+ scheduleReflow(immediate);
3838
+ });
3839
+ observer.observe(el);
3840
+ return () => {
3841
+ observer.disconnect();
3842
+ clear();
3843
+ };
3844
+ }
3845
+ const onWindowResize = () => scheduleReflow(false);
3846
+ window.addEventListener("resize", onWindowResize);
3689
3847
  return () => {
3690
- window.removeEventListener("resize", onResize);
3691
- if (timer) clearTimeout(timer);
3848
+ window.removeEventListener("resize", onWindowResize);
3849
+ clear();
3692
3850
  };
3693
- }, [book, surface, enableResize, resizeDebounce]);
3851
+ }, [surface, enableResize, resizeDebounce, recompute]);
3694
3852
  return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
3695
3853
  }
3696
3854
 
3697
3855
  // src/useReadingPosition.ts
3698
- import { useCallback as useCallback16, useEffect as useEffect16, useRef as useRef19, useState as useState18 } from "react";
3699
- var STORAGE_VERSION2 = 2;
3856
+ import {
3857
+ parseReadingPosition,
3858
+ serializeReadingPosition
3859
+ } from "@libraz/mejiro";
3860
+ import { useCallback as useCallback16, useEffect as useEffect16, useRef as useRef19, useState as useState19 } from "react";
3700
3861
  function resolveDefaultStorage2() {
3701
3862
  if (typeof globalThis === "undefined") return null;
3702
3863
  const w = globalThis.localStorage;
3703
3864
  return w ?? null;
3704
3865
  }
3705
- function parsePosition(raw) {
3706
- if (!raw) return null;
3707
- let parsed;
3708
- try {
3709
- parsed = JSON.parse(raw);
3710
- } catch {
3711
- return null;
3712
- }
3713
- if (!parsed || typeof parsed !== "object") return null;
3714
- const p = parsed;
3715
- if (typeof p.chapter !== "number") return null;
3716
- if (p.version === STORAGE_VERSION2) {
3717
- if (typeof p.paragraph !== "number" || typeof p.charIndex !== "number") return null;
3718
- return { chapter: p.chapter, paragraph: p.paragraph, charIndex: p.charIndex };
3719
- }
3720
- if (typeof p.spreadIdx === "number") {
3721
- if (typeof console !== "undefined" && typeof console.warn === "function") {
3722
- console.warn(
3723
- "[mejiro] useReadingPosition: migrating legacy {chapter, spreadIdx} format. spreadIdx has been dropped because it does not survive reflow; the position now resumes at the start of the chapter. Persist new positions via save() to upgrade."
3724
- );
3725
- }
3726
- return { chapter: p.chapter, paragraph: 0, charIndex: 0 };
3727
- }
3728
- return null;
3729
- }
3730
- function serializePosition(value) {
3731
- const payload = {
3732
- version: STORAGE_VERSION2,
3733
- chapter: value.chapter,
3734
- paragraph: value.paragraph,
3735
- charIndex: value.charIndex
3736
- };
3737
- return JSON.stringify(payload);
3738
- }
3739
3866
  function useReadingPosition(options) {
3740
3867
  const { key, throttleMs = 250 } = options;
3741
3868
  const storage = options.storage ?? resolveDefaultStorage2();
3742
- const [position, setPosition] = useState18(() => {
3869
+ const [position, setPosition] = useState19(() => {
3743
3870
  if (!storage) return null;
3744
3871
  try {
3745
- return parsePosition(storage.getItem(key));
3872
+ return parseReadingPosition(storage.getItem(key));
3746
3873
  } catch {
3747
3874
  return null;
3748
3875
  }
3749
3876
  });
3877
+ const storageRef = useRef19(storage);
3878
+ storageRef.current = storage;
3750
3879
  useEffect16(() => {
3751
- if (!storage) {
3880
+ const currentStorage = storageRef.current;
3881
+ if (!currentStorage) {
3752
3882
  setPosition(null);
3753
3883
  return;
3754
3884
  }
3755
3885
  try {
3756
- setPosition(parsePosition(storage.getItem(key)));
3886
+ setPosition(parseReadingPosition(currentStorage.getItem(key)));
3757
3887
  } catch {
3758
3888
  setPosition(null);
3759
3889
  }
3760
- }, [storage, key]);
3890
+ }, [key]);
3761
3891
  const timerRef = useRef19(null);
3762
3892
  useEffect16(
3763
3893
  () => () => {
@@ -3770,30 +3900,32 @@ function useReadingPosition(options) {
3770
3900
  const save = useCallback16(
3771
3901
  (next) => {
3772
3902
  setPosition(next);
3773
- if (storage) {
3903
+ const currentStorage = storageRef.current;
3904
+ if (currentStorage) {
3774
3905
  if (timerRef.current) clearTimeout(timerRef.current);
3775
3906
  timerRef.current = setTimeout(() => {
3776
3907
  try {
3777
- storage.setItem(key, serializePosition(next));
3908
+ currentStorage.setItem(key, serializeReadingPosition(next));
3778
3909
  } catch {
3779
3910
  }
3780
3911
  }, throttleMs);
3781
3912
  }
3782
3913
  onChangeRef.current?.(next);
3783
3914
  },
3784
- [storage, key, throttleMs]
3915
+ [key, throttleMs]
3785
3916
  );
3786
3917
  const clear = useCallback16(() => {
3787
3918
  setPosition(null);
3788
3919
  if (timerRef.current) clearTimeout(timerRef.current);
3789
- if (storage) {
3920
+ const currentStorage = storageRef.current;
3921
+ if (currentStorage) {
3790
3922
  try {
3791
- storage.removeItem(key);
3923
+ currentStorage.removeItem(key);
3792
3924
  } catch {
3793
3925
  }
3794
3926
  }
3795
3927
  onChangeRef.current?.(null);
3796
- }, [storage, key]);
3928
+ }, [key]);
3797
3929
  return { position, save, clear };
3798
3930
  }
3799
3931
 
@@ -3824,7 +3956,7 @@ export {
3824
3956
  MejiroSpread,
3825
3957
  MejiroStats,
3826
3958
  MejiroToc,
3827
- enMessages,
3959
+ enMessages2 as enMessages,
3828
3960
  format,
3829
3961
  jaMessages,
3830
3962
  resolveMessages,