@libraz/mejiro-react 0.6.0 → 0.8.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +216 -366
  2. package/dist/index.js +626 -663
  3. package/package.json +7 -6
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,7 +151,7 @@ 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
157
  import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
@@ -312,38 +189,36 @@ import {
312
189
 
313
190
  // src/MejiroPageView.tsx
314
191
  import { normalizeFontFamily } from "@libraz/mejiro/browser";
315
- import { Fragment as Fragment3 } from "react";
316
192
 
317
- // src/MejiroPage.tsx
318
- import { Fragment as Fragment2 } from "react";
319
- 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";
320
199
  function renderSegment(segment, key) {
321
- switch (segment.type) {
322
- case "text":
323
- return /* @__PURE__ */ jsx4(Fragment2, { children: segment.text }, key);
324
- case "ruby":
325
- return /* @__PURE__ */ jsxs4("ruby", { children: [
326
- segment.base,
327
- /* @__PURE__ */ jsx4("rt", { children: segment.rubyText })
328
- ] }, key);
329
- case "emphasis":
330
- return /* @__PURE__ */ jsx4("span", { className: `mejiro-emphasis mejiro-emphasis--${segment.style}`, children: segment.text }, key);
331
- case "tcy":
332
- return /* @__PURE__ */ jsx4("span", { className: "mejiro-tcy", children: segment.text }, key);
333
- case "em":
334
- return /* @__PURE__ */ jsx4("em", { children: segment.text }, key);
335
- case "strong":
336
- return /* @__PURE__ */ jsx4("strong", { children: segment.text }, key);
337
- case "link":
338
- return /* @__PURE__ */ jsx4("a", { href: segment.href, title: segment.title, children: segment.text }, key);
339
- case "footnote-ref":
340
- return /* @__PURE__ */ jsx4("a", { className: "mejiro-footnote-ref", href: `#${segment.noteId}`, children: segment.text }, key);
341
- }
200
+ return renderInlineNode(segmentToInlineNode(segment), key);
342
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";
343
218
  function renderLine(line, lineIndex) {
344
219
  const nodes = [];
345
220
  if (lineIndex > 0) {
346
- nodes.push(/* @__PURE__ */ jsx4("br", {}, `br-${lineIndex}`));
221
+ nodes.push(/* @__PURE__ */ jsx5("br", {}, `br-${lineIndex}`));
347
222
  }
348
223
  for (let i = 0; i < line.segments.length; i++) {
349
224
  nodes.push(renderSegment(line.segments[i], `${lineIndex}-${i}`));
@@ -352,7 +227,7 @@ function renderLine(line, lineIndex) {
352
227
  }
353
228
  function MejiroPage({ page, className, style }) {
354
229
  const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
355
- 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) => {
356
231
  let paraClass = "mejiro-paragraph";
357
232
  if (paragraph.headingLevel != null) {
358
233
  paraClass += ` mejiro-paragraph--h${paragraph.headingLevel}`;
@@ -361,36 +236,13 @@ function MejiroPage({ page, className, style }) {
361
236
  }
362
237
  return (
363
238
  // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
364
- /* @__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)
365
240
  );
366
241
  }) });
367
242
  }
368
243
 
369
244
  // src/MejiroPageView.tsx
370
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
371
- function renderSlotSegment(segment, key) {
372
- switch (segment.type) {
373
- case "text":
374
- return /* @__PURE__ */ jsx5(Fragment3, { children: segment.text }, key);
375
- case "ruby":
376
- return /* @__PURE__ */ jsxs5("ruby", { children: [
377
- segment.base,
378
- /* @__PURE__ */ jsx5("rt", { children: segment.rubyText })
379
- ] }, key);
380
- case "emphasis":
381
- return /* @__PURE__ */ jsx5("span", { className: `mejiro-emphasis mejiro-emphasis--${segment.style}`, children: segment.text }, key);
382
- case "tcy":
383
- return /* @__PURE__ */ jsx5("span", { className: "mejiro-tcy", children: segment.text }, key);
384
- case "em":
385
- return /* @__PURE__ */ jsx5("em", { children: segment.text }, key);
386
- case "strong":
387
- return /* @__PURE__ */ jsx5("strong", { children: segment.text }, key);
388
- case "link":
389
- return /* @__PURE__ */ jsx5("a", { href: segment.href, title: segment.title, children: segment.text }, key);
390
- case "footnote-ref":
391
- return /* @__PURE__ */ jsx5("a", { className: "mejiro-footnote-ref", href: `#${segment.noteId}`, children: segment.text }, key);
392
- }
393
- }
245
+ import { jsx as jsx6 } from "react/jsx-runtime";
394
246
  function MejiroPageView({
395
247
  result,
396
248
  fontFamily,
@@ -402,7 +254,7 @@ function MejiroPageView({
402
254
  if (result.hasImages || slotMode) {
403
255
  const rootClass = className ? `mejiro-page-slots ${className}` : "mejiro-page-slots";
404
256
  const fontFamilyCss = fontFamily != null ? normalizeFontFamily(fontFamily) : void 0;
405
- 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) => {
406
258
  const slot = result.slots[i];
407
259
  if (!slot || slot.height <= 0) return null;
408
260
  const colStyle = {
@@ -420,15 +272,15 @@ function MejiroPageView({
420
272
  };
421
273
  return (
422
274
  // biome-ignore lint/suspicious/noArrayIndexKey: lines have no stable ID
423
- /* @__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)
424
276
  );
425
277
  }) });
426
278
  }
427
- return /* @__PURE__ */ jsx5(MejiroPage, { page: result.page, className, style });
279
+ return /* @__PURE__ */ jsx6(MejiroPage, { page: result.page, className, style });
428
280
  }
429
281
 
430
282
  // src/MejiroScrollView.tsx
431
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
283
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
432
284
  function MejiroScrollView({
433
285
  layout,
434
286
  pageWidth,
@@ -445,10 +297,12 @@ function MejiroScrollView({
445
297
  const pageRefs = useRef2([]);
446
298
  const onVisiblePageChangeRef = useRef2(onVisiblePageChange);
447
299
  onVisiblePageChangeRef.current = onVisiblePageChange;
300
+ const programmaticScrollRef = useRef2(false);
448
301
  const pages = useMemo2(() => {
449
302
  const total = layout.totalPages;
450
303
  return Array.from({ length: total }, (_, i) => layout.getPage(i));
451
304
  }, [layout]);
305
+ const pageCount = pages.length;
452
306
  const useSlot = true;
453
307
  const contentStyle = { height: contentHeight };
454
308
  if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
@@ -457,6 +311,7 @@ function MejiroScrollView({
457
311
  useEffect(() => {
458
312
  const container = containerRef.current;
459
313
  if (!container) return;
314
+ if (pageCount === 0) return;
460
315
  if (typeof IntersectionObserver === "undefined") return;
461
316
  let mostVisibleIdx = -1;
462
317
  let mostVisibleRatio = 0;
@@ -475,7 +330,12 @@ function MejiroScrollView({
475
330
  }
476
331
  }
477
332
  }
478
- if (mostVisibleIdx >= 0) onVisiblePageChangeRef.current?.(mostVisibleIdx);
333
+ if (mostVisibleIdx >= 0) {
334
+ onVisiblePageChangeRef.current?.(
335
+ mostVisibleIdx,
336
+ programmaticScrollRef.current ? "programmatic" : "user"
337
+ );
338
+ }
479
339
  },
480
340
  { root: container, threshold: [0.25, 0.5, 0.75] }
481
341
  );
@@ -483,22 +343,28 @@ function MejiroScrollView({
483
343
  if (el) observer.observe(el);
484
344
  }
485
345
  return () => observer.disconnect();
486
- }, []);
346
+ }, [pageCount]);
487
347
  useLayoutEffect(() => {
488
348
  if (scrollToPage == null) return;
349
+ if (pageCount === 0) return;
489
350
  const el = pageRefs.current[scrollToPage];
490
351
  if (!(el && containerRef.current)) return;
352
+ programmaticScrollRef.current = true;
491
353
  containerRef.current.scrollTo({
492
354
  top: el.offsetTop,
493
355
  behavior: "auto"
494
356
  });
495
- }, [scrollToPage]);
496
- 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(
497
363
  "div",
498
364
  {
499
365
  className: "mejiro-reader-scroll-track",
500
366
  style: { display: "flex", flexDirection: "column", gap: pageGap },
501
- children: pages.map((result, i) => /* @__PURE__ */ jsxs6(
367
+ children: pages.map((result, i) => /* @__PURE__ */ jsxs4(
502
368
  "div",
503
369
  {
504
370
  ref: (el) => {
@@ -508,12 +374,12 @@ function MejiroScrollView({
508
374
  className: "mejiro-reader-page",
509
375
  style: { width: pageWidth, height: pageHeight, flexShrink: 0 },
510
376
  children: [
511
- /* @__PURE__ */ jsx6("div", { className: "mejiro-reader-page-rule" }),
512
- /* @__PURE__ */ jsxs6("div", { className: "mejiro-reader-page-header", children: [
513
- /* @__PURE__ */ jsx6("span", { className: "mejiro-reader-page-header-title" }),
514
- /* @__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 })
515
381
  ] }),
516
- /* @__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(
517
383
  MejiroPageView,
518
384
  {
519
385
  result,
@@ -534,7 +400,7 @@ function MejiroScrollView({
534
400
 
535
401
  // src/MejiroSettingsPanel.tsx
536
402
  import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
537
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
403
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
538
404
  var DEFAULT_FONTS = [
539
405
  { value: "serif", label: "System Serif" },
540
406
  { value: "sans-serif", label: "System Sans" }
@@ -543,6 +409,14 @@ function fontLabelFromCss(css) {
543
409
  const first = css.split(",")[0]?.trim() ?? css;
544
410
  return first.replace(/^["']|["']$/g, "") || css;
545
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
+ }
546
420
  function MejiroSettingsPanel({
547
421
  open,
548
422
  settings,
@@ -555,13 +429,13 @@ function MejiroSettingsPanel({
555
429
  const patch = (next) => onChange({ ...settings, ...next });
556
430
  const currentFont = normalizeFontFamily3(settings.fontFamily);
557
431
  const fontOptions = fonts.some((f) => f.value === currentFont) ? fonts : [{ value: currentFont, label: fontLabelFromCss(currentFont) }, ...fonts];
558
- return /* @__PURE__ */ jsx7("div", { className: `mejiro-reader-settings-panel${open ? " is-open" : ""}`, children: /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-settings-inner", children: /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-settings-content", children: [
559
- /* @__PURE__ */ jsxs7("div", { className: "mejiro-reader-settings-group", children: [
560
- /* @__PURE__ */ jsx7("span", { className: "mejiro-reader-settings-group-title", children: messages.settingsFont }),
561
- /* @__PURE__ */ jsx7("div", { className: "mejiro-reader-control", children: /* @__PURE__ */ jsx7("select", { value: currentFont, onChange: (e) => patch({ fontFamily: e.target.value }), children: fontOptions.map((f) => /* @__PURE__ */ jsx7("option", { value: f.value, children: f.label }, f.value)) }) }),
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(
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,7 +519,14 @@ 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
  ] })
@@ -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,12 +563,12 @@ 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",
@@ -706,7 +594,7 @@ function MejiroImageOverlay({
706
594
  }
707
595
 
708
596
  // src/MejiroSelectionLayer.tsx
709
- import { jsx as jsx9 } from "react/jsx-runtime";
597
+ import { jsx as jsx10 } from "react/jsx-runtime";
710
598
  function MejiroSelectionLayer({
711
599
  rects,
712
600
  side,
@@ -717,7 +605,7 @@ function MejiroSelectionLayer({
717
605
  }) {
718
606
  const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
719
607
  const rectClass = rectClassName ?? "mejiro-selection-rect";
720
- return /* @__PURE__ */ jsx9(
608
+ return /* @__PURE__ */ jsx10(
721
609
  "div",
722
610
  {
723
611
  className: wrapperClass,
@@ -730,7 +618,7 @@ function MejiroSelectionLayer({
730
618
  children: rects.map((r, i) => {
731
619
  if (r.side !== side) return null;
732
620
  const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
733
- return /* @__PURE__ */ jsx9(
621
+ return /* @__PURE__ */ jsx10(
734
622
  "div",
735
623
  {
736
624
  className: rectClass,
@@ -750,11 +638,11 @@ function MejiroSelectionLayer({
750
638
  }
751
639
 
752
640
  // src/MejiroSpread.tsx
753
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
641
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
754
642
  function defaultHeader(data) {
755
- return /* @__PURE__ */ jsxs9("div", { className: "mejiro-reader-page-header", children: [
756
- /* @__PURE__ */ jsx10("span", { className: "mejiro-reader-page-header-title", children: data.title ?? "" }),
757
- /* @__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) : "" })
758
646
  ] });
759
647
  }
760
648
  function MejiroSpread({
@@ -869,16 +757,16 @@ function MejiroSpread({
869
757
  height: pageHeight
870
758
  };
871
759
  if (isRight && hasImages) pageStyle.overflow = "visible";
872
- return /* @__PURE__ */ jsxs9(
760
+ return /* @__PURE__ */ jsxs7(
873
761
  "div",
874
762
  {
875
763
  className: `mejiro-reader-page mejiro-reader-page--${side}`,
876
764
  style: pageStyle,
877
765
  children: [
878
- /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-page-rule" }),
766
+ /* @__PURE__ */ jsx11("div", { className: "mejiro-reader-page-rule" }),
879
767
  renderPageHeader ? renderPageHeader(side, header) : defaultHeader(header),
880
- /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsxs9("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: [
881
- /* @__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(
882
770
  MejiroPageView,
883
771
  {
884
772
  result,
@@ -890,9 +778,9 @@ function MejiroSpread({
890
778
  },
891
779
  pageKey
892
780
  ),
893
- selectionRects && selectionRects.length > 0 && /* @__PURE__ */ jsx10(MejiroSelectionLayer, { rects: selectionRects, side })
781
+ selectionRects && selectionRects.length > 0 && /* @__PURE__ */ jsx11(MejiroSelectionLayer, { rects: selectionRects, side })
894
782
  ] }) }),
895
- isRight && images.map((item) => /* @__PURE__ */ jsx10(
783
+ isRight && images.map((item) => /* @__PURE__ */ jsx11(
896
784
  MejiroImageOverlay,
897
785
  {
898
786
  rect: item.rect,
@@ -919,7 +807,7 @@ function MejiroSpread({
919
807
  if (selectionEnabled) handlePointerUp(e);
920
808
  }
921
809
  const useCombined = selectionEnabled || gestureEnabled;
922
- return /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs9(
810
+ return /* @__PURE__ */ jsx11("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs7(
923
811
  "div",
924
812
  {
925
813
  className: `mejiro-reader-spread${turning ? " is-turning" : ""}${singlePage ? " mejiro-reader-spread--single" : ""}`,
@@ -930,7 +818,7 @@ function MejiroSpread({
930
818
  children: [
931
819
  renderPage("right"),
932
820
  !singlePage && renderPage("left"),
933
- /* @__PURE__ */ jsx10(
821
+ /* @__PURE__ */ jsx11(
934
822
  "button",
935
823
  {
936
824
  type: "button",
@@ -939,7 +827,7 @@ function MejiroSpread({
939
827
  onClick: onPrev
940
828
  }
941
829
  ),
942
- /* @__PURE__ */ jsx10(
830
+ /* @__PURE__ */ jsx11(
943
831
  "button",
944
832
  {
945
833
  type: "button",
@@ -956,7 +844,7 @@ function MejiroSpread({
956
844
 
957
845
  // src/MejiroStats.tsx
958
846
  import { estimateReadingTime, formatReadingTime } from "@libraz/mejiro/book";
959
- import { jsx as jsx11 } from "react/jsx-runtime";
847
+ import { jsx as jsx12 } from "react/jsx-runtime";
960
848
  function MejiroStats({
961
849
  chapter,
962
850
  totalPages,
@@ -966,7 +854,7 @@ function MejiroStats({
966
854
  cpm = 600,
967
855
  readingTimeLocale = "ja"
968
856
  }) {
969
- if (!chapter) return /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-stats" });
857
+ if (!chapter) return /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-stats" });
970
858
  const totalChars = chapter.paragraphs.reduce((s, p) => s + p.text.length, 0);
971
859
  const totalRuby = chapter.paragraphs.reduce(
972
860
  (s, p) => s + p.inlineAnnotations.filter((a) => a.kind === "ruby").length,
@@ -981,7 +869,7 @@ function MejiroStats({
981
869
  fontLabel || null,
982
870
  `${elapsedMs.toFixed(0)}ms`
983
871
  ];
984
- 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(" / ") });
985
873
  }
986
874
 
987
875
  // src/useChapterLayout.ts
@@ -1458,6 +1346,7 @@ function useSpread(layout, options = {}) {
1458
1346
  spreadIdxRef.current = spreadIdx;
1459
1347
  const turnTimerRef = useRef8(null);
1460
1348
  const layoutGenerationRef = useRef8(0);
1349
+ const lastEmittedRef = useRef8(null);
1461
1350
  const totalPages = layout?.totalPages ?? 0;
1462
1351
  const totalSpreads = Math.max(1, Math.ceil(totalPages / 2));
1463
1352
  const refresh = useCallback5(() => {
@@ -1484,7 +1373,15 @@ function useSpread(layout, options = {}) {
1484
1373
  useEffect5(() => {
1485
1374
  if (!layout) return;
1486
1375
  setSpread(layout.getSpread(spreadIdx));
1487
- 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 };
1488
1385
  }, [layout, spreadIdx]);
1489
1386
  const goTo = useCallback5(
1490
1387
  (index) => {
@@ -1551,12 +1448,12 @@ function useSpread(layout, options = {}) {
1551
1448
  }
1552
1449
 
1553
1450
  // src/MejiroReader.tsx
1554
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1451
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
1555
1452
  function MejiroReaderInner(props, ref) {
1556
1453
  const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
1557
1454
  const dialectProp = "dialect" in props ? props.dialect : void 0;
1558
1455
  const {
1559
- options = DEFAULT_BOOK_OPTIONS,
1456
+ options: optionsProp,
1560
1457
  pageGeometry: pageGeometryProp,
1561
1458
  fonts,
1562
1459
  epub: epubProp,
@@ -1601,6 +1498,7 @@ function MejiroReaderInner(props, ref) {
1601
1498
  const fileRef = useRef9(null);
1602
1499
  const [settingsOpen, setSettingsOpen] = useState7(false);
1603
1500
  const [chapterState, setChapterState] = useState7(chapterProp ?? 0);
1501
+ const chapterIsUncontrolled = chapterProp == null;
1604
1502
  const chapter = chapterProp ?? chapterState;
1605
1503
  const [chromeHidden, setChromeHidden] = useState7(false);
1606
1504
  const [autoSingle, setAutoSingle] = useState7(false);
@@ -1645,7 +1543,19 @@ function MejiroReaderInner(props, ref) {
1645
1543
  const aspect = resolvedGeometry?.aspect ?? DEFAULT_PAGE_GEOMETRY.aspect;
1646
1544
  return `${columns} / ${aspect}`;
1647
1545
  }, [effectiveSingle, resolvedGeometry]);
1648
- const { book, options: bookOptions, setOptions } = useMejiroBook(options);
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]);
1649
1559
  const synthesizedEpub = useMemo4(() => {
1650
1560
  if (manuscriptProp === void 0) return null;
1651
1561
  return manuscriptToEpubBook(manuscriptProp, { dialect: dialectProp });
@@ -1752,11 +1662,11 @@ function MejiroReaderInner(props, ref) {
1752
1662
  onLoadRef.current = onLoad;
1753
1663
  useEffect6(() => {
1754
1664
  if (!controlled) return;
1755
- if (chapterProp == null) setChapterState(0);
1665
+ if (chapterIsUncontrolled) setChapterState(0);
1756
1666
  clearImagesRef.current();
1757
1667
  book.clearCache();
1758
- if (epubProp) onLoadRef.current?.(epubProp);
1759
- }, [controlled, epubProp, book, chapterProp]);
1668
+ if (e) onLoadRef.current?.(e);
1669
+ }, [controlled, e, book, chapterIsUncontrolled]);
1760
1670
  const spreadGoToRef = useRef9(spreadCtx.goTo);
1761
1671
  spreadGoToRef.current = spreadCtx.goTo;
1762
1672
  useEffect6(() => {
@@ -1950,17 +1860,17 @@ function MejiroReaderInner(props, ref) {
1950
1860
  }, [themeOverride, pageGeometryProp, fit, surfaceAspect]);
1951
1861
  const resolvedMessages = useI18n({ locale, messages });
1952
1862
  const effectiveSubtitle = subtitle ?? resolvedMessages.logoSubtitle;
1953
- const defaultLogo = /* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-logo", children: [
1954
- /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-logo-mark", children: title }),
1955
- 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 })
1956
1866
  ] });
1957
- const header = enableHeader ? /* @__PURE__ */ jsxs10("header", { className: "mejiro-reader-header", children: [
1958
- /* @__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: [
1959
1869
  logo === void 0 ? defaultLogo : logo,
1960
- showChapterSelect && /* @__PURE__ */ jsx12(MejiroChapterNav, { epub: e, chapter, onChange: onChapter })
1870
+ showChapterSelect && /* @__PURE__ */ jsx13(MejiroChapterNav, { epub: e, chapter, onChange: onChapter })
1961
1871
  ] }),
1962
- /* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-header-actions", children: [
1963
- enableStats && /* @__PURE__ */ jsx12(
1872
+ /* @__PURE__ */ jsxs8("div", { className: "mejiro-reader-header-actions", children: [
1873
+ enableStats && /* @__PURE__ */ jsx13(
1964
1874
  MejiroStats,
1965
1875
  {
1966
1876
  chapter: e?.chapters[chapter] ?? null,
@@ -1969,7 +1879,7 @@ function MejiroReaderInner(props, ref) {
1969
1879
  fontLabel
1970
1880
  }
1971
1881
  ),
1972
- enableDropZone && /* @__PURE__ */ jsx12(
1882
+ enableDropZone && /* @__PURE__ */ jsx13(
1973
1883
  "button",
1974
1884
  {
1975
1885
  type: "button",
@@ -1978,7 +1888,7 @@ function MejiroReaderInner(props, ref) {
1978
1888
  children: resolvedMessages.openButton
1979
1889
  }
1980
1890
  ),
1981
- enableImageOverlay && e && /* @__PURE__ */ jsx12(
1891
+ enableImageOverlay && e && /* @__PURE__ */ jsx13(
1982
1892
  "button",
1983
1893
  {
1984
1894
  type: "button",
@@ -1987,7 +1897,7 @@ function MejiroReaderInner(props, ref) {
1987
1897
  children: resolvedMessages.imageButton
1988
1898
  }
1989
1899
  ),
1990
- enableSettings && /* @__PURE__ */ jsxs10(
1900
+ enableSettings && /* @__PURE__ */ jsxs8(
1991
1901
  "button",
1992
1902
  {
1993
1903
  type: "button",
@@ -1995,7 +1905,7 @@ function MejiroReaderInner(props, ref) {
1995
1905
  onClick: () => setSettingsOpen((v) => !v),
1996
1906
  children: [
1997
1907
  resolvedMessages.settingsButton,
1998
- /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-btn-arrow", children: "\u25BE" })
1908
+ /* @__PURE__ */ jsx13("span", { className: "mejiro-reader-btn-arrow", children: "\u25BE" })
1999
1909
  ]
2000
1910
  }
2001
1911
  )
@@ -2007,7 +1917,7 @@ function MejiroReaderInner(props, ref) {
2007
1917
  const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
2008
1918
  const showRightNum = pageNumbers === "both" || pageNumbers === "right";
2009
1919
  const showLeftNum = pageNumbers === "both" || pageNumbers === "left";
2010
- return /* @__PURE__ */ jsx12(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs10(
1920
+ return /* @__PURE__ */ jsx13(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs8(
2011
1921
  "div",
2012
1922
  {
2013
1923
  className: `mejiro-reader${chromeHidden ? " mejiro-reader--chrome-hidden" : ""}${fit === "width" ? " mejiro-reader--fit-width" : ""}`,
@@ -2015,12 +1925,12 @@ function MejiroReaderInner(props, ref) {
2015
1925
  style: themeStyle,
2016
1926
  children: [
2017
1927
  header,
2018
- enableSettings && (renderSettings ? /* @__PURE__ */ jsx12("div", { className: `mejiro-reader-settings-panel${settingsOpen ? " is-open" : ""}`, children: /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-settings-inner", children: /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-settings-content", children: renderSettings({
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({
2019
1929
  settings: editable,
2020
1930
  update: setOptions,
2021
1931
  open: settingsOpen,
2022
1932
  toggle: () => setSettingsOpen((v) => !v)
2023
- }) }) }) }) : /* @__PURE__ */ jsx12(
1933
+ }) }) }) }) : /* @__PURE__ */ jsx13(
2024
1934
  MejiroSettingsPanel,
2025
1935
  {
2026
1936
  open: settingsOpen,
@@ -2029,13 +1939,13 @@ function MejiroReaderInner(props, ref) {
2029
1939
  onChange: setOptions
2030
1940
  }
2031
1941
  )),
2032
- /* @__PURE__ */ jsxs10("div", { className: `mejiro-reader-body${showChapterPanel ? " has-chapter-panel" : ""}`, children: [
2033
- showChapterPanel && /* @__PURE__ */ jsx12(MejiroChapterNav, { epub: e, chapter, onChange: onChapter, variant: "panel" }),
2034
- /* @__PURE__ */ jsxs10("div", { ref: surfaceRef, className: "mejiro-reader-surface", children: [
2035
- !(e || epubCtx.loading) && enableDropZone && /* @__PURE__ */ jsx12(MejiroDropZone, { onFile: (f) => void epubCtx.loadFile(f) }),
2036
- epubCtx.loading && /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-loading", children: resolvedMessages.loading }),
2037
- !layoutReady && fallback && /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-fallback", children: fallback }),
2038
- layoutReady && mode === "scroll" && layoutCtx.layout && /* @__PURE__ */ jsx12(
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(
2039
1949
  MejiroScrollView,
2040
1950
  {
2041
1951
  layout: layoutCtx.layout,
@@ -2046,13 +1956,14 @@ function MejiroReaderInner(props, ref) {
2046
1956
  fontSize: bookOptions.fontSize,
2047
1957
  lineSpacing: bookOptions.lineSpacing,
2048
1958
  scrollToPage: spreadCtx.spreadIdx * 2,
2049
- onVisiblePageChange: (pageIdx) => {
1959
+ onVisiblePageChange: (pageIdx, source) => {
1960
+ if (source === "programmatic") return;
2050
1961
  const target = Math.floor(pageIdx / 2);
2051
- if (target !== spreadCtx.spreadIdx) spreadGoToRef.current(target);
1962
+ if (target !== spreadCtx.spreadIdx) spreadCtx.setSpread(target);
2052
1963
  }
2053
1964
  }
2054
1965
  ),
2055
- layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx12(
1966
+ layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx13(
2056
1967
  MejiroSpread,
2057
1968
  {
2058
1969
  singlePage: effectiveSingle,
@@ -2075,7 +1986,7 @@ function MejiroReaderInner(props, ref) {
2075
1986
  pageNumber: showLeft && showLeftNum ? leftPage : null
2076
1987
  },
2077
1988
  images: imageCtx.currentImages,
2078
- indicator: enablePageIndicator ? /* @__PURE__ */ jsx12(
1989
+ indicator: enablePageIndicator ? /* @__PURE__ */ jsx13(
2079
1990
  MejiroPageIndicator,
2080
1991
  {
2081
1992
  current: spreadCtx.spreadIdx + 1,
@@ -2093,7 +2004,7 @@ function MejiroReaderInner(props, ref) {
2093
2004
  )
2094
2005
  ] })
2095
2006
  ] }),
2096
- /* @__PURE__ */ jsx12(
2007
+ /* @__PURE__ */ jsx13(
2097
2008
  "input",
2098
2009
  {
2099
2010
  ref: fileRef,
@@ -2106,7 +2017,7 @@ function MejiroReaderInner(props, ref) {
2106
2017
  }
2107
2018
  }
2108
2019
  ),
2109
- /* @__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, {
2110
2021
  spread: spreadCtx.spreadIdx + 1,
2111
2022
  total: spreadCtx.totalSpreads
2112
2023
  }) : "" })
@@ -2118,7 +2029,7 @@ var MejiroReader = forwardRef(MejiroReaderInner);
2118
2029
  MejiroReader.displayName = "MejiroReader";
2119
2030
 
2120
2031
  // src/MejiroEditor.tsx
2121
- 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";
2122
2033
  function cloneBook(book) {
2123
2034
  return {
2124
2035
  ...book,
@@ -2194,8 +2105,11 @@ function MejiroEditor({
2194
2105
  const book = editor?.book ?? null;
2195
2106
  const chapter = book?.chapters[selection.chapter] ?? null;
2196
2107
  const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
2197
- const previewBook = book ? cloneBook(book) : null;
2198
- 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]);
2199
2113
  useEffect7(() => {
2200
2114
  onLoadRef.current = onLoad;
2201
2115
  }, [onLoad]);
@@ -2289,7 +2203,7 @@ function MejiroEditor({
2289
2203
  setRevision((value) => value + 1);
2290
2204
  }
2291
2205
  function applyRuby() {
2292
- if (!(editor && paragraph && rubyText.trim())) return;
2206
+ if (!(editor && paragraph && rubyText.trim()) || textDirty) return;
2293
2207
  const len = [...text].length;
2294
2208
  const start = Math.max(0, Math.min(rubyStart, len));
2295
2209
  const end = Math.max(start + 1, Math.min(rubyEnd, len));
@@ -2340,9 +2254,9 @@ function MejiroEditor({
2340
2254
  a.click();
2341
2255
  URL.revokeObjectURL(url);
2342
2256
  }
2343
- return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor", children: [
2344
- /* @__PURE__ */ jsxs11("main", { className: "mejiro-editor-preview", children: [
2345
- 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(
2346
2260
  MejiroReader,
2347
2261
  {
2348
2262
  epub: previewBook,
@@ -2352,38 +2266,38 @@ function MejiroEditor({
2352
2266
  enableImageOverlay: false,
2353
2267
  enableSurfaceTap: false
2354
2268
  }
2355
- ) : /* @__PURE__ */ jsx13(MejiroDropZone, { onFile: (file) => void loadFile(file) }),
2356
- loading && /* @__PURE__ */ jsx13("div", { className: "mejiro-editor-loading", children: messages.loading }),
2357
- 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 })
2358
2272
  ] }),
2359
- /* @__PURE__ */ jsxs11("aside", { className: "mejiro-editor-panel", children: [
2360
- /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-head", children: [
2361
- /* @__PURE__ */ jsx13("span", { children: messages.editorTitle }),
2362
- /* @__PURE__ */ jsx13("strong", { children: editor?.title ?? messages.editorNoBookLoaded }),
2363
- 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 })
2364
2278
  ] }),
2365
- book && /* @__PURE__ */ jsxs11(Fragment4, { children: [
2366
- /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2367
- /* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorParagraphs }),
2368
- /* @__PURE__ */ jsx13("div", { className: "mejiro-editor-paragraphs", children: book.chapters.map(
2369
- (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(
2370
2284
  "button",
2371
2285
  {
2372
2286
  type: "button",
2373
2287
  className: selection.chapter === ci && selection.paragraph === pi ? "is-active" : "",
2374
2288
  onClick: () => selectParagraph(ci, pi),
2375
2289
  children: [
2376
- /* @__PURE__ */ jsx13("span", { children: ch.title ?? format(messages.chapterN, { n: ci + 1 }) }),
2377
- /* @__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) })
2378
2292
  ]
2379
2293
  },
2380
2294
  `${ch.href}-${block.id}`
2381
2295
  ))
2382
2296
  ) })
2383
2297
  ] }),
2384
- enableProofread && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2385
- /* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorProofread }),
2386
- /* @__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(
2387
2301
  "textarea",
2388
2302
  {
2389
2303
  ref: textareaRef,
@@ -2392,17 +2306,17 @@ function MejiroEditor({
2392
2306
  onSelect: captureRubyRange
2393
2307
  }
2394
2308
  ),
2395
- /* @__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 })
2396
2310
  ] }),
2397
- enableRuby && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2398
- /* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorRuby }),
2399
- /* @__PURE__ */ jsx13("p", { className: "mejiro-editor-hint", children: messages.editorRubyHint }),
2400
- /* @__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, {
2401
2315
  start: rubyStart,
2402
2316
  end: rubyEnd,
2403
2317
  count: Math.max(0, rubyEnd - rubyStart)
2404
2318
  }) }),
2405
- /* @__PURE__ */ jsx13(
2319
+ /* @__PURE__ */ jsx14(
2406
2320
  "input",
2407
2321
  {
2408
2322
  value: rubyText,
@@ -2410,12 +2324,21 @@ function MejiroEditor({
2410
2324
  onChange: (event) => setRubyText(event.target.value)
2411
2325
  }
2412
2326
  ),
2413
- /* @__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
+ )
2414
2337
  ] }),
2415
- enableImages && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2416
- /* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorImages }),
2417
- /* @__PURE__ */ jsx13("button", { type: "button", onClick: () => imageInputRef.current?.click(), children: messages.editorInsertImageAfterParagraph }),
2418
- /* @__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(
2419
2342
  "input",
2420
2343
  {
2421
2344
  ref: imageInputRef,
@@ -2429,7 +2352,7 @@ function MejiroEditor({
2429
2352
  }
2430
2353
  )
2431
2354
  ] }),
2432
- enableExport && /* @__PURE__ */ jsx13(
2355
+ enableExport && /* @__PURE__ */ jsx14(
2433
2356
  "button",
2434
2357
  {
2435
2358
  type: "button",
@@ -2454,35 +2377,189 @@ function paragraphBlockId(chapter, paragraphIndex) {
2454
2377
 
2455
2378
  // src/MejiroManuscriptEditor.tsx
2456
2379
  import { EpubProject } from "@libraz/mejiro/epub";
2457
- 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
+ }
2458
2496
 
2459
2497
  // src/useManuscriptDraft.ts
2460
- 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";
2461
2499
  var DEFAULT_DELAY = 800;
2462
- function defaultChapter(index) {
2463
- 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) };
2464
2502
  }
2465
2503
  function useManuscriptDraft(options = {}) {
2466
- const { onAutosave, autosaveDelay = DEFAULT_DELAY } = options;
2467
- const [chapters, setChaptersState] = useState9(
2468
- () => 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)]
2469
2518
  );
2470
- const [selected, setSelectedState] = useState9(0);
2471
- const saveRef = useRef11(onAutosave);
2519
+ const [selected, setSelectedState] = useState10(0);
2520
+ const [autosaveError, setAutosaveError] = useState10(null);
2521
+ const saveRef = useRef12(onAutosave);
2472
2522
  saveRef.current = onAutosave;
2473
- 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
+ }, []);
2474
2540
  useEffect8(() => {
2475
2541
  if (!didMountRef.current) {
2476
2542
  didMountRef.current = true;
2477
2543
  return void 0;
2478
2544
  }
2479
- const callback = saveRef.current;
2480
- if (!callback) return void 0;
2545
+ if (!saveRef.current) return void 0;
2546
+ dirtyRef.current = true;
2547
+ setAutosaveError(null);
2481
2548
  const timer = setTimeout(() => {
2482
- void callback(chapters);
2549
+ flushAutosave();
2483
2550
  }, autosaveDelay);
2484
2551
  return () => clearTimeout(timer);
2485
- }, [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]);
2486
2563
  const setSelected = useCallback8(
2487
2564
  (index) => {
2488
2565
  setSelectedState(() => Math.max(0, Math.min(index, chapters.length - 1)));
@@ -2491,7 +2568,7 @@ function useManuscriptDraft(options = {}) {
2491
2568
  );
2492
2569
  const setChapters = useCallback8(
2493
2570
  (next) => {
2494
- const normalized = next.length ? next : [defaultChapter(0)];
2571
+ const normalized = next.length ? next : [defaultChapter(0, titleForRef.current, bodyForRef.current)];
2495
2572
  setChaptersState(normalized);
2496
2573
  setSelectedState((current) => {
2497
2574
  const selectedId = chapters[current]?.id;
@@ -2508,7 +2585,7 @@ function useManuscriptDraft(options = {}) {
2508
2585
  }, []);
2509
2586
  const addChapter = useCallback8((chapter = {}) => {
2510
2587
  setChaptersState((current) => {
2511
- const generated = defaultChapter(current.length);
2588
+ const generated = defaultChapter(current.length, titleForRef.current, bodyForRef.current);
2512
2589
  const next = [
2513
2590
  ...current,
2514
2591
  {
@@ -2557,12 +2634,14 @@ function useManuscriptDraft(options = {}) {
2557
2634
  patchChapter,
2558
2635
  addChapter,
2559
2636
  removeChapter,
2560
- reorderChapters
2637
+ reorderChapters,
2638
+ autosaveError,
2639
+ flushAutosave
2561
2640
  };
2562
2641
  }
2563
2642
 
2564
2643
  // src/MejiroManuscriptEditor.tsx
2565
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2644
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
2566
2645
  function downloadEpub(buffer, title) {
2567
2646
  const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2568
2647
  const a = document.createElement("a");
@@ -2614,14 +2693,14 @@ function MejiroManuscriptEditor({
2614
2693
  onExport
2615
2694
  }) {
2616
2695
  const messages = useI18n();
2617
- const coverInputRef = useRef12(null);
2618
- const bodyTextareaRef = useRef12(null);
2696
+ const coverInputRef = useRef13(null);
2697
+ const bodyTextareaRef = useRef13(null);
2619
2698
  const titleControlled = onTitleChange !== void 0;
2620
2699
  const authorControlled = onAuthorChange !== void 0;
2621
2700
  const coverControlled = onCoverChange !== void 0;
2622
- const [titleState, setTitleState] = useState10(titleProp ?? messages.manuscriptDefaultTitle);
2623
- const [authorState, setAuthorState] = useState10(authorProp ?? "");
2624
- 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);
2625
2704
  const title = titleControlled ? titleProp ?? "" : titleState;
2626
2705
  const author = authorControlled ? authorProp ?? "" : authorState;
2627
2706
  const cover = coverControlled ? coverProp ?? null : coverState;
@@ -2658,7 +2737,10 @@ function MejiroManuscriptEditor({
2658
2737
  const draft = useManuscriptDraft({
2659
2738
  initialChapters: initialChapters?.length ? initialChapters : [defaultChapter2(messages)],
2660
2739
  onAutosave,
2661
- 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 })
2662
2744
  });
2663
2745
  const {
2664
2746
  chapters,
@@ -2670,7 +2752,7 @@ function MejiroManuscriptEditor({
2670
2752
  reorderChapters
2671
2753
  } = draft;
2672
2754
  const current = chapters[selected] ?? chapters[0];
2673
- const [draggingIndex, setDraggingIndex] = useState10(null);
2755
+ const [draggingIndex, setDraggingIndex] = useState11(null);
2674
2756
  function wrapSelection(open, close) {
2675
2757
  const el = bodyTextareaRef.current;
2676
2758
  if (!el) return;
@@ -2708,8 +2790,8 @@ function MejiroManuscriptEditor({
2708
2790
  onExport?.(buffer);
2709
2791
  downloadEpub(buffer, title);
2710
2792
  }, [assetResolver, author, chapters, cover, messages.untitled, onExport, title]);
2711
- return /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor mejiro-manuscript-editor", children: [
2712
- /* @__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(
2713
2795
  MejiroReader,
2714
2796
  {
2715
2797
  subtitle: messages.manuscriptPreviewSubtitle,
@@ -2727,18 +2809,19 @@ function MejiroManuscriptEditor({
2727
2809
  enableImageOverlay: false
2728
2810
  }
2729
2811
  ) }),
2730
- /* @__PURE__ */ jsxs12("aside", { className: "mejiro-editor-panel", children: [
2731
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-head", children: [
2732
- /* @__PURE__ */ jsx14("span", { children: messages.manuscriptTitle }),
2733
- /* @__PURE__ */ jsx14("strong", { children: title }),
2734
- /* @__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 })
2735
2817
  ] }),
2736
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
2737
- /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptMetadata }),
2738
- /* @__PURE__ */ jsx14("input", { value: title, onChange: (event) => setTitle(event.target.value) }),
2739
- /* @__PURE__ */ jsx14("input", { value: author, onChange: (event) => setAuthor(event.target.value) }),
2740
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => coverInputRef.current?.click(), children: cover ? cover.name : messages.manuscriptChooseCoverImage }),
2741
- /* @__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(
2742
2825
  "input",
2743
2826
  {
2744
2827
  ref: coverInputRef,
@@ -2749,9 +2832,9 @@ function MejiroManuscriptEditor({
2749
2832
  }
2750
2833
  )
2751
2834
  ] }),
2752
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
2753
- /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptChapters }),
2754
- /* @__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(
2755
2838
  "button",
2756
2839
  {
2757
2840
  type: "button",
@@ -2780,14 +2863,14 @@ function MejiroManuscriptEditor({
2780
2863
  },
2781
2864
  "data-dragging": draggingIndex === index ? "" : void 0,
2782
2865
  children: [
2783
- /* @__PURE__ */ jsx14("span", { children: format(messages.chapterN, { n: index + 1 }) }),
2784
- /* @__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 })
2785
2868
  ]
2786
2869
  },
2787
2870
  chapter.id
2788
2871
  )) }),
2789
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-grid", children: [
2790
- /* @__PURE__ */ jsx14(
2872
+ /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-grid", children: [
2873
+ /* @__PURE__ */ jsx16(
2791
2874
  "button",
2792
2875
  {
2793
2876
  type: "button",
@@ -2799,138 +2882,41 @@ function MejiroManuscriptEditor({
2799
2882
  children: messages.manuscriptAddChapter
2800
2883
  }
2801
2884
  ),
2802
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => removeChapter(selected), children: messages.manuscriptRemove })
2885
+ /* @__PURE__ */ jsx16("button", { type: "button", onClick: () => removeChapter(selected), children: messages.manuscriptRemove })
2803
2886
  ] })
2804
2887
  ] }),
2805
- current && /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
2806
- /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptDraft }),
2807
- /* @__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(
2808
2891
  "input",
2809
2892
  {
2810
2893
  value: current.title,
2811
2894
  onChange: (event) => patchChapter(selected, { title: event.target.value })
2812
2895
  }
2813
2896
  ),
2814
- /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-grid mejiro-editor-notation", children: [
2815
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("\u300A\u300A", "\u300B\u300B"), children: messages.manuscriptEmphasisDots }),
2816
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("\u3014", "\u3015"), children: messages.manuscriptTcy }),
2817
- /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => wrapSelection("*", "*"), children: messages.manuscriptEm }),
2818
- /* @__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 })
2819
2902
  ] }),
2820
- /* @__PURE__ */ jsx14(
2821
- "textarea",
2903
+ /* @__PURE__ */ jsx16(
2904
+ MejiroNotationHighlighter,
2822
2905
  {
2823
2906
  ref: bodyTextareaRef,
2824
2907
  className: "mejiro-editor-manuscript",
2825
2908
  value: current.body,
2826
- onChange: (event) => patchChapter(selected, { body: event.target.value })
2909
+ onChange: (body) => patchChapter(selected, { body })
2827
2910
  }
2828
2911
  )
2829
2912
  ] }),
2830
- /* @__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 })
2831
2914
  ] })
2832
2915
  ] });
2833
2916
  }
2834
2917
 
2835
- // src/MejiroNotationHighlighter.tsx
2836
- import { tokenizeManuscriptSource } from "@libraz/mejiro";
2837
- import {
2838
- forwardRef as forwardRef2,
2839
- useImperativeHandle as useImperativeHandle2,
2840
- useMemo as useMemo5,
2841
- useRef as useRef13
2842
- } from "react";
2843
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2844
- var MejiroNotationHighlighter = forwardRef2(function MejiroNotationHighlighter2({
2845
- value,
2846
- onChange,
2847
- dialect = "mejiro",
2848
- wrapperClassName,
2849
- wrapperStyle,
2850
- className,
2851
- style,
2852
- onScroll,
2853
- ...rest
2854
- }, ref) {
2855
- const textareaRef = useRef13(null);
2856
- const overlayRef = useRef13(null);
2857
- useImperativeHandle2(ref, () => textareaRef.current, []);
2858
- const segments = useMemo5(() => buildSegments(value, dialect), [value, dialect]);
2859
- return /* @__PURE__ */ jsxs13(
2860
- "div",
2861
- {
2862
- className: `mejiro-notation-highlighter${wrapperClassName ? ` ${wrapperClassName}` : ""}`,
2863
- style: wrapperStyle,
2864
- children: [
2865
- /* @__PURE__ */ jsxs13(
2866
- "div",
2867
- {
2868
- ref: overlayRef,
2869
- className: "mejiro-notation-overlay",
2870
- "aria-hidden": "true",
2871
- children: [
2872
- segments.map((segment) => /* @__PURE__ */ jsx15(
2873
- "span",
2874
- {
2875
- className: segment.kind ? `mejiro-notation-token mejiro-notation-${segment.kind}` : void 0,
2876
- "data-token": segment.kind,
2877
- children: segment.text
2878
- },
2879
- segment.key
2880
- )),
2881
- "\u200B"
2882
- ]
2883
- }
2884
- ),
2885
- /* @__PURE__ */ jsx15(
2886
- "textarea",
2887
- {
2888
- ...rest,
2889
- ref: textareaRef,
2890
- className: `mejiro-notation-textarea${className ? ` ${className}` : ""}`,
2891
- style,
2892
- value,
2893
- onChange: (event) => onChange(event.target.value),
2894
- onScroll: (event) => {
2895
- if (overlayRef.current) {
2896
- overlayRef.current.scrollTop = event.currentTarget.scrollTop;
2897
- overlayRef.current.scrollLeft = event.currentTarget.scrollLeft;
2898
- }
2899
- onScroll?.(event);
2900
- },
2901
- spellCheck: false
2902
- }
2903
- )
2904
- ]
2905
- }
2906
- );
2907
- });
2908
- function buildSegments(text, dialect) {
2909
- const tokens = tokenizeManuscriptSource(text, dialect);
2910
- const segments = [];
2911
- let cursor = 0;
2912
- for (const [i, token] of tokens.entries()) {
2913
- if (token.start > cursor) {
2914
- segments.push({ key: `t-${i}-pre`, text: text.slice(cursor, token.start) });
2915
- }
2916
- segments.push({
2917
- key: `t-${i}`,
2918
- text: text.slice(token.start, token.end),
2919
- kind: token.kind
2920
- });
2921
- cursor = token.end;
2922
- }
2923
- if (cursor < text.length) {
2924
- segments.push({ key: "tail", text: text.slice(cursor) });
2925
- }
2926
- if (segments.length === 0) {
2927
- segments.push({ key: "empty", text });
2928
- }
2929
- return segments;
2930
- }
2931
-
2932
2918
  // src/MejiroShelf.tsx
2933
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2919
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
2934
2920
  function MejiroShelf({
2935
2921
  volumes,
2936
2922
  currentId,
@@ -2939,26 +2925,26 @@ function MejiroShelf({
2939
2925
  }) {
2940
2926
  const messages = useI18n();
2941
2927
  const resolvedTitle = title ?? messages.shelfTitle;
2942
- return /* @__PURE__ */ jsxs14("section", { className: "mejiro-shelf", "aria-label": resolvedTitle, children: [
2943
- /* @__PURE__ */ jsx16("header", { className: "mejiro-shelf-header", children: /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-title", children: resolvedTitle }) }),
2944
- /* @__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(
2945
2931
  "button",
2946
2932
  {
2947
2933
  type: "button",
2948
2934
  className: `mejiro-shelf-card${v.id === currentId ? " is-active" : ""}`,
2949
2935
  onClick: () => onSelect?.(v),
2950
2936
  children: [
2951
- v.cover ? /* @__PURE__ */ jsx16(
2937
+ v.cover ? /* @__PURE__ */ jsx17(
2952
2938
  "span",
2953
2939
  {
2954
2940
  className: "mejiro-shelf-cover",
2955
2941
  style: { backgroundImage: `url(${JSON.stringify(v.cover)})` },
2956
2942
  "aria-hidden": "true"
2957
2943
  }
2958
- ) : /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-cover mejiro-shelf-cover--blank", "aria-hidden": "true" }),
2959
- /* @__PURE__ */ jsxs14("span", { className: "mejiro-shelf-meta", children: [
2960
- /* @__PURE__ */ jsx16("span", { className: "mejiro-shelf-label", children: v.label }),
2961
- 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 })
2962
2948
  ] })
2963
2949
  ]
2964
2950
  }
@@ -2967,8 +2953,8 @@ function MejiroShelf({
2967
2953
  }
2968
2954
 
2969
2955
  // src/MejiroToc.tsx
2970
- import { useMemo as useMemo6, useState as useState11 } from "react";
2971
- 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";
2972
2958
  function buildEntries(epub, messages) {
2973
2959
  return epub.chapters.map((ch, i) => {
2974
2960
  const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
@@ -2987,9 +2973,9 @@ function MejiroToc({
2987
2973
  const messages = useI18n();
2988
2974
  const resolvedTitle = title ?? messages.tocTitle;
2989
2975
  const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
2990
- const [query, setQuery] = useState11("");
2991
- const entries = useMemo6(() => buildEntries(epub, messages), [epub, messages]);
2992
- const filtered = useMemo6(() => {
2976
+ const [query, setQuery] = useState12("");
2977
+ const entries = useMemo7(() => buildEntries(epub, messages), [epub, messages]);
2978
+ const filtered = useMemo7(() => {
2993
2979
  if (!query) return entries;
2994
2980
  const needle = query.toLowerCase();
2995
2981
  return entries.filter(
@@ -2997,12 +2983,12 @@ function MejiroToc({
2997
2983
  );
2998
2984
  }, [entries, query]);
2999
2985
  const activeIndex = currentAnchor?.chapter ?? -1;
3000
- return /* @__PURE__ */ jsxs15("nav", { className: "mejiro-toc", "aria-label": resolvedTitle, children: [
3001
- /* @__PURE__ */ jsxs15("header", { className: "mejiro-toc-header", children: [
3002
- /* @__PURE__ */ jsx17("span", { className: "mejiro-toc-title", children: resolvedTitle }),
3003
- 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 })
3004
2990
  ] }),
3005
- searchable && /* @__PURE__ */ jsx17("div", { className: "mejiro-toc-search", children: /* @__PURE__ */ jsx17(
2991
+ searchable && /* @__PURE__ */ jsx18("div", { className: "mejiro-toc-search", children: /* @__PURE__ */ jsx18(
3006
2992
  "input",
3007
2993
  {
3008
2994
  type: "search",
@@ -3012,8 +2998,8 @@ function MejiroToc({
3012
2998
  "aria-label": resolvedSearchPlaceholder
3013
2999
  }
3014
3000
  ) }),
3015
- /* @__PURE__ */ jsx17("ol", { className: "mejiro-toc-list", children: filtered.map((entry) => /* @__PURE__ */ jsxs15("li", { className: "mejiro-toc-item", children: [
3016
- /* @__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(
3017
3003
  "button",
3018
3004
  {
3019
3005
  type: "button",
@@ -3021,58 +3007,37 @@ function MejiroToc({
3021
3007
  "aria-current": entry.index === activeIndex ? "true" : void 0,
3022
3008
  onClick: () => onSelect?.(entry.index),
3023
3009
  children: [
3024
- /* @__PURE__ */ jsx17("span", { className: "mejiro-toc-num", children: String(entry.index + 1).padStart(2, "0") }),
3025
- /* @__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 })
3026
3012
  ]
3027
3013
  }
3028
3014
  ),
3029
- 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)) })
3030
3016
  ] }, entry.index)) }),
3031
- 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 }) })
3032
3018
  ] });
3033
3019
  }
3034
3020
 
3035
3021
  // src/useAnnotations.ts
3036
- import { useCallback as useCallback10, useEffect as useEffect10, useRef as useRef14, useState as useState12 } from "react";
3037
- 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";
3038
3029
  function resolveDefaultStorage() {
3039
3030
  if (typeof globalThis === "undefined") return null;
3040
3031
  const w = globalThis.localStorage;
3041
3032
  return w ?? null;
3042
3033
  }
3043
- function parse(raw) {
3044
- if (!raw) return [];
3045
- try {
3046
- const parsed = JSON.parse(raw);
3047
- if (parsed && Array.isArray(parsed.annotations)) return parsed.annotations;
3048
- } catch {
3049
- }
3050
- return [];
3051
- }
3052
- function serialize(annotations) {
3053
- const payload = { version: STORAGE_VERSION, annotations: [...annotations] };
3054
- return JSON.stringify(payload);
3055
- }
3056
- function sort(annotations) {
3057
- return [...annotations].sort((a, b) => {
3058
- if (a.chapter !== b.chapter) return a.chapter - b.chapter;
3059
- if (a.start.paragraph !== b.start.paragraph) return a.start.paragraph - b.start.paragraph;
3060
- return a.start.charIndex - b.start.charIndex;
3061
- });
3062
- }
3063
- function makeId() {
3064
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
3065
- return crypto.randomUUID();
3066
- }
3067
- return `ann-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
3068
- }
3069
3034
  function useAnnotations(options) {
3070
3035
  const { key, throttleMs = 250 } = options;
3071
3036
  const storage = options.storage ?? resolveDefaultStorage();
3072
- const [annotations, setAnnotations] = useState12(() => {
3037
+ const [annotations, setAnnotations] = useState13(() => {
3073
3038
  if (!storage) return [];
3074
3039
  try {
3075
- return sort(parse(storage.getItem(key)));
3040
+ return sortAnnotations(parseAnnotations(storage.getItem(key)));
3076
3041
  } catch {
3077
3042
  return [];
3078
3043
  }
@@ -3083,7 +3048,7 @@ function useAnnotations(options) {
3083
3048
  return;
3084
3049
  }
3085
3050
  try {
3086
- setAnnotations(sort(parse(storage.getItem(key))));
3051
+ setAnnotations(sortAnnotations(parseAnnotations(storage.getItem(key))));
3087
3052
  } catch {
3088
3053
  setAnnotations([]);
3089
3054
  }
@@ -3103,7 +3068,7 @@ function useAnnotations(options) {
3103
3068
  if (timerRef.current) clearTimeout(timerRef.current);
3104
3069
  timerRef.current = setTimeout(() => {
3105
3070
  try {
3106
- storage.setItem(key, serialize(next));
3071
+ storage.setItem(key, serializeAnnotations(next));
3107
3072
  } catch {
3108
3073
  }
3109
3074
  }, throttleMs);
@@ -3116,11 +3081,11 @@ function useAnnotations(options) {
3116
3081
  (input) => {
3117
3082
  const annotation = {
3118
3083
  ...input,
3119
- id: input.id ?? makeId(),
3084
+ id: input.id ?? createAnnotationId(),
3120
3085
  createdAt: input.createdAt ?? Date.now()
3121
3086
  };
3122
3087
  setAnnotations((current) => {
3123
- const next = sort([...current, annotation]);
3088
+ const next = sortAnnotations([...current, annotation]);
3124
3089
  commit(next);
3125
3090
  return next;
3126
3091
  });
@@ -3148,7 +3113,7 @@ function useAnnotations(options) {
3148
3113
  return { ...annotation, ...patch, id };
3149
3114
  });
3150
3115
  if (!changed) return current;
3151
- const sorted = sort(next);
3116
+ const sorted = sortAnnotations(next);
3152
3117
  commit(sorted);
3153
3118
  return sorted;
3154
3119
  });
@@ -3173,14 +3138,14 @@ function useAnnotations(options) {
3173
3138
  import {
3174
3139
  EditableEpub as EditableEpub2
3175
3140
  } from "@libraz/mejiro/epub";
3176
- 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";
3177
3142
  function useEditableEpub(options = {}) {
3178
- const [editor, setEditor] = useState13(null);
3179
- const [loading, setLoading] = useState13(false);
3180
- const [exporting, setExporting] = useState13(false);
3181
- const [error, setError] = useState13(null);
3182
- const [revision, setRevision] = useState13(0);
3183
- 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({
3184
3149
  chapter: 0,
3185
3150
  paragraph: 0
3186
3151
  });
@@ -3268,7 +3233,10 @@ function useEditableEpub(options = {}) {
3268
3233
  }, [options.defaultUrl, loadUrl]);
3269
3234
  const book = editor?.book ?? null;
3270
3235
  const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
3271
- const previewBook = book ? cloneBook2(book) : null;
3236
+ const previewBook = useMemo8(() => {
3237
+ void revision;
3238
+ return book ? cloneBook2(book) : null;
3239
+ }, [book, revision]);
3272
3240
  const history = editor?.history ?? null;
3273
3241
  const setSelection = useCallback11(
3274
3242
  (nextSelection) => {
@@ -3391,20 +3359,22 @@ import {
3391
3359
  EpubProject as EpubProject2,
3392
3360
  parseEpub as parseEpub2
3393
3361
  } from "@libraz/mejiro/epub";
3394
- 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";
3395
3363
  function useEpubProject(options = {}) {
3396
- const [metadata, setMetadataState] = useState14({
3364
+ const defaultTitle = options.defaultChapterTitle;
3365
+ const defaultBody = options.defaultChapterBody;
3366
+ const [metadata, setMetadataState] = useState15({
3397
3367
  title: "\u65B0\u3057\u3044\u4F5C\u54C1",
3398
3368
  language: "ja",
3399
3369
  ...options.metadata
3400
3370
  });
3401
- const [chapters, setChaptersState] = useState14(
3402
- options.chapters?.length ? options.chapters : [defaultChapter3(0)]
3371
+ const [chapters, setChaptersState] = useState15(
3372
+ options.chapters?.length ? options.chapters : [defaultChapter3(0, defaultTitle, defaultBody)]
3403
3373
  );
3404
- const [selectedChapter, setSelectedChapterState] = useState14(0);
3405
- const [previewBook, setPreviewBook] = useState14(null);
3406
- const [previewError, setPreviewError] = useState14(null);
3407
- 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);
3408
3378
  const previewRequestIdRef = useRef16(0);
3409
3379
  const onPreviewRef = useRef16(options.onPreview);
3410
3380
  const onExportRef = useRef16(options.onExport);
@@ -3461,7 +3431,7 @@ function useEpubProject(options = {}) {
3461
3431
  }, []);
3462
3432
  const setChapters = useCallback12(
3463
3433
  (next) => {
3464
- const normalized = next.length ? next : [defaultChapter3(0)];
3434
+ const normalized = next.length ? next : [defaultChapter3(0, defaultTitle, defaultBody)];
3465
3435
  setChaptersState(normalized);
3466
3436
  setSelectedChapterState((current) => {
3467
3437
  const selectedId = chapters[current]?.id;
@@ -3469,7 +3439,7 @@ function useEpubProject(options = {}) {
3469
3439
  return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
3470
3440
  });
3471
3441
  },
3472
- [chapters]
3442
+ [chapters, defaultBody, defaultTitle]
3473
3443
  );
3474
3444
  const setSelectedChapter = useCallback12(
3475
3445
  (index) => {
@@ -3484,21 +3454,24 @@ function useEpubProject(options = {}) {
3484
3454
  )
3485
3455
  );
3486
3456
  }, []);
3487
- const addChapter = useCallback12((chapter = {}) => {
3488
- setChaptersState((current) => {
3489
- const generated = defaultChapter3(current.length);
3490
- const next = [
3491
- ...current,
3492
- {
3493
- id: chapter.id ?? generated.id,
3494
- title: chapter.title ?? generated.title,
3495
- body: chapter.body ?? generated.body
3496
- }
3497
- ];
3498
- setSelectedChapterState(next.length - 1);
3499
- return next;
3500
- });
3501
- }, []);
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
+ );
3502
3475
  const removeChapter = useCallback12(
3503
3476
  (index = selectedChapter) => {
3504
3477
  setChaptersState((current) => {
@@ -3536,7 +3509,7 @@ function useEpubProject(options = {}) {
3536
3509
  onExportRef.current?.(buffer);
3537
3510
  return buffer;
3538
3511
  }, [buildProject]);
3539
- return useMemo7(
3512
+ return useMemo9(
3540
3513
  () => ({
3541
3514
  metadata,
3542
3515
  chapters,
@@ -3575,11 +3548,11 @@ function useEpubProject(options = {}) {
3575
3548
  ]
3576
3549
  );
3577
3550
  }
3578
- 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" : "") {
3579
3552
  return {
3580
3553
  id: `chapter-${Date.now()}-${index}`,
3581
- title: index === 0 ? "\u7B2C\u4E00\u8A71" : `\u7B2C${index + 1}\u8A71`,
3582
- 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)
3583
3556
  };
3584
3557
  }
3585
3558
 
@@ -3589,7 +3562,7 @@ import {
3589
3562
  useCallback as useCallback13,
3590
3563
  useEffect as useEffect13,
3591
3564
  useRef as useRef17,
3592
- useState as useState15
3565
+ useState as useState16
3593
3566
  } from "react";
3594
3567
  function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3595
3568
  const defW = options?.defaultWidth ?? 120;
@@ -3597,7 +3570,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3597
3570
  const defX = options?.defaultX ?? 80;
3598
3571
  const defY = options?.defaultY ?? 100;
3599
3572
  const margin = options?.margin;
3600
- const [imageRect, setImageRect] = useState15(null);
3573
+ const [imageRect, setImageRect] = useState16(null);
3601
3574
  const rectRef = useRef17(imageRect);
3602
3575
  rectRef.current = imageRect;
3603
3576
  const layoutRef = useRef17(layout);
@@ -3719,14 +3692,14 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
3719
3692
  }
3720
3693
 
3721
3694
  // src/useLibrary.ts
3722
- 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";
3723
3696
  function useLibrary(options) {
3724
3697
  const { volumes, initialVolumeId, onChange } = options;
3725
3698
  const initialIndex = initialVolumeId != null ? Math.max(
3726
3699
  0,
3727
3700
  volumes.findIndex((v) => v.id === initialVolumeId)
3728
3701
  ) : 0;
3729
- const [currentId, setCurrentId] = useState16(
3702
+ const [currentId, setCurrentId] = useState17(
3730
3703
  volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
3731
3704
  );
3732
3705
  useEffect14(() => {
@@ -3739,13 +3712,13 @@ function useLibrary(options) {
3739
3712
  setCurrentId(fallback?.id ?? null);
3740
3713
  if (fallback) onChange?.(fallback);
3741
3714
  }, [currentId, volumes, onChange]);
3742
- const index = useMemo8(() => {
3715
+ const index = useMemo10(() => {
3743
3716
  if (volumes.length === 0) return -1;
3744
3717
  if (currentId == null) return 0;
3745
3718
  const found = volumes.findIndex((v) => v.id === currentId);
3746
3719
  return found >= 0 ? found : 0;
3747
3720
  }, [currentId, volumes]);
3748
- const current = useMemo8(
3721
+ const current = useMemo10(
3749
3722
  () => index >= 0 && index < volumes.length ? volumes[index] : null,
3750
3723
  [index, volumes]
3751
3724
  );
@@ -3787,19 +3760,17 @@ function useLibrary(options) {
3787
3760
  }
3788
3761
 
3789
3762
  // src/useManuscriptLayout.ts
3790
- import { useCallback as useCallback15, useEffect as useEffect15, useLayoutEffect as useLayoutEffect5, 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";
3791
3764
  function useManuscriptLayout(book, chapter, surface, options = {}) {
3792
3765
  const dialect = options.dialect ?? "mejiro";
3793
3766
  const enableResize = options.enableResize ?? true;
3794
3767
  const resizeDebounce = options.resizeDebounce ?? 120;
3795
- const [layout, setLayout] = useState17(null);
3796
- const [pageWidth, setPageWidth] = useState17(0);
3797
- const [pageHeight, setPageHeight] = useState17(0);
3798
- const [contentHeight, setContentHeight] = useState17(0);
3799
- const [elapsedMs, setElapsedMs] = useState17(0);
3800
- 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);
3801
3773
  const requestIdRef = useRef18(0);
3802
- layoutRef.current = layout;
3803
3774
  const recompute = useCallback15(async () => {
3804
3775
  const requestId = ++requestIdRef.current;
3805
3776
  if (!(chapter && surface.current)) {
@@ -3838,95 +3809,85 @@ function useManuscriptLayout(book, chapter, surface, options = {}) {
3838
3809
  }, [recompute]);
3839
3810
  useEffect15(() => {
3840
3811
  if (!enableResize) return;
3812
+ const el = surface.current;
3813
+ if (!el) return;
3841
3814
  let timer = null;
3842
- const onResize = () => {
3843
- if (!(surface.current && layoutRef.current)) return;
3844
- 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
+ }
3845
3828
  timer = setTimeout(() => {
3846
- if (!(surface.current && layoutRef.current)) return;
3847
- const dims = book.computePageSize(surface.current);
3848
- setPageWidth(dims.pageWidth);
3849
- setPageHeight(dims.pageHeight);
3850
- setContentHeight(dims.contentHeight);
3851
- layoutRef.current.resize({
3852
- pageWidth: dims.pageWidth,
3853
- lineWidth: dims.contentHeight - book.getOptions().fontSize * 0.5
3854
- });
3829
+ timer = null;
3830
+ void recompute();
3855
3831
  }, resizeDebounce);
3856
3832
  };
3857
- 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);
3858
3847
  return () => {
3859
- window.removeEventListener("resize", onResize);
3860
- if (timer) clearTimeout(timer);
3848
+ window.removeEventListener("resize", onWindowResize);
3849
+ clear();
3861
3850
  };
3862
- }, [book, surface, enableResize, resizeDebounce]);
3851
+ }, [surface, enableResize, resizeDebounce, recompute]);
3863
3852
  return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
3864
3853
  }
3865
3854
 
3866
3855
  // src/useReadingPosition.ts
3867
- import { useCallback as useCallback16, useEffect as useEffect16, useRef as useRef19, useState as useState18 } from "react";
3868
- 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";
3869
3861
  function resolveDefaultStorage2() {
3870
3862
  if (typeof globalThis === "undefined") return null;
3871
3863
  const w = globalThis.localStorage;
3872
3864
  return w ?? null;
3873
3865
  }
3874
- function parsePosition(raw) {
3875
- if (!raw) return null;
3876
- let parsed;
3877
- try {
3878
- parsed = JSON.parse(raw);
3879
- } catch {
3880
- return null;
3881
- }
3882
- if (!parsed || typeof parsed !== "object") return null;
3883
- const p = parsed;
3884
- if (typeof p.chapter !== "number") return null;
3885
- if (p.version === STORAGE_VERSION2) {
3886
- if (typeof p.paragraph !== "number" || typeof p.charIndex !== "number") return null;
3887
- return { chapter: p.chapter, paragraph: p.paragraph, charIndex: p.charIndex };
3888
- }
3889
- if (typeof p.spreadIdx === "number") {
3890
- if (typeof console !== "undefined" && typeof console.warn === "function") {
3891
- console.warn(
3892
- "[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."
3893
- );
3894
- }
3895
- return { chapter: p.chapter, paragraph: 0, charIndex: 0 };
3896
- }
3897
- return null;
3898
- }
3899
- function serializePosition(value) {
3900
- const payload = {
3901
- version: STORAGE_VERSION2,
3902
- chapter: value.chapter,
3903
- paragraph: value.paragraph,
3904
- charIndex: value.charIndex
3905
- };
3906
- return JSON.stringify(payload);
3907
- }
3908
3866
  function useReadingPosition(options) {
3909
3867
  const { key, throttleMs = 250 } = options;
3910
3868
  const storage = options.storage ?? resolveDefaultStorage2();
3911
- const [position, setPosition] = useState18(() => {
3869
+ const [position, setPosition] = useState19(() => {
3912
3870
  if (!storage) return null;
3913
3871
  try {
3914
- return parsePosition(storage.getItem(key));
3872
+ return parseReadingPosition(storage.getItem(key));
3915
3873
  } catch {
3916
3874
  return null;
3917
3875
  }
3918
3876
  });
3877
+ const storageRef = useRef19(storage);
3878
+ storageRef.current = storage;
3919
3879
  useEffect16(() => {
3920
- if (!storage) {
3880
+ const currentStorage = storageRef.current;
3881
+ if (!currentStorage) {
3921
3882
  setPosition(null);
3922
3883
  return;
3923
3884
  }
3924
3885
  try {
3925
- setPosition(parsePosition(storage.getItem(key)));
3886
+ setPosition(parseReadingPosition(currentStorage.getItem(key)));
3926
3887
  } catch {
3927
3888
  setPosition(null);
3928
3889
  }
3929
- }, [storage, key]);
3890
+ }, [key]);
3930
3891
  const timerRef = useRef19(null);
3931
3892
  useEffect16(
3932
3893
  () => () => {
@@ -3939,30 +3900,32 @@ function useReadingPosition(options) {
3939
3900
  const save = useCallback16(
3940
3901
  (next) => {
3941
3902
  setPosition(next);
3942
- if (storage) {
3903
+ const currentStorage = storageRef.current;
3904
+ if (currentStorage) {
3943
3905
  if (timerRef.current) clearTimeout(timerRef.current);
3944
3906
  timerRef.current = setTimeout(() => {
3945
3907
  try {
3946
- storage.setItem(key, serializePosition(next));
3908
+ currentStorage.setItem(key, serializeReadingPosition(next));
3947
3909
  } catch {
3948
3910
  }
3949
3911
  }, throttleMs);
3950
3912
  }
3951
3913
  onChangeRef.current?.(next);
3952
3914
  },
3953
- [storage, key, throttleMs]
3915
+ [key, throttleMs]
3954
3916
  );
3955
3917
  const clear = useCallback16(() => {
3956
3918
  setPosition(null);
3957
3919
  if (timerRef.current) clearTimeout(timerRef.current);
3958
- if (storage) {
3920
+ const currentStorage = storageRef.current;
3921
+ if (currentStorage) {
3959
3922
  try {
3960
- storage.removeItem(key);
3923
+ currentStorage.removeItem(key);
3961
3924
  } catch {
3962
3925
  }
3963
3926
  }
3964
3927
  onChangeRef.current?.(null);
3965
- }, [storage, key]);
3928
+ }, [key]);
3966
3929
  return { position, save, clear };
3967
3930
  }
3968
3931
 
@@ -3993,7 +3956,7 @@ export {
3993
3956
  MejiroSpread,
3994
3957
  MejiroStats,
3995
3958
  MejiroToc,
3996
- enMessages,
3959
+ enMessages2 as enMessages,
3997
3960
  format,
3998
3961
  jaMessages,
3999
3962
  resolveMessages,