@libraz/mejiro-react 0.4.0 → 0.6.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 (4) hide show
  1. package/README.md +34 -60
  2. package/dist/index.d.ts +1673 -38
  3. package/dist/index.js +3854 -37
  4. package/package.json +35 -8
package/dist/index.js CHANGED
@@ -1,27 +1,358 @@
1
+ // src/i18n.tsx
2
+ import { createContext, useContext, useMemo } from "react";
3
+ 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
+ var I18nContext = createContext(enMessages);
133
+ function MejiroI18nProvider({
134
+ locale,
135
+ messages,
136
+ children
137
+ }) {
138
+ 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]);
143
+ return /* @__PURE__ */ jsx(I18nContext.Provider, { value, children });
144
+ }
145
+ function useI18n(options = {}) {
146
+ const fromContext = useContext(I18nContext);
147
+ const { locale, messages } = options;
148
+ return useMemo(() => {
149
+ if (locale == null && messages == null) return fromContext;
150
+ const base = locale != null ? CATALOGS[locale] : fromContext;
151
+ return messages ? { ...base, ...messages } : base;
152
+ }, [locale, messages, fromContext]);
153
+ }
154
+ function resolveMessages(locale, overrides) {
155
+ const base = locale != null ? CATALOGS[locale] : enMessages;
156
+ return overrides ? { ...base, ...overrides } : base;
157
+ }
158
+ function format(template, vars) {
159
+ return template.replace(
160
+ /\{(\w+)\}/g,
161
+ (_, k) => Object.hasOwn(vars, k) ? String(vars[k]) : `{${k}}`
162
+ );
163
+ }
164
+
165
+ // src/MejiroChapterNav.tsx
166
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
167
+ function textPreview(text, max = 72) {
168
+ const compact = text.replace(/\s+/g, " ").trim();
169
+ if (compact.length <= max) return compact;
170
+ return `${compact.slice(0, max - 1)}...`;
171
+ }
172
+ function MejiroChapterNav({
173
+ epub,
174
+ chapter,
175
+ onChange,
176
+ variant = "select"
177
+ }) {
178
+ const messages = useI18n();
179
+ if (variant === "panel") {
180
+ return /* @__PURE__ */ jsxs("nav", { className: "mejiro-reader-chapter-panel", "aria-label": messages.tocTitle, children: [
181
+ /* @__PURE__ */ jsxs("div", { className: "mejiro-reader-chapter-panel-head", children: [
182
+ /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-panel-kicker", children: messages.tocTitle }),
183
+ /* @__PURE__ */ jsx2("strong", { children: epub.title }),
184
+ epub.author && /* @__PURE__ */ jsx2("span", { children: epub.author })
185
+ ] }),
186
+ /* @__PURE__ */ jsx2("ol", { className: "mejiro-reader-chapter-list", children: epub.chapters.map((ch, i) => {
187
+ const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
188
+ const preview = ch.paragraphs.find((p) => !p.headingLevel && p.text.trim())?.text;
189
+ const chapterKey = `${title}-${ch.paragraphs.map((p) => p.text).join("|").slice(0, 120)}`;
190
+ const headings = ch.paragraphs.filter((p) => p.headingLevel && p.text.trim() && p.text.trim() !== title).slice(0, 3);
191
+ return /* @__PURE__ */ jsx2("li", { className: "mejiro-reader-chapter-list-item", children: /* @__PURE__ */ jsxs(
192
+ "button",
193
+ {
194
+ type: "button",
195
+ className: `mejiro-reader-chapter-card${i === chapter ? " is-active" : ""}`,
196
+ "aria-current": i === chapter ? "true" : void 0,
197
+ onClick: () => onChange(i),
198
+ children: [
199
+ /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-number", children: String(i + 1).padStart(2, "0") }),
200
+ /* @__PURE__ */ jsxs("span", { className: "mejiro-reader-chapter-main", children: [
201
+ /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-title", children: title }),
202
+ preview && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-preview", children: textPreview(preview) }),
203
+ headings.length > 0 && /* @__PURE__ */ jsx2("span", { className: "mejiro-reader-chapter-subheads", children: headings.map((heading) => /* @__PURE__ */ jsx2("span", { children: textPreview(heading.text, 30) }, heading.text)) })
204
+ ] })
205
+ ]
206
+ }
207
+ ) }, chapterKey);
208
+ }) })
209
+ ] });
210
+ }
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}`)) }) });
212
+ }
213
+
214
+ // src/MejiroDropZone.tsx
215
+ import { useRef, useState } from "react";
216
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
217
+ function MejiroDropZone({
218
+ accept = ".epub",
219
+ validateFile,
220
+ onFile,
221
+ children,
222
+ className,
223
+ style
224
+ }) {
225
+ const messages = useI18n();
226
+ const inputRef = useRef(null);
227
+ const [dragover, setDragover] = useState(false);
228
+ const isValid = (file) => {
229
+ if (validateFile) return validateFile(file);
230
+ return file.name.endsWith(".epub");
231
+ };
232
+ const rootClass = ["mejiro-reader-drop-zone", dragover ? "is-dragover" : "", className ?? ""].filter(Boolean).join(" ");
233
+ return /* @__PURE__ */ jsxs2(
234
+ "button",
235
+ {
236
+ type: "button",
237
+ className: rootClass,
238
+ style,
239
+ onClick: () => inputRef.current?.click(),
240
+ onDragOver: (e) => {
241
+ e.preventDefault();
242
+ setDragover(true);
243
+ },
244
+ onDragLeave: () => setDragover(false),
245
+ onDrop: (e) => {
246
+ e.preventDefault();
247
+ setDragover(false);
248
+ const file = e.dataTransfer?.files[0];
249
+ if (file && isValid(file)) onFile(file);
250
+ },
251
+ children: [
252
+ children ?? /* @__PURE__ */ jsxs2(Fragment, { children: [
253
+ /* @__PURE__ */ jsx3("div", { className: "mejiro-reader-drop-zone-icon", children: "\u{1F4D6}" }),
254
+ /* @__PURE__ */ jsx3("div", { className: "mejiro-reader-drop-zone-text", children: /* @__PURE__ */ jsx3("strong", { children: messages.dropZoneTitle }) }),
255
+ /* @__PURE__ */ jsx3("div", { className: "mejiro-reader-drop-zone-hint", children: messages.dropZoneHint })
256
+ ] }),
257
+ /* @__PURE__ */ jsx3(
258
+ "input",
259
+ {
260
+ ref: inputRef,
261
+ type: "file",
262
+ accept,
263
+ hidden: true,
264
+ onChange: (e) => {
265
+ const file = e.target.files?.[0];
266
+ if (file && isValid(file)) onFile(file);
267
+ }
268
+ }
269
+ )
270
+ ]
271
+ }
272
+ );
273
+ }
274
+
275
+ // src/MejiroEditor.tsx
276
+ import { EditableEpub } from "@libraz/mejiro/epub";
277
+ import { useCallback as useCallback7, useEffect as useEffect7, useRef as useRef10, useState as useState8 } from "react";
278
+
279
+ // src/MejiroReader.tsx
280
+ import { DEFAULT_BOOK_OPTIONS, DEFAULT_PAGE_GEOMETRY } from "@libraz/mejiro/book";
281
+ import { normalizeFontFamily as normalizeFontFamily5 } from "@libraz/mejiro/browser";
282
+ import { manuscriptToEpubBook } from "@libraz/mejiro/epub";
283
+ import {
284
+ forwardRef,
285
+ useCallback as useCallback6,
286
+ useEffect as useEffect6,
287
+ useImperativeHandle,
288
+ useLayoutEffect as useLayoutEffect4,
289
+ useMemo as useMemo4,
290
+ useRef as useRef9,
291
+ useState as useState7
292
+ } from "react";
293
+
294
+ // src/MejiroPageIndicator.tsx
295
+ import { jsxs as jsxs3 } from "react/jsx-runtime";
296
+ function MejiroPageIndicator({ current, total }) {
297
+ return /* @__PURE__ */ jsxs3("div", { className: "mejiro-reader-page-indicator", children: [
298
+ current,
299
+ " / ",
300
+ total
301
+ ] });
302
+ }
303
+
304
+ // src/MejiroScrollView.tsx
305
+ import { normalizeFontFamily as normalizeFontFamily2 } from "@libraz/mejiro/browser";
306
+ import {
307
+ useEffect,
308
+ useLayoutEffect,
309
+ useMemo as useMemo2,
310
+ useRef as useRef2
311
+ } from "react";
312
+
313
+ // src/MejiroPageView.tsx
314
+ import { normalizeFontFamily } from "@libraz/mejiro/browser";
315
+ import { Fragment as Fragment3 } from "react";
316
+
1
317
  // src/MejiroPage.tsx
2
- import { jsx, jsxs } from "react/jsx-runtime";
318
+ import { Fragment as Fragment2 } from "react";
319
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3
320
  function renderSegment(segment, key) {
4
- if (segment.type === "text") {
5
- return segment.text;
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);
6
341
  }
7
- return /* @__PURE__ */ jsxs("ruby", { children: [
8
- segment.base,
9
- /* @__PURE__ */ jsx("rt", { children: segment.rubyText })
10
- ] }, key);
11
342
  }
12
343
  function renderLine(line, lineIndex) {
13
344
  const nodes = [];
14
345
  if (lineIndex > 0) {
15
- nodes.push(/* @__PURE__ */ jsx("br", {}, `br-${lineIndex}`));
346
+ nodes.push(/* @__PURE__ */ jsx4("br", {}, `br-${lineIndex}`));
16
347
  }
17
348
  for (let i = 0; i < line.segments.length; i++) {
18
- nodes.push(renderSegment(line.segments[i], i));
349
+ nodes.push(renderSegment(line.segments[i], `${lineIndex}-${i}`));
19
350
  }
20
351
  return nodes;
21
352
  }
22
353
  function MejiroPage({ page, className, style }) {
23
354
  const rootClass = className ? `mejiro-page ${className}` : "mejiro-page";
24
- return /* @__PURE__ */ jsx("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
355
+ return /* @__PURE__ */ jsx4("div", { className: rootClass, style, children: page.paragraphs.map((paragraph, pi) => {
25
356
  let paraClass = "mejiro-paragraph";
26
357
  if (paragraph.headingLevel != null) {
27
358
  paraClass += ` mejiro-paragraph--h${paragraph.headingLevel}`;
@@ -30,19 +361,35 @@ function MejiroPage({ page, className, style }) {
30
361
  }
31
362
  return (
32
363
  // biome-ignore lint/suspicious/noArrayIndexKey: paragraphs have no stable ID
33
- /* @__PURE__ */ jsx("div", { className: paraClass, children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
364
+ /* @__PURE__ */ jsx4("div", { className: paraClass, children: paragraph.lines.flatMap((line, li) => renderLine(line, li)) }, pi)
34
365
  );
35
366
  }) });
36
367
  }
37
368
 
38
369
  // src/MejiroPageView.tsx
39
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
370
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
40
371
  function renderSlotSegment(segment, key) {
41
- if (segment.type === "text") return segment.text;
42
- return /* @__PURE__ */ jsxs2("ruby", { children: [
43
- segment.base,
44
- /* @__PURE__ */ jsx2("rt", { children: segment.rubyText })
45
- ] }, 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
+ }
46
393
  }
47
394
  function MejiroPageView({
48
395
  result,
@@ -54,7 +401,8 @@ function MejiroPageView({
54
401
  }) {
55
402
  if (result.hasImages || slotMode) {
56
403
  const rootClass = className ? `mejiro-page-slots ${className}` : "mejiro-page-slots";
57
- return /* @__PURE__ */ jsx2("div", { className: rootClass, style: { position: "relative", ...style }, children: result.lines.map((line, i) => {
404
+ 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) => {
58
406
  const slot = result.slots[i];
59
407
  if (!slot || slot.height <= 0) return null;
60
408
  const colStyle = {
@@ -66,38 +414,3207 @@ function MejiroPageView({
66
414
  top: slot.yStart,
67
415
  height: slot.height,
68
416
  fontSize: line.fontSize,
69
- fontFamily,
417
+ fontFamily: fontFamilyCss,
70
418
  lineHeight: lineSpacing,
71
419
  fontWeight: line.headingLevel != null ? 700 : void 0
72
420
  };
73
421
  return (
74
422
  // biome-ignore lint/suspicious/noArrayIndexKey: lines have no stable ID
75
- /* @__PURE__ */ jsx2("div", { style: colStyle, children: line.segments.map((seg, si) => renderSlotSegment(seg, si)) }, i)
423
+ /* @__PURE__ */ jsx5("div", { style: colStyle, children: line.segments.map((seg, si) => renderSlotSegment(seg, `${i}-${si}`)) }, i)
76
424
  );
77
425
  }) });
78
426
  }
79
- return /* @__PURE__ */ jsx2(MejiroPage, { page: result.page, className, style });
427
+ return /* @__PURE__ */ jsx5(MejiroPage, { page: result.page, className, style });
80
428
  }
81
429
 
82
- // src/useImageOverlay.ts
430
+ // src/MejiroScrollView.tsx
431
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
432
+ function MejiroScrollView({
433
+ layout,
434
+ pageWidth,
435
+ pageHeight,
436
+ contentHeight,
437
+ fontFamily,
438
+ fontSize,
439
+ lineSpacing,
440
+ onVisiblePageChange,
441
+ scrollToPage,
442
+ pageGap = 24
443
+ }) {
444
+ const containerRef = useRef2(null);
445
+ const pageRefs = useRef2([]);
446
+ const onVisiblePageChangeRef = useRef2(onVisiblePageChange);
447
+ onVisiblePageChangeRef.current = onVisiblePageChange;
448
+ const pages = useMemo2(() => {
449
+ const total = layout.totalPages;
450
+ return Array.from({ length: total }, (_, i) => layout.getPage(i));
451
+ }, [layout]);
452
+ const useSlot = true;
453
+ const contentStyle = { height: contentHeight };
454
+ if (fontFamily) contentStyle.fontFamily = normalizeFontFamily2(fontFamily);
455
+ if (fontSize != null) contentStyle.fontSize = fontSize;
456
+ if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
457
+ useEffect(() => {
458
+ const container = containerRef.current;
459
+ if (!container) return;
460
+ if (typeof IntersectionObserver === "undefined") return;
461
+ let mostVisibleIdx = -1;
462
+ let mostVisibleRatio = 0;
463
+ const observer = new IntersectionObserver(
464
+ (entries) => {
465
+ for (const entry of entries) {
466
+ const idx = Number(entry.target.dataset.pageIdx);
467
+ if (Number.isNaN(idx)) continue;
468
+ if (entry.intersectionRatio > mostVisibleRatio || idx === mostVisibleIdx) {
469
+ if (entry.isIntersecting) {
470
+ mostVisibleRatio = entry.intersectionRatio;
471
+ mostVisibleIdx = idx;
472
+ } else if (idx === mostVisibleIdx) {
473
+ mostVisibleIdx = -1;
474
+ mostVisibleRatio = 0;
475
+ }
476
+ }
477
+ }
478
+ if (mostVisibleIdx >= 0) onVisiblePageChangeRef.current?.(mostVisibleIdx);
479
+ },
480
+ { root: container, threshold: [0.25, 0.5, 0.75] }
481
+ );
482
+ for (const el of pageRefs.current) {
483
+ if (el) observer.observe(el);
484
+ }
485
+ return () => observer.disconnect();
486
+ }, []);
487
+ useLayoutEffect(() => {
488
+ if (scrollToPage == null) return;
489
+ const el = pageRefs.current[scrollToPage];
490
+ if (!(el && containerRef.current)) return;
491
+ containerRef.current.scrollTo({
492
+ top: el.offsetTop,
493
+ behavior: "auto"
494
+ });
495
+ }, [scrollToPage]);
496
+ return /* @__PURE__ */ jsx6("div", { ref: containerRef, className: "mejiro-reader-scroll", children: /* @__PURE__ */ jsx6(
497
+ "div",
498
+ {
499
+ className: "mejiro-reader-scroll-track",
500
+ style: { display: "flex", flexDirection: "column", gap: pageGap },
501
+ children: pages.map((result, i) => /* @__PURE__ */ jsxs6(
502
+ "div",
503
+ {
504
+ ref: (el) => {
505
+ pageRefs.current[i] = el;
506
+ },
507
+ "data-page-idx": i,
508
+ className: "mejiro-reader-page",
509
+ style: { width: pageWidth, height: pageHeight, flexShrink: 0 },
510
+ 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 })
515
+ ] }),
516
+ /* @__PURE__ */ jsx6("div", { className: "mejiro-reader-page-viewport", children: /* @__PURE__ */ jsx6("div", { className: "mejiro-reader-page-clip", style: { height: contentHeight }, children: /* @__PURE__ */ jsx6(
517
+ MejiroPageView,
518
+ {
519
+ result,
520
+ slotMode: useSlot,
521
+ fontFamily,
522
+ lineSpacing,
523
+ className: "mejiro-reader-page-content",
524
+ style: contentStyle
525
+ }
526
+ ) }) })
527
+ ]
528
+ },
529
+ i
530
+ ))
531
+ }
532
+ ) });
533
+ }
534
+
535
+ // src/MejiroSettingsPanel.tsx
536
+ import { normalizeFontFamily as normalizeFontFamily3 } from "@libraz/mejiro/browser";
537
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
538
+ var DEFAULT_FONTS = [
539
+ { value: "serif", label: "System Serif" },
540
+ { value: "sans-serif", label: "System Sans" }
541
+ ];
542
+ function fontLabelFromCss(css) {
543
+ const first = css.split(",")[0]?.trim() ?? css;
544
+ return first.replace(/^["']|["']$/g, "") || css;
545
+ }
546
+ function MejiroSettingsPanel({
547
+ open,
548
+ settings,
549
+ onChange,
550
+ fonts = DEFAULT_FONTS,
551
+ minFontSize = 10,
552
+ maxFontSize = 48
553
+ }) {
554
+ const messages = useI18n();
555
+ const patch = (next) => onChange({ ...settings, ...next });
556
+ const currentFont = normalizeFontFamily3(settings.fontFamily);
557
+ 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(
565
+ "button",
566
+ {
567
+ type: "button",
568
+ className: "mejiro-reader-btn mejiro-reader-btn--icon",
569
+ "aria-label": messages.settingsSizeDown,
570
+ onClick: () => patch({ fontSize: Math.max(minFontSize, settings.fontSize - 1) }),
571
+ children: "A\u2212"
572
+ }
573
+ ),
574
+ /* @__PURE__ */ jsx7(
575
+ "input",
576
+ {
577
+ id: "mejiro-reader-font-size",
578
+ type: "number",
579
+ value: settings.fontSize,
580
+ min: minFontSize,
581
+ max: maxFontSize,
582
+ onChange: (e) => patch({ fontSize: Number(e.target.value) })
583
+ }
584
+ ),
585
+ /* @__PURE__ */ jsx7(
586
+ "button",
587
+ {
588
+ type: "button",
589
+ className: "mejiro-reader-btn mejiro-reader-btn--icon",
590
+ "aria-label": messages.settingsSizeUp,
591
+ onClick: () => patch({ fontSize: Math.min(maxFontSize, settings.fontSize + 1) }),
592
+ children: "A+"
593
+ }
594
+ )
595
+ ] })
596
+ ] }),
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(
602
+ "select",
603
+ {
604
+ id: "mejiro-reader-kinsoku",
605
+ value: settings.mode ?? "strict",
606
+ onChange: (e) => patch({ mode: e.target.value }),
607
+ children: [
608
+ /* @__PURE__ */ jsx7("option", { value: "strict", children: messages.settingsStrict }),
609
+ /* @__PURE__ */ jsx7("option", { value: "loose", children: messages.settingsLoose })
610
+ ]
611
+ }
612
+ )
613
+ ] }),
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(
617
+ "select",
618
+ {
619
+ id: "mejiro-reader-hanging",
620
+ value: String(settings.enableHanging ?? true),
621
+ onChange: (e) => patch({ enableHanging: e.target.value === "true" }),
622
+ children: [
623
+ /* @__PURE__ */ jsx7("option", { value: "true", children: messages.toggleOn }),
624
+ /* @__PURE__ */ jsx7("option", { value: "false", children: messages.toggleOff })
625
+ ]
626
+ }
627
+ )
628
+ ] }),
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(
632
+ "input",
633
+ {
634
+ id: "mejiro-reader-line-spacing",
635
+ className: "mejiro-reader-control--wide",
636
+ type: "number",
637
+ value: settings.lineSpacing ?? 1.8,
638
+ min: 1,
639
+ max: 3,
640
+ step: 0.1,
641
+ onChange: (e) => patch({ lineSpacing: Number(e.target.value) })
642
+ }
643
+ )
644
+ ] })
645
+ ] })
646
+ ] }) }) });
647
+ }
648
+
649
+ // src/MejiroSpread.tsx
650
+ import { normalizeFontFamily as normalizeFontFamily4 } from "@libraz/mejiro/browser";
651
+ import {
652
+ useRef as useRef3
653
+ } from "react";
654
+
655
+ // src/MejiroImageOverlay.tsx
656
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
657
+ function MejiroImageOverlay({
658
+ rect,
659
+ label,
660
+ onOverlayPointerDown,
661
+ onResizePointerDown,
662
+ onClose
663
+ }) {
664
+ const messages = useI18n();
665
+ const resolvedLabel = label ?? messages.imageButton;
666
+ return /* @__PURE__ */ jsxs8(
667
+ "div",
668
+ {
669
+ className: "mejiro-reader-image-overlay",
670
+ style: {
671
+ left: rect.x,
672
+ top: rect.y,
673
+ width: rect.w,
674
+ height: rect.h
675
+ },
676
+ onPointerDown: onOverlayPointerDown,
677
+ 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 })
681
+ ] }),
682
+ /* @__PURE__ */ jsx8("div", { className: "mejiro-reader-image-overlay-resize", onPointerDown: onResizePointerDown }),
683
+ /* @__PURE__ */ jsx8(
684
+ "button",
685
+ {
686
+ type: "button",
687
+ "aria-label": messages.imageRemoveButton,
688
+ title: messages.imageRemoveButton,
689
+ className: "mejiro-reader-image-overlay-close",
690
+ onPointerDown: (e) => {
691
+ e.stopPropagation();
692
+ e.preventDefault();
693
+ onClose?.();
694
+ },
695
+ onKeyDown: (e) => {
696
+ if (e.key !== "Enter" && e.key !== " ") return;
697
+ e.stopPropagation();
698
+ e.preventDefault();
699
+ onClose?.();
700
+ }
701
+ }
702
+ )
703
+ ]
704
+ }
705
+ );
706
+ }
707
+
708
+ // src/MejiroSelectionLayer.tsx
709
+ import { jsx as jsx9 } from "react/jsx-runtime";
710
+ function MejiroSelectionLayer({
711
+ rects,
712
+ side,
713
+ className,
714
+ style,
715
+ rectClassName,
716
+ rectStyle
717
+ }) {
718
+ const wrapperClass = className ? `mejiro-selection-layer ${className}` : "mejiro-selection-layer";
719
+ const rectClass = rectClassName ?? "mejiro-selection-rect";
720
+ return /* @__PURE__ */ jsx9(
721
+ "div",
722
+ {
723
+ className: wrapperClass,
724
+ style: {
725
+ position: "absolute",
726
+ inset: 0,
727
+ pointerEvents: "none",
728
+ ...style
729
+ },
730
+ children: rects.map((r, i) => {
731
+ if (r.side !== side) return null;
732
+ const posStyle = side === "right" ? { left: r.x, top: r.y } : { right: -(r.x + r.width), top: r.y };
733
+ return /* @__PURE__ */ jsx9(
734
+ "div",
735
+ {
736
+ className: rectClass,
737
+ style: {
738
+ position: "absolute",
739
+ width: r.width,
740
+ height: r.height,
741
+ ...posStyle,
742
+ ...rectStyle
743
+ }
744
+ },
745
+ i
746
+ );
747
+ })
748
+ }
749
+ );
750
+ }
751
+
752
+ // src/MejiroSpread.tsx
753
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
754
+ 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) : "" })
758
+ ] });
759
+ }
760
+ function MejiroSpread({
761
+ spread,
762
+ pageWidth,
763
+ pageHeight,
764
+ contentHeight,
765
+ fontFamily,
766
+ fontSize,
767
+ lineSpacing,
768
+ turning = false,
769
+ rightHeader = {},
770
+ leftHeader = {},
771
+ images = [],
772
+ renderPageHeader,
773
+ indicator,
774
+ onPrev,
775
+ onNext,
776
+ onImagePointerDown,
777
+ onImageResizePointerDown,
778
+ onImageClose,
779
+ anchorAtCoord,
780
+ selectionRects,
781
+ onSelectionChange,
782
+ singlePage = false,
783
+ onSwipe,
784
+ onSurfaceTap
785
+ }) {
786
+ const messages = useI18n();
787
+ const hasImages = images.length > 0;
788
+ const useSlot = true;
789
+ const selectionStartRef = useRef3(null);
790
+ const selectionEnabled = anchorAtCoord != null && onSelectionChange != null;
791
+ const gestureRef = useRef3(null);
792
+ const gestureEnabled = onSwipe != null || onSurfaceTap != null;
793
+ const SWIPE_THRESHOLD = 40;
794
+ const TAP_MOVE_THRESHOLD = 8;
795
+ function handleGesturePointerDown(e) {
796
+ if (!gestureEnabled) return;
797
+ if (e.pointerType === "mouse" && e.button !== 0) return;
798
+ gestureRef.current = { x: e.clientX, y: e.clientY, t: Date.now() };
799
+ }
800
+ function handleGesturePointerUp(e) {
801
+ if (!gestureEnabled) return;
802
+ const start = gestureRef.current;
803
+ gestureRef.current = null;
804
+ if (!start) return;
805
+ const dx = e.clientX - start.x;
806
+ const dy = e.clientY - start.y;
807
+ const ax = Math.abs(dx);
808
+ const ay = Math.abs(dy);
809
+ if (ax >= SWIPE_THRESHOLD && ax >= ay * 1.4) {
810
+ onSwipe?.(dx < 0 ? "next" : "prev");
811
+ return;
812
+ }
813
+ if (ax < TAP_MOVE_THRESHOLD && ay < TAP_MOVE_THRESHOLD) {
814
+ const target = e.target;
815
+ if (target?.closest("button, a, .mejiro-reader-image-overlay")) return;
816
+ onSurfaceTap?.();
817
+ }
818
+ }
819
+ function resolvePointer(e) {
820
+ if (!anchorAtCoord) return null;
821
+ const target = e.target;
822
+ if (!target) return null;
823
+ const content = target.closest(".mejiro-reader-page-content");
824
+ if (!content) return null;
825
+ const pageEl = content.closest(".mejiro-reader-page");
826
+ const isRight = pageEl?.classList.contains("mejiro-reader-page--right") ?? true;
827
+ const rect = content.getBoundingClientRect();
828
+ const offsetX = e.clientX - rect.left;
829
+ const offsetY = e.clientY - rect.top;
830
+ const spreadX = isRight ? offsetX : offsetX - rect.width;
831
+ return anchorAtCoord(spreadX, offsetY);
832
+ }
833
+ function handlePointerDown(e) {
834
+ if (!selectionEnabled) return;
835
+ const anchor = resolvePointer(e);
836
+ if (!anchor) return;
837
+ e.preventDefault();
838
+ selectionStartRef.current = anchor;
839
+ e.currentTarget.setPointerCapture(e.pointerId);
840
+ onSelectionChange?.(null);
841
+ }
842
+ function handlePointerMove(e) {
843
+ if (!selectionEnabled) return;
844
+ const start = selectionStartRef.current;
845
+ if (!start) return;
846
+ const end = resolvePointer(e);
847
+ if (!end) return;
848
+ if (end.paragraph === start.paragraph && end.charIndex === start.charIndex) return;
849
+ onSelectionChange?.({ start, end });
850
+ }
851
+ function handlePointerUp(e) {
852
+ if (!selectionEnabled) return;
853
+ selectionStartRef.current = null;
854
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
855
+ e.currentTarget.releasePointerCapture(e.pointerId);
856
+ }
857
+ }
858
+ const contentStyle = { height: contentHeight };
859
+ if (fontFamily) contentStyle.fontFamily = normalizeFontFamily4(fontFamily);
860
+ if (fontSize != null) contentStyle.fontSize = fontSize;
861
+ if (lineSpacing != null) contentStyle.lineHeight = String(lineSpacing);
862
+ const renderPage = (side) => {
863
+ const isRight = side === "right";
864
+ const result = isRight ? spread.right : spread.left;
865
+ const header = isRight ? rightHeader : leftHeader;
866
+ const pageKey = `${side}-${header.pageNumber ?? "blank"}`;
867
+ const pageStyle = {
868
+ width: pageWidth,
869
+ height: pageHeight
870
+ };
871
+ if (isRight && hasImages) pageStyle.overflow = "visible";
872
+ return /* @__PURE__ */ jsxs9(
873
+ "div",
874
+ {
875
+ className: `mejiro-reader-page mejiro-reader-page--${side}`,
876
+ style: pageStyle,
877
+ children: [
878
+ /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-page-rule" }),
879
+ 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(
882
+ MejiroPageView,
883
+ {
884
+ result,
885
+ slotMode: useSlot,
886
+ fontFamily,
887
+ lineSpacing,
888
+ className: "mejiro-reader-page-content",
889
+ style: contentStyle
890
+ },
891
+ pageKey
892
+ ),
893
+ selectionRects && selectionRects.length > 0 && /* @__PURE__ */ jsx10(MejiroSelectionLayer, { rects: selectionRects, side })
894
+ ] }) }),
895
+ isRight && images.map((item) => /* @__PURE__ */ jsx10(
896
+ MejiroImageOverlay,
897
+ {
898
+ rect: item.rect,
899
+ onOverlayPointerDown: (e) => onImagePointerDown?.(item.id, e),
900
+ onResizePointerDown: (e) => onImageResizePointerDown?.(item.id, e),
901
+ onClose: () => onImageClose?.(item.id)
902
+ },
903
+ item.id
904
+ ))
905
+ ]
906
+ },
907
+ pageKey
908
+ );
909
+ };
910
+ function combinedPointerDown(e) {
911
+ if (gestureEnabled) handleGesturePointerDown(e);
912
+ if (selectionEnabled) handlePointerDown(e);
913
+ }
914
+ function combinedPointerMove(e) {
915
+ if (selectionEnabled) handlePointerMove(e);
916
+ }
917
+ function combinedPointerUp(e) {
918
+ if (gestureEnabled) handleGesturePointerUp(e);
919
+ if (selectionEnabled) handlePointerUp(e);
920
+ }
921
+ const useCombined = selectionEnabled || gestureEnabled;
922
+ return /* @__PURE__ */ jsx10("div", { className: "mejiro-reader-book", children: /* @__PURE__ */ jsxs9(
923
+ "div",
924
+ {
925
+ className: `mejiro-reader-spread${turning ? " is-turning" : ""}${singlePage ? " mejiro-reader-spread--single" : ""}`,
926
+ onPointerDown: useCombined ? combinedPointerDown : void 0,
927
+ onPointerMove: useCombined ? combinedPointerMove : void 0,
928
+ onPointerUp: useCombined ? combinedPointerUp : void 0,
929
+ onPointerCancel: useCombined ? combinedPointerUp : void 0,
930
+ children: [
931
+ renderPage("right"),
932
+ !singlePage && renderPage("left"),
933
+ /* @__PURE__ */ jsx10(
934
+ "button",
935
+ {
936
+ type: "button",
937
+ "aria-label": messages.prevSpread,
938
+ className: "mejiro-reader-nav-zone mejiro-reader-nav-zone--prev",
939
+ onClick: onPrev
940
+ }
941
+ ),
942
+ /* @__PURE__ */ jsx10(
943
+ "button",
944
+ {
945
+ type: "button",
946
+ "aria-label": messages.nextSpread,
947
+ className: "mejiro-reader-nav-zone mejiro-reader-nav-zone--next",
948
+ onClick: onNext
949
+ }
950
+ ),
951
+ indicator
952
+ ]
953
+ }
954
+ ) });
955
+ }
956
+
957
+ // src/MejiroStats.tsx
958
+ import { estimateReadingTime, formatReadingTime } from "@libraz/mejiro/book";
959
+ import { jsx as jsx11 } from "react/jsx-runtime";
960
+ function MejiroStats({
961
+ chapter,
962
+ totalPages,
963
+ elapsedMs,
964
+ fontLabel,
965
+ showReadingTime = false,
966
+ cpm = 600,
967
+ readingTimeLocale = "ja"
968
+ }) {
969
+ if (!chapter) return /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-stats" });
970
+ const totalChars = chapter.paragraphs.reduce((s, p) => s + p.text.length, 0);
971
+ const totalRuby = chapter.paragraphs.reduce(
972
+ (s, p) => s + p.inlineAnnotations.filter((a) => a.kind === "ruby").length,
973
+ 0
974
+ );
975
+ const readingTimeLabel = showReadingTime ? formatReadingTime(estimateReadingTime(chapter, { cpm }), readingTimeLocale) : null;
976
+ const parts = [
977
+ `${totalChars}ch`,
978
+ `${totalPages}pp`,
979
+ totalRuby > 0 ? `${totalRuby}ruby` : null,
980
+ readingTimeLabel,
981
+ fontLabel || null,
982
+ `${elapsedMs.toFixed(0)}ms`
983
+ ];
984
+ return /* @__PURE__ */ jsx11("span", { className: "mejiro-reader-stats", children: parts.filter(Boolean).join(" / ") });
985
+ }
986
+
987
+ // src/useChapterLayout.ts
988
+ import {
989
+ useCallback,
990
+ useEffect as useEffect2,
991
+ useLayoutEffect as useLayoutEffect2,
992
+ useRef as useRef4,
993
+ useState as useState2
994
+ } from "react";
995
+ function useChapterLayout(book, epub, chapterIndex, surface, options = {}) {
996
+ const enableResize = options.enableResize ?? true;
997
+ const resizeDebounce = options.resizeDebounce ?? 120;
998
+ const [layout, setLayout] = useState2(null);
999
+ const [pageWidth, setPageWidth] = useState2(0);
1000
+ const [pageHeight, setPageHeight] = useState2(0);
1001
+ const [contentHeight, setContentHeight] = useState2(0);
1002
+ const [elapsedMs, setElapsedMs] = useState2(0);
1003
+ const layoutRef = useRef4(null);
1004
+ const requestIdRef = useRef4(0);
1005
+ const pendingRestore = useRef4(null);
1006
+ layoutRef.current = layout;
1007
+ const optionsRef = useRef4(options);
1008
+ optionsRef.current = options;
1009
+ const recompute = useCallback(
1010
+ async (opts = {}) => {
1011
+ const blank = opts.blank ?? true;
1012
+ const o = optionsRef.current;
1013
+ const requestId = ++requestIdRef.current;
1014
+ const chapter = epub?.chapters[chapterIndex];
1015
+ if (!(chapter && surface.current)) {
1016
+ setLayout(null);
1017
+ setPageWidth(0);
1018
+ setPageHeight(0);
1019
+ setContentHeight(0);
1020
+ setElapsedMs(0);
1021
+ return;
1022
+ }
1023
+ const captured = !blank && layoutRef.current ? o.capturePosition?.(layoutRef.current) ?? null : null;
1024
+ pendingRestore.current = captured;
1025
+ if (blank) setLayout(null);
1026
+ const dims = book.computePageSize(surface.current, o.pageGeometry?.());
1027
+ setPageWidth(dims.pageWidth);
1028
+ setPageHeight(dims.pageHeight);
1029
+ setContentHeight(dims.contentHeight);
1030
+ const t0 = performance.now();
1031
+ const l = await book.layoutChapter(chapter);
1032
+ if (requestId !== requestIdRef.current) return;
1033
+ layoutRef.current = l;
1034
+ setLayout(l);
1035
+ setElapsedMs(performance.now() - t0);
1036
+ },
1037
+ [book, epub, chapterIndex, surface]
1038
+ );
1039
+ useLayoutEffect2(() => {
1040
+ requestIdRef.current++;
1041
+ pendingRestore.current = null;
1042
+ setLayout(null);
1043
+ setPageWidth(0);
1044
+ setPageHeight(0);
1045
+ setContentHeight(0);
1046
+ setElapsedMs(0);
1047
+ }, [recompute]);
1048
+ useEffect2(() => {
1049
+ void recompute();
1050
+ return () => {
1051
+ requestIdRef.current++;
1052
+ };
1053
+ }, [recompute]);
1054
+ useEffect2(() => {
1055
+ if (!enableResize) return;
1056
+ const el = surface.current;
1057
+ if (!el) return;
1058
+ let timer = null;
1059
+ let observed = false;
1060
+ const clear = () => {
1061
+ if (timer) {
1062
+ clearTimeout(timer);
1063
+ timer = null;
1064
+ }
1065
+ };
1066
+ const scheduleReflow = (immediate) => {
1067
+ clear();
1068
+ if (immediate) {
1069
+ void recompute({ blank: false });
1070
+ return;
1071
+ }
1072
+ timer = setTimeout(() => {
1073
+ timer = null;
1074
+ void recompute({ blank: false });
1075
+ }, resizeDebounce);
1076
+ };
1077
+ if (typeof ResizeObserver !== "undefined") {
1078
+ const observer = new ResizeObserver(() => {
1079
+ const immediate = !observed;
1080
+ observed = true;
1081
+ scheduleReflow(immediate);
1082
+ });
1083
+ observer.observe(el);
1084
+ return () => {
1085
+ observer.disconnect();
1086
+ clear();
1087
+ };
1088
+ }
1089
+ const onWindowResize = () => scheduleReflow(false);
1090
+ window.addEventListener("resize", onWindowResize);
1091
+ return () => {
1092
+ window.removeEventListener("resize", onWindowResize);
1093
+ clear();
1094
+ };
1095
+ }, [surface, enableResize, resizeDebounce, recompute]);
1096
+ return {
1097
+ layout,
1098
+ pageWidth,
1099
+ pageHeight,
1100
+ contentHeight,
1101
+ elapsedMs,
1102
+ recompute,
1103
+ pendingRestore
1104
+ };
1105
+ }
1106
+
1107
+ // src/useEpub.ts
1108
+ import { parseEpub } from "@libraz/mejiro/epub";
1109
+ import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef5, useState as useState3 } from "react";
1110
+ function useEpub(options = {}) {
1111
+ const [epub, setEpub] = useState3(null);
1112
+ const [loading, setLoading] = useState3(false);
1113
+ const [error, setError] = useState3(null);
1114
+ const requestIdRef = useRef5(0);
1115
+ const onLoadRef = useRef5(options.onLoad);
1116
+ onLoadRef.current = options.onLoad;
1117
+ const onErrorRef = useRef5(options.onError);
1118
+ onErrorRef.current = options.onError;
1119
+ const fetchOptionsRef = useRef5(options.fetchOptions);
1120
+ fetchOptionsRef.current = options.fetchOptions;
1121
+ const fetchEpubRef = useRef5(options.fetchEpub);
1122
+ fetchEpubRef.current = options.fetchEpub;
1123
+ const loadBuffer = useCallback2(async (buffer) => {
1124
+ const requestId = ++requestIdRef.current;
1125
+ setLoading(true);
1126
+ setError(null);
1127
+ try {
1128
+ const book = await parseEpub(buffer);
1129
+ if (requestId !== requestIdRef.current) return null;
1130
+ setEpub(book);
1131
+ onLoadRef.current?.(book);
1132
+ return book;
1133
+ } catch (err) {
1134
+ if (requestId === requestIdRef.current) {
1135
+ const nextError = err instanceof Error ? err : new Error(String(err));
1136
+ setError(nextError);
1137
+ onErrorRef.current?.(nextError);
1138
+ }
1139
+ return null;
1140
+ } finally {
1141
+ if (requestId === requestIdRef.current) setLoading(false);
1142
+ }
1143
+ }, []);
1144
+ const loadFile = useCallback2(async (file) => {
1145
+ const requestId = ++requestIdRef.current;
1146
+ setLoading(true);
1147
+ setError(null);
1148
+ try {
1149
+ const book = await parseEpub(await file.arrayBuffer());
1150
+ if (requestId !== requestIdRef.current) return null;
1151
+ setEpub(book);
1152
+ onLoadRef.current?.(book);
1153
+ return book;
1154
+ } catch (err) {
1155
+ if (requestId === requestIdRef.current) {
1156
+ const nextError = err instanceof Error ? err : new Error(String(err));
1157
+ setError(nextError);
1158
+ onErrorRef.current?.(nextError);
1159
+ }
1160
+ return null;
1161
+ } finally {
1162
+ if (requestId === requestIdRef.current) setLoading(false);
1163
+ }
1164
+ }, []);
1165
+ const loadUrl = useCallback2(async (url) => {
1166
+ const requestId = ++requestIdRef.current;
1167
+ setLoading(true);
1168
+ setError(null);
1169
+ try {
1170
+ let buffer;
1171
+ if (fetchEpubRef.current) {
1172
+ buffer = await fetchEpubRef.current(url);
1173
+ } else {
1174
+ const init = fetchOptionsRef.current;
1175
+ const res = init ? await fetch(url, init) : await fetch(url);
1176
+ if (!res.ok) return null;
1177
+ buffer = await res.arrayBuffer();
1178
+ }
1179
+ const book = await parseEpub(buffer);
1180
+ if (requestId !== requestIdRef.current) return null;
1181
+ setEpub(book);
1182
+ onLoadRef.current?.(book);
1183
+ return book;
1184
+ } catch (err) {
1185
+ if (requestId === requestIdRef.current) {
1186
+ const nextError = err instanceof Error ? err : new Error(String(err));
1187
+ setError(nextError);
1188
+ onErrorRef.current?.(nextError);
1189
+ }
1190
+ return null;
1191
+ } finally {
1192
+ if (requestId === requestIdRef.current) setLoading(false);
1193
+ }
1194
+ }, []);
1195
+ const defaultUrl = options.defaultUrl;
1196
+ useEffect3(() => {
1197
+ if (defaultUrl) void loadUrl(defaultUrl);
1198
+ }, [defaultUrl, loadUrl]);
1199
+ const replaceEpub = useCallback2((book) => {
1200
+ requestIdRef.current++;
1201
+ setLoading(false);
1202
+ setError(null);
1203
+ setEpub(book);
1204
+ }, []);
1205
+ return { epub, loading, error, loadBuffer, loadFile, loadUrl, setEpub: replaceEpub };
1206
+ }
1207
+
1208
+ // src/useMejiroBook.ts
1209
+ import { MejiroBook } from "@libraz/mejiro/book";
1210
+ import { useCallback as useCallback3, useRef as useRef6, useState as useState4 } from "react";
1211
+ function useMejiroBook(initial) {
1212
+ const bookRef = useRef6(null);
1213
+ if (!bookRef.current) bookRef.current = new MejiroBook(initial);
1214
+ const [options, setLocal] = useState4(initial);
1215
+ const setOptions = useCallback3(async (next) => {
1216
+ setLocal((prev) => ({ ...prev, ...next }));
1217
+ await bookRef.current?.setOptions(next);
1218
+ }, []);
1219
+ return { book: bookRef.current, options, setOptions };
1220
+ }
1221
+
1222
+ // src/useMultiImageOverlay.ts
83
1223
  import { moveImageOverlayRect, resizeImageOverlayRect } from "@libraz/mejiro";
84
- import { useCallback, useRef, useState } from "react";
1224
+ import {
1225
+ useCallback as useCallback4,
1226
+ useEffect as useEffect4,
1227
+ useMemo as useMemo3,
1228
+ useRef as useRef7,
1229
+ useState as useState5
1230
+ } from "react";
1231
+ var nextId = 0;
1232
+ function useMultiImageOverlay(layout, spreadIdx, options = {}) {
1233
+ const defW = options.defaultWidth ?? 120;
1234
+ const defH = options.defaultHeight ?? 160;
1235
+ const defX = options.defaultX ?? 80;
1236
+ const defY = options.defaultY ?? 100;
1237
+ const margin = options.margin;
1238
+ const [images, setImages] = useState5(() => /* @__PURE__ */ new Map());
1239
+ const imagesRef = useRef7(images);
1240
+ imagesRef.current = images;
1241
+ const layoutRef = useRef7(null);
1242
+ layoutRef.current = layout;
1243
+ const spreadIdxRef = useRef7(spreadIdx);
1244
+ spreadIdxRef.current = spreadIdx;
1245
+ const onUpdateRef = useRef7(options.onUpdate);
1246
+ onUpdateRef.current = options.onUpdate;
1247
+ const activeDragCleanupsRef = useRef7(/* @__PURE__ */ new Set());
1248
+ useEffect4(
1249
+ () => () => {
1250
+ for (const cleanup of activeDragCleanupsRef.current) cleanup();
1251
+ activeDragCleanupsRef.current.clear();
1252
+ },
1253
+ []
1254
+ );
1255
+ const syncSpread = useCallback4(
1256
+ (si) => {
1257
+ const lo = layoutRef.current;
1258
+ if (!lo) return;
1259
+ const items = imagesRef.current.get(si) ?? [];
1260
+ const bookImages = items.map((it) => ({
1261
+ x: it.rect.x,
1262
+ y: it.rect.y,
1263
+ w: it.rect.w,
1264
+ h: it.rect.h,
1265
+ margin
1266
+ }));
1267
+ lo.setImages(si, bookImages);
1268
+ onUpdateRef.current?.(lo.getSpread(si));
1269
+ },
1270
+ [margin]
1271
+ );
1272
+ const commit = useCallback4(
1273
+ (next, affectedSpread) => {
1274
+ imagesRef.current = next;
1275
+ setImages(next);
1276
+ syncSpread(affectedSpread);
1277
+ },
1278
+ [syncSpread]
1279
+ );
1280
+ const findById = useCallback4(
1281
+ (id) => {
1282
+ for (const [s, list] of imagesRef.current) {
1283
+ const i = list.findIndex((it) => it.id === id);
1284
+ if (i >= 0) return { spread: s, index: i, item: list[i] };
1285
+ }
1286
+ return null;
1287
+ },
1288
+ []
1289
+ );
1290
+ const addImage = useCallback4(
1291
+ (overrides) => {
1292
+ const si = spreadIdxRef.current;
1293
+ const list = imagesRef.current.get(si) ?? [];
1294
+ const last = list[list.length - 1];
1295
+ const baseX = last ? last.rect.x - 100 : defX;
1296
+ const baseY = last ? last.rect.y + 40 : defY;
1297
+ nextId += 1;
1298
+ const item = {
1299
+ id: `mejiro-img-${nextId}`,
1300
+ rect: {
1301
+ x: overrides?.x ?? baseX,
1302
+ y: overrides?.y ?? baseY,
1303
+ w: overrides?.w ?? defW,
1304
+ h: overrides?.h ?? defH
1305
+ }
1306
+ };
1307
+ const next = new Map(imagesRef.current);
1308
+ next.set(si, [...list, item]);
1309
+ commit(next, si);
1310
+ return item;
1311
+ },
1312
+ [defX, defY, defW, defH, commit]
1313
+ );
1314
+ const removeImage = useCallback4(
1315
+ (id) => {
1316
+ const found = findById(id);
1317
+ if (!found) return;
1318
+ const next = new Map(imagesRef.current);
1319
+ const list = (next.get(found.spread) ?? []).filter((it) => it.id !== id);
1320
+ if (list.length === 0) next.delete(found.spread);
1321
+ else next.set(found.spread, list);
1322
+ commit(next, found.spread);
1323
+ },
1324
+ [findById, commit]
1325
+ );
1326
+ const updateImage = useCallback4(
1327
+ (id, rect) => {
1328
+ const found = findById(id);
1329
+ if (!found) return;
1330
+ const next = new Map(imagesRef.current);
1331
+ const list = (next.get(found.spread) ?? []).map(
1332
+ (it) => it.id === id ? { ...it, rect: { ...it.rect, ...rect } } : it
1333
+ );
1334
+ next.set(found.spread, list);
1335
+ commit(next, found.spread);
1336
+ },
1337
+ [findById, commit]
1338
+ );
1339
+ const clearImages = useCallback4(
1340
+ (target) => {
1341
+ if (target == null) {
1342
+ const old = imagesRef.current;
1343
+ imagesRef.current = /* @__PURE__ */ new Map();
1344
+ setImages(/* @__PURE__ */ new Map());
1345
+ layoutRef.current?.clearImages();
1346
+ if (layoutRef.current) {
1347
+ for (const si of old.keys()) onUpdateRef.current?.(layoutRef.current.getSpread(si));
1348
+ }
1349
+ } else {
1350
+ const next = new Map(imagesRef.current);
1351
+ next.delete(target);
1352
+ commit(next, target);
1353
+ }
1354
+ },
1355
+ [commit]
1356
+ );
1357
+ const onOverlayPointerDown = useCallback4(
1358
+ (id, e) => {
1359
+ e.preventDefault();
1360
+ const found = findById(id);
1361
+ if (!found) return;
1362
+ const startX = e.clientX;
1363
+ const startY = e.clientY;
1364
+ const start = { ...found.item.rect };
1365
+ const target = e.currentTarget;
1366
+ target.setPointerCapture(e.nativeEvent.pointerId);
1367
+ target.classList.add("is-dragging");
1368
+ let rafId = 0;
1369
+ const onMove = (me) => {
1370
+ const dx = me.clientX - startX;
1371
+ const dy = me.clientY - startY;
1372
+ const r = moveImageOverlayRect(start, dx, dy);
1373
+ cancelAnimationFrame(rafId);
1374
+ rafId = requestAnimationFrame(() => updateImage(id, r));
1375
+ };
1376
+ let cleanup;
1377
+ const onUp = () => cleanup();
1378
+ cleanup = () => {
1379
+ cancelAnimationFrame(rafId);
1380
+ target.classList.remove("is-dragging");
1381
+ document.removeEventListener("pointermove", onMove);
1382
+ document.removeEventListener("pointerup", onUp);
1383
+ activeDragCleanupsRef.current.delete(cleanup);
1384
+ };
1385
+ document.addEventListener("pointermove", onMove);
1386
+ document.addEventListener("pointerup", onUp);
1387
+ activeDragCleanupsRef.current.add(cleanup);
1388
+ },
1389
+ [findById, updateImage]
1390
+ );
1391
+ const onResizePointerDown = useCallback4(
1392
+ (id, e) => {
1393
+ e.preventDefault();
1394
+ e.stopPropagation();
1395
+ const found = findById(id);
1396
+ if (!found) return;
1397
+ const startX = e.clientX;
1398
+ const startY = e.clientY;
1399
+ const start = { ...found.item.rect };
1400
+ const target = e.currentTarget;
1401
+ target.setPointerCapture(e.nativeEvent.pointerId);
1402
+ target.parentElement?.classList.add("is-dragging");
1403
+ let rafId = 0;
1404
+ const onMove = (me) => {
1405
+ const dx = me.clientX - startX;
1406
+ const dy = me.clientY - startY;
1407
+ const r = resizeImageOverlayRect(start, dx, dy);
1408
+ cancelAnimationFrame(rafId);
1409
+ rafId = requestAnimationFrame(() => updateImage(id, r));
1410
+ };
1411
+ let cleanup;
1412
+ const onUp = () => cleanup();
1413
+ cleanup = () => {
1414
+ cancelAnimationFrame(rafId);
1415
+ target.parentElement?.classList.remove("is-dragging");
1416
+ document.removeEventListener("pointermove", onMove);
1417
+ document.removeEventListener("pointerup", onUp);
1418
+ activeDragCleanupsRef.current.delete(cleanup);
1419
+ };
1420
+ document.addEventListener("pointermove", onMove);
1421
+ document.addEventListener("pointerup", onUp);
1422
+ activeDragCleanupsRef.current.add(cleanup);
1423
+ },
1424
+ [findById, updateImage]
1425
+ );
1426
+ useEffect4(() => {
1427
+ if (!layout) return;
1428
+ for (const si of imagesRef.current.keys()) syncSpread(si);
1429
+ }, [layout, syncSpread]);
1430
+ const currentImages = useMemo3(() => images.get(spreadIdx) ?? [], [images, spreadIdx]);
1431
+ const hasImages = useMemo3(() => [...images.values()].some((list) => list.length > 0), [images]);
1432
+ return {
1433
+ imagesBySpread: images,
1434
+ currentImages,
1435
+ hasImages,
1436
+ addImage,
1437
+ removeImage,
1438
+ updateImage,
1439
+ clearImages,
1440
+ onOverlayPointerDown,
1441
+ onResizePointerDown
1442
+ };
1443
+ }
1444
+
1445
+ // src/useSpread.ts
1446
+ import { useCallback as useCallback5, useEffect as useEffect5, useLayoutEffect as useLayoutEffect3, useRef as useRef8, useState as useState6 } from "react";
1447
+ function useSpread(layout, options = {}) {
1448
+ const enableKeyboard = options.enableKeyboard ?? true;
1449
+ const turnDuration = options.turnDuration ?? 180;
1450
+ const onChangeRef = useRef8(options.onChange);
1451
+ onChangeRef.current = options.onChange;
1452
+ const [spreadIdx, setSpreadIdx] = useState6(0);
1453
+ const [spread, setSpread] = useState6(null);
1454
+ const [turning, setTurning] = useState6(false);
1455
+ const layoutRef = useRef8(null);
1456
+ layoutRef.current = layout;
1457
+ const spreadIdxRef = useRef8(0);
1458
+ spreadIdxRef.current = spreadIdx;
1459
+ const turnTimerRef = useRef8(null);
1460
+ const layoutGenerationRef = useRef8(0);
1461
+ const totalPages = layout?.totalPages ?? 0;
1462
+ const totalSpreads = Math.max(1, Math.ceil(totalPages / 2));
1463
+ const refresh = useCallback5(() => {
1464
+ if (!layoutRef.current) {
1465
+ setSpread(null);
1466
+ return;
1467
+ }
1468
+ setSpread(layoutRef.current.getSpread(spreadIdxRef.current));
1469
+ }, []);
1470
+ useLayoutEffect3(() => {
1471
+ layoutGenerationRef.current++;
1472
+ if (turnTimerRef.current) {
1473
+ clearTimeout(turnTimerRef.current);
1474
+ turnTimerRef.current = null;
1475
+ }
1476
+ setTurning(false);
1477
+ setSpreadIdx(0);
1478
+ if (!layout) {
1479
+ setSpread(null);
1480
+ return;
1481
+ }
1482
+ setSpread(layout.getSpread(0));
1483
+ }, [layout]);
1484
+ useEffect5(() => {
1485
+ if (!layout) return;
1486
+ setSpread(layout.getSpread(spreadIdx));
1487
+ onChangeRef.current?.(spreadIdx);
1488
+ }, [layout, spreadIdx]);
1489
+ const goTo = useCallback5(
1490
+ (index) => {
1491
+ if (!layoutRef.current) return;
1492
+ const max = Math.max(1, Math.ceil(layoutRef.current.totalPages / 2)) - 1;
1493
+ const target = Math.max(0, Math.min(max, index));
1494
+ if (target === spreadIdxRef.current) return;
1495
+ if (turnDuration > 0) {
1496
+ const generation = layoutGenerationRef.current;
1497
+ if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
1498
+ setTurning(true);
1499
+ turnTimerRef.current = setTimeout(() => {
1500
+ if (generation !== layoutGenerationRef.current) return;
1501
+ setSpreadIdx(target);
1502
+ setTurning(false);
1503
+ turnTimerRef.current = null;
1504
+ }, turnDuration);
1505
+ } else {
1506
+ setSpreadIdx(target);
1507
+ }
1508
+ },
1509
+ [turnDuration]
1510
+ );
1511
+ const setSpreadIndex = useCallback5((index) => {
1512
+ if (!layoutRef.current) return;
1513
+ if (turnTimerRef.current) {
1514
+ clearTimeout(turnTimerRef.current);
1515
+ turnTimerRef.current = null;
1516
+ }
1517
+ setTurning(false);
1518
+ const max = Math.max(1, Math.ceil(layoutRef.current.totalPages / 2)) - 1;
1519
+ setSpreadIdx(Math.max(0, Math.min(max, index)));
1520
+ }, []);
1521
+ const next = useCallback5(() => goTo(spreadIdxRef.current + 1), [goTo]);
1522
+ const prev = useCallback5(() => goTo(spreadIdxRef.current - 1), [goTo]);
1523
+ useEffect5(() => {
1524
+ if (!enableKeyboard) return;
1525
+ const onKey = (e) => {
1526
+ if (!layoutRef.current) return;
1527
+ if (e.key === "ArrowLeft") next();
1528
+ else if (e.key === "ArrowRight") prev();
1529
+ };
1530
+ window.addEventListener("keydown", onKey);
1531
+ return () => window.removeEventListener("keydown", onKey);
1532
+ }, [enableKeyboard, next, prev]);
1533
+ useEffect5(
1534
+ () => () => {
1535
+ if (turnTimerRef.current) clearTimeout(turnTimerRef.current);
1536
+ },
1537
+ []
1538
+ );
1539
+ return {
1540
+ spreadIdx,
1541
+ spread,
1542
+ totalPages,
1543
+ totalSpreads,
1544
+ turning,
1545
+ next,
1546
+ prev,
1547
+ goTo,
1548
+ setSpread: setSpreadIndex,
1549
+ refresh
1550
+ };
1551
+ }
1552
+
1553
+ // src/MejiroReader.tsx
1554
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1555
+ function MejiroReaderInner(props, ref) {
1556
+ const manuscriptProp = "manuscript" in props ? props.manuscript : void 0;
1557
+ const dialectProp = "dialect" in props ? props.dialect : void 0;
1558
+ const {
1559
+ options = DEFAULT_BOOK_OPTIONS,
1560
+ pageGeometry: pageGeometryProp,
1561
+ fonts,
1562
+ epub: epubProp,
1563
+ epubUrl,
1564
+ chapter: chapterProp,
1565
+ spreadIdx: spreadIdxProp,
1566
+ theme = "light",
1567
+ mode = "paginated",
1568
+ spreadMode = "double",
1569
+ fit = "fill",
1570
+ enableSurfaceTap = true,
1571
+ fallback,
1572
+ fetchOptions,
1573
+ fetchEpub: fetchEpubFn,
1574
+ locale,
1575
+ messages,
1576
+ title = "mejiro",
1577
+ subtitle,
1578
+ logo,
1579
+ bare = false,
1580
+ enableHeader = !bare,
1581
+ enableDropZone = false,
1582
+ enableChapterNav = !bare,
1583
+ chapterNavMode = "select",
1584
+ enableSettings = !bare,
1585
+ renderSettings,
1586
+ enableImageOverlay = false,
1587
+ enableStats = !bare,
1588
+ enableKeyboard = true,
1589
+ enablePageIndicator = !bare,
1590
+ pageNumbers = "both",
1591
+ annotations,
1592
+ onLoad,
1593
+ onError,
1594
+ onChapterChange,
1595
+ onSpreadChange,
1596
+ onSpreadIdxChange,
1597
+ onPageRead,
1598
+ onChapterCompleted
1599
+ } = props;
1600
+ const surfaceRef = useRef9(null);
1601
+ const fileRef = useRef9(null);
1602
+ const [settingsOpen, setSettingsOpen] = useState7(false);
1603
+ const [chapterState, setChapterState] = useState7(chapterProp ?? 0);
1604
+ const chapter = chapterProp ?? chapterState;
1605
+ const [chromeHidden, setChromeHidden] = useState7(false);
1606
+ const [autoSingle, setAutoSingle] = useState7(false);
1607
+ useEffect6(() => {
1608
+ if (!settingsOpen) return;
1609
+ const onKey = (e2) => {
1610
+ if (e2.key === "Escape") setSettingsOpen(false);
1611
+ };
1612
+ window.addEventListener("keydown", onKey);
1613
+ return () => window.removeEventListener("keydown", onKey);
1614
+ }, [settingsOpen]);
1615
+ useEffect6(() => {
1616
+ if (spreadMode !== "auto") return;
1617
+ const surface = surfaceRef.current;
1618
+ if (!surface) return;
1619
+ const update = () => {
1620
+ const rect = surface.getBoundingClientRect();
1621
+ setAutoSingle(rect.width < rect.height);
1622
+ };
1623
+ update();
1624
+ if (typeof ResizeObserver === "undefined") return;
1625
+ const observer = new ResizeObserver(update);
1626
+ observer.observe(surface);
1627
+ return () => observer.disconnect();
1628
+ }, [spreadMode]);
1629
+ const effectiveSingle = spreadMode === "single" || spreadMode === "auto" && autoSingle;
1630
+ const resolvedGeometry = useMemo4(() => {
1631
+ const columns = effectiveSingle ? 1 : 2;
1632
+ if (fit !== "width") return { columns, ...pageGeometryProp };
1633
+ return {
1634
+ columns,
1635
+ gutterOffset: 0,
1636
+ headerOffset: 0,
1637
+ maxHeight: Number.POSITIVE_INFINITY,
1638
+ minWidth: 0,
1639
+ minHeight: 0,
1640
+ ...pageGeometryProp
1641
+ };
1642
+ }, [fit, pageGeometryProp, effectiveSingle]);
1643
+ const surfaceAspect = useMemo4(() => {
1644
+ const columns = effectiveSingle ? 1 : 2;
1645
+ const aspect = resolvedGeometry?.aspect ?? DEFAULT_PAGE_GEOMETRY.aspect;
1646
+ return `${columns} / ${aspect}`;
1647
+ }, [effectiveSingle, resolvedGeometry]);
1648
+ const { book, options: bookOptions, setOptions } = useMejiroBook(options);
1649
+ const synthesizedEpub = useMemo4(() => {
1650
+ if (manuscriptProp === void 0) return null;
1651
+ return manuscriptToEpubBook(manuscriptProp, { dialect: dialectProp });
1652
+ }, [manuscriptProp, dialectProp]);
1653
+ const epubCtx = useEpub({
1654
+ // `epub` / `manuscript` take precedence: skip the URL fetch when a parsed
1655
+ // book (or a synthesized manuscript book) is supplied.
1656
+ defaultUrl: epubProp !== void 0 || manuscriptProp !== void 0 ? void 0 : epubUrl,
1657
+ fetchOptions,
1658
+ fetchEpub: fetchEpubFn,
1659
+ onLoad: (b) => {
1660
+ if (chapterProp == null) setChapterState(0);
1661
+ onLoad?.(b);
1662
+ }
1663
+ });
1664
+ const controlled = epubProp !== void 0 || manuscriptProp !== void 0;
1665
+ const isManuscriptSource = manuscriptProp !== void 0;
1666
+ const e = isManuscriptSource ? synthesizedEpub : controlled ? epubProp ?? null : epubCtx.epub;
1667
+ const onErrorRef = useRef9(onError);
1668
+ onErrorRef.current = onError;
1669
+ useEffect6(() => {
1670
+ if (epubCtx.error) onErrorRef.current?.(epubCtx.error);
1671
+ }, [epubCtx.error]);
1672
+ const pageGeometryRef = useRef9(resolvedGeometry);
1673
+ pageGeometryRef.current = resolvedGeometry;
1674
+ const curSpreadIdxRef = useRef9(0);
1675
+ const spreadIdxPropRef = useRef9(spreadIdxProp);
1676
+ spreadIdxPropRef.current = spreadIdxProp;
1677
+ const layoutCtx = useChapterLayout(book, e, chapter, surfaceRef, {
1678
+ pageGeometry: () => pageGeometryRef.current,
1679
+ // Preserve the reading position across a reflow re-layout (size / option
1680
+ // changes), but only in uncontrolled mode — when `spreadIdx` is controlled
1681
+ // the host owns the position and the controlled-restore effect handles it.
1682
+ capturePosition: (layout2) => spreadIdxPropRef.current != null ? null : layout2.anchorAt(curSpreadIdxRef.current, "right")
1683
+ });
1684
+ const spreadChangedRef = useRef9(void 0);
1685
+ spreadChangedRef.current = (i) => {
1686
+ onSpreadChange?.(i);
1687
+ onSpreadIdxChange?.(i);
1688
+ };
1689
+ const spreadCtx = useSpread(layoutCtx.layout, {
1690
+ enableKeyboard,
1691
+ onChange: (i) => spreadChangedRef.current?.(i)
1692
+ });
1693
+ curSpreadIdxRef.current = spreadCtx.spreadIdx;
1694
+ const setSpreadRef = useRef9(spreadCtx.setSpread);
1695
+ setSpreadRef.current = spreadCtx.setSpread;
1696
+ useLayoutEffect4(() => {
1697
+ const anchor = layoutCtx.pendingRestore.current;
1698
+ if (!(anchor && layoutCtx.layout)) return;
1699
+ layoutCtx.pendingRestore.current = null;
1700
+ const loc = layoutCtx.layout.locateAnchor(anchor);
1701
+ setSpreadRef.current(loc?.spreadIdx ?? 0);
1702
+ }, [layoutCtx.layout]);
1703
+ const optionsKey = [
1704
+ bookOptions.fontFamily,
1705
+ bookOptions.fontSize,
1706
+ bookOptions.lineSpacing,
1707
+ bookOptions.mode,
1708
+ bookOptions.enableHanging
1709
+ ].join("|");
1710
+ const optionsKeyRef = useRef9(optionsKey);
1711
+ const bookOptionsRef = useRef9(bookOptions);
1712
+ bookOptionsRef.current = bookOptions;
1713
+ const recomputeRef = useRef9(layoutCtx.recompute);
1714
+ recomputeRef.current = layoutCtx.recompute;
1715
+ useEffect6(() => {
1716
+ if (optionsKeyRef.current === optionsKey) return;
1717
+ optionsKeyRef.current = optionsKey;
1718
+ const timer = setTimeout(() => {
1719
+ void (async () => {
1720
+ await setOptions({ ...bookOptionsRef.current });
1721
+ await recomputeRef.current({ blank: false });
1722
+ })();
1723
+ }, 60);
1724
+ return () => clearTimeout(timer);
1725
+ }, [optionsKey, setOptions]);
1726
+ const geometryKey = JSON.stringify(resolvedGeometry ?? null);
1727
+ const geometryKeyRef = useRef9(geometryKey);
1728
+ useEffect6(() => {
1729
+ if (geometryKeyRef.current === geometryKey) return;
1730
+ geometryKeyRef.current = geometryKey;
1731
+ void recomputeRef.current({ blank: false });
1732
+ }, [geometryKey]);
1733
+ const imageCtx = useMultiImageOverlay(layoutCtx.layout, spreadCtx.spreadIdx, {
1734
+ onUpdate: () => spreadCtx.refresh()
1735
+ });
1736
+ const annotationRects = useMemo4(() => {
1737
+ if (!(annotations && layoutCtx.layout)) return [];
1738
+ const result = [];
1739
+ for (const annotation of annotations) {
1740
+ if (annotation.chapter !== chapter) continue;
1741
+ const rects = layoutCtx.layout.selectionRects({
1742
+ start: annotation.start,
1743
+ end: annotation.end
1744
+ });
1745
+ for (const rect of rects) result.push(rect);
1746
+ }
1747
+ return result;
1748
+ }, [annotations, layoutCtx.layout, chapter]);
1749
+ const clearImagesRef = useRef9(imageCtx.clearImages);
1750
+ clearImagesRef.current = imageCtx.clearImages;
1751
+ const onLoadRef = useRef9(onLoad);
1752
+ onLoadRef.current = onLoad;
1753
+ useEffect6(() => {
1754
+ if (!controlled) return;
1755
+ if (chapterProp == null) setChapterState(0);
1756
+ clearImagesRef.current();
1757
+ book.clearCache();
1758
+ if (epubProp) onLoadRef.current?.(epubProp);
1759
+ }, [controlled, epubProp, book, chapterProp]);
1760
+ const spreadGoToRef = useRef9(spreadCtx.goTo);
1761
+ spreadGoToRef.current = spreadCtx.goTo;
1762
+ useEffect6(() => {
1763
+ if (spreadIdxProp == null) return;
1764
+ spreadGoToRef.current(spreadIdxProp);
1765
+ }, [spreadIdxProp]);
1766
+ useLayoutEffect4(() => {
1767
+ const next = spreadIdxPropRef.current;
1768
+ if (next == null || !layoutCtx.layout) return;
1769
+ setSpreadRef.current(next);
1770
+ }, [layoutCtx.layout]);
1771
+ const onChapter = useCallback6(
1772
+ (i) => {
1773
+ if (i === chapter) return;
1774
+ if (chapterProp == null) setChapterState(i);
1775
+ onChapterChange?.(i);
1776
+ },
1777
+ [chapter, chapterProp, onChapterChange]
1778
+ );
1779
+ const listenersRef = useRef9(/* @__PURE__ */ new Map());
1780
+ const emit = useCallback6(
1781
+ (event, payload) => {
1782
+ const set = listenersRef.current.get(event);
1783
+ if (!set) return;
1784
+ for (const cb of set) cb(payload);
1785
+ },
1786
+ []
1787
+ );
1788
+ const pendingAnchorRef = useRef9(null);
1789
+ const layout = layoutCtx.layout;
1790
+ const tryApplyPendingAnchor = useCallback6(() => {
1791
+ const pending = pendingAnchorRef.current;
1792
+ if (!pending) return;
1793
+ if (pending.anchor.chapter !== chapter) return;
1794
+ if (!layout) return;
1795
+ const loc = layout.locateAnchor({
1796
+ paragraph: pending.anchor.paragraph,
1797
+ charIndex: pending.anchor.charIndex
1798
+ });
1799
+ if (!loc) return;
1800
+ pendingAnchorRef.current = null;
1801
+ spreadGoToRef.current(loc.spreadIdx);
1802
+ pending.resolve();
1803
+ }, [chapter, layout]);
1804
+ useEffect6(() => {
1805
+ tryApplyPendingAnchor();
1806
+ }, [tryApplyPendingAnchor]);
1807
+ useEffect6(
1808
+ () => () => {
1809
+ pendingAnchorRef.current?.resolve();
1810
+ pendingAnchorRef.current = null;
1811
+ },
1812
+ []
1813
+ );
1814
+ const didMountRef = useRef9(false);
1815
+ const dwellRef = useRef9(null);
1816
+ const onPageReadRef = useRef9(onPageRead);
1817
+ onPageReadRef.current = onPageRead;
1818
+ const onChapterCompletedRef = useRef9(onChapterCompleted);
1819
+ onChapterCompletedRef.current = onChapterCompleted;
1820
+ useEffect6(() => {
1821
+ const now = typeof performance !== "undefined" ? performance.now() : Date.now();
1822
+ if (didMountRef.current && dwellRef.current) {
1823
+ onPageReadRef.current?.(dwellRef.current.anchor, now - dwellRef.current.ts);
1824
+ }
1825
+ if (layout) {
1826
+ const inCh = layout.anchorAt(spreadCtx.spreadIdx, "right");
1827
+ dwellRef.current = inCh ? { anchor: { chapter, ...inCh }, ts: now } : null;
1828
+ } else {
1829
+ dwellRef.current = null;
1830
+ }
1831
+ if (!didMountRef.current) {
1832
+ didMountRef.current = true;
1833
+ return;
1834
+ }
1835
+ emit("spreadChanged", { chapter, spreadIdx: spreadCtx.spreadIdx });
1836
+ if (spreadCtx.totalSpreads > 0 && spreadCtx.spreadIdx === spreadCtx.totalSpreads - 1) {
1837
+ emit("chapterFinished", { chapter });
1838
+ onChapterCompletedRef.current?.(chapter);
1839
+ }
1840
+ }, [chapter, spreadCtx.spreadIdx, spreadCtx.totalSpreads, layout, emit]);
1841
+ const prevTurningRef = useRef9(false);
1842
+ useEffect6(() => {
1843
+ if (spreadCtx.turning && !prevTurningRef.current) {
1844
+ emit("turnStart", { from: spreadCtx.spreadIdx });
1845
+ } else if (!spreadCtx.turning && prevTurningRef.current) {
1846
+ emit("turnEnd", { to: spreadCtx.spreadIdx });
1847
+ }
1848
+ prevTurningRef.current = spreadCtx.turning;
1849
+ }, [spreadCtx.turning, spreadCtx.spreadIdx, emit]);
1850
+ useImperativeHandle(
1851
+ ref,
1852
+ () => ({
1853
+ goToSpread: (i) => spreadGoToRef.current(i),
1854
+ next: () => spreadCtx.next(),
1855
+ prev: () => spreadCtx.prev(),
1856
+ goToChapter: (i) => onChapter(i),
1857
+ getReadingPosition: () => ({
1858
+ chapter,
1859
+ spreadIdx: spreadCtx.spreadIdx,
1860
+ totalPages: spreadCtx.totalPages,
1861
+ totalSpreads: spreadCtx.totalSpreads
1862
+ }),
1863
+ goToAnchor: (anchor) => new Promise((resolve) => {
1864
+ pendingAnchorRef.current?.resolve();
1865
+ pendingAnchorRef.current = { anchor, resolve };
1866
+ if (anchor.chapter !== chapter) onChapter(anchor.chapter);
1867
+ tryApplyPendingAnchor();
1868
+ }),
1869
+ getAnchor: () => {
1870
+ if (!layout) return null;
1871
+ const inCh = layout.anchorAt(spreadCtx.spreadIdx, "right");
1872
+ return inCh ? { chapter, ...inCh } : null;
1873
+ },
1874
+ getVisibleRange: () => {
1875
+ if (!layout) return null;
1876
+ const start = layout.anchorAt(spreadCtx.spreadIdx, "right");
1877
+ if (!start) return null;
1878
+ const next = layout.anchorAt(spreadCtx.spreadIdx + 1, "right");
1879
+ let end;
1880
+ if (next) {
1881
+ end = next;
1882
+ } else {
1883
+ const ch = e?.chapters[chapter];
1884
+ const lastP = (ch?.paragraphs.length ?? 1) - 1;
1885
+ const lastText = ch?.paragraphs[lastP]?.text ?? "";
1886
+ end = { paragraph: Math.max(0, lastP), charIndex: [...lastText].length };
1887
+ }
1888
+ return {
1889
+ start: { chapter, ...start },
1890
+ end: { chapter, ...end }
1891
+ };
1892
+ },
1893
+ setOptions: (partial) => setOptions(partial),
1894
+ subscribe: (event, listener) => {
1895
+ let set = listenersRef.current.get(event);
1896
+ if (!set) {
1897
+ set = /* @__PURE__ */ new Set();
1898
+ listenersRef.current.set(event, set);
1899
+ }
1900
+ set.add(listener);
1901
+ return () => {
1902
+ listenersRef.current.get(event)?.delete(listener);
1903
+ };
1904
+ }
1905
+ }),
1906
+ [
1907
+ spreadCtx.next,
1908
+ spreadCtx.prev,
1909
+ spreadCtx.spreadIdx,
1910
+ spreadCtx.totalPages,
1911
+ spreadCtx.totalSpreads,
1912
+ onChapter,
1913
+ chapter,
1914
+ layout,
1915
+ e,
1916
+ setOptions,
1917
+ tryApplyPendingAnchor
1918
+ ]
1919
+ );
1920
+ const editable = {
1921
+ fontFamily: bookOptions.fontFamily,
1922
+ fontSize: bookOptions.fontSize,
1923
+ lineSpacing: bookOptions.lineSpacing ?? 1.8,
1924
+ mode: bookOptions.mode ?? "strict",
1925
+ enableHanging: bookOptions.enableHanging ?? true
1926
+ };
1927
+ const fontLabel = (() => {
1928
+ const css = normalizeFontFamily5(bookOptions.fontFamily);
1929
+ const f = fonts?.find((x) => x.value === css);
1930
+ const name = f?.label ?? css;
1931
+ return `${name} ${bookOptions.fontSize}px`;
1932
+ })();
1933
+ const showChapterSelect = e && enableChapterNav && (chapterNavMode === "select" || chapterNavMode === "both");
1934
+ const showChapterPanel = e && enableChapterNav && (chapterNavMode === "panel" || chapterNavMode === "both");
1935
+ const runningTitleRight = e ? e.author ? `${e.author} ${e.title}` : e.title : "";
1936
+ const runningTitleLeft = e?.chapters[chapter]?.title ?? "";
1937
+ const layoutReady = e && spreadCtx.spread && layoutCtx.layout && layoutCtx.pageWidth > 0;
1938
+ const themeName = typeof theme === "string" ? theme : theme.name;
1939
+ const themeOverride = typeof theme === "string" ? void 0 : theme.override;
1940
+ const themeStyle = useMemo4(() => {
1941
+ const pad = pageGeometryProp?.padding;
1942
+ const styleVars = {};
1943
+ if (pad?.x != null) styleVars["--mejiro-page-pad-x"] = `${pad.x}px`;
1944
+ if (pad?.y != null) styleVars["--mejiro-page-pad-y"] = `${pad.y}px`;
1945
+ if (pad?.bottom != null) styleVars["--mejiro-page-pad-bottom"] = `${pad.bottom}px`;
1946
+ if (fit === "width") styleVars["--mejiro-surface-aspect"] = surfaceAspect;
1947
+ const hasVars = Object.keys(styleVars).length > 0;
1948
+ if (!(themeOverride || hasVars)) return void 0;
1949
+ return { ...themeOverride, ...styleVars };
1950
+ }, [themeOverride, pageGeometryProp, fit, surfaceAspect]);
1951
+ const resolvedMessages = useI18n({ locale, messages });
1952
+ 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 })
1956
+ ] });
1957
+ const header = enableHeader ? /* @__PURE__ */ jsxs10("header", { className: "mejiro-reader-header", children: [
1958
+ /* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-header-left", children: [
1959
+ logo === void 0 ? defaultLogo : logo,
1960
+ showChapterSelect && /* @__PURE__ */ jsx12(MejiroChapterNav, { epub: e, chapter, onChange: onChapter })
1961
+ ] }),
1962
+ /* @__PURE__ */ jsxs10("div", { className: "mejiro-reader-header-actions", children: [
1963
+ enableStats && /* @__PURE__ */ jsx12(
1964
+ MejiroStats,
1965
+ {
1966
+ chapter: e?.chapters[chapter] ?? null,
1967
+ totalPages: layoutCtx.layout?.totalPages ?? 0,
1968
+ elapsedMs: layoutCtx.elapsedMs,
1969
+ fontLabel
1970
+ }
1971
+ ),
1972
+ enableDropZone && /* @__PURE__ */ jsx12(
1973
+ "button",
1974
+ {
1975
+ type: "button",
1976
+ className: "mejiro-reader-btn",
1977
+ onClick: () => fileRef.current?.click(),
1978
+ children: resolvedMessages.openButton
1979
+ }
1980
+ ),
1981
+ enableImageOverlay && e && /* @__PURE__ */ jsx12(
1982
+ "button",
1983
+ {
1984
+ type: "button",
1985
+ className: `mejiro-reader-btn${imageCtx.hasImages ? " is-active" : ""}`,
1986
+ onClick: () => imageCtx.addImage(),
1987
+ children: resolvedMessages.imageButton
1988
+ }
1989
+ ),
1990
+ enableSettings && /* @__PURE__ */ jsxs10(
1991
+ "button",
1992
+ {
1993
+ type: "button",
1994
+ className: `mejiro-reader-btn${settingsOpen ? " is-active" : ""}`,
1995
+ onClick: () => setSettingsOpen((v) => !v),
1996
+ children: [
1997
+ resolvedMessages.settingsButton,
1998
+ /* @__PURE__ */ jsx12("span", { className: "mejiro-reader-btn-arrow", children: "\u25BE" })
1999
+ ]
2000
+ }
2001
+ )
2002
+ ] })
2003
+ ] }) : null;
2004
+ const currentSpread = spreadCtx.spreadIdx;
2005
+ const rightPage = currentSpread * 2 + 1;
2006
+ const leftPage = currentSpread * 2 + 2;
2007
+ const showLeft = spreadCtx.spread != null && leftPage <= spreadCtx.spread.totalPages;
2008
+ const showRightNum = pageNumbers === "both" || pageNumbers === "right";
2009
+ const showLeftNum = pageNumbers === "both" || pageNumbers === "left";
2010
+ return /* @__PURE__ */ jsx12(MejiroI18nProvider, { messages: resolvedMessages, children: /* @__PURE__ */ jsxs10(
2011
+ "div",
2012
+ {
2013
+ className: `mejiro-reader${chromeHidden ? " mejiro-reader--chrome-hidden" : ""}${fit === "width" ? " mejiro-reader--fit-width" : ""}`,
2014
+ "data-mejiro-theme": themeName,
2015
+ style: themeStyle,
2016
+ children: [
2017
+ 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({
2019
+ settings: editable,
2020
+ update: setOptions,
2021
+ open: settingsOpen,
2022
+ toggle: () => setSettingsOpen((v) => !v)
2023
+ }) }) }) }) : /* @__PURE__ */ jsx12(
2024
+ MejiroSettingsPanel,
2025
+ {
2026
+ open: settingsOpen,
2027
+ settings: editable,
2028
+ fonts,
2029
+ onChange: setOptions
2030
+ }
2031
+ )),
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(
2039
+ MejiroScrollView,
2040
+ {
2041
+ layout: layoutCtx.layout,
2042
+ pageWidth: layoutCtx.pageWidth,
2043
+ pageHeight: layoutCtx.pageHeight,
2044
+ contentHeight: layoutCtx.contentHeight,
2045
+ fontFamily: bookOptions.fontFamily,
2046
+ fontSize: bookOptions.fontSize,
2047
+ lineSpacing: bookOptions.lineSpacing,
2048
+ scrollToPage: spreadCtx.spreadIdx * 2,
2049
+ onVisiblePageChange: (pageIdx) => {
2050
+ const target = Math.floor(pageIdx / 2);
2051
+ if (target !== spreadCtx.spreadIdx) spreadGoToRef.current(target);
2052
+ }
2053
+ }
2054
+ ),
2055
+ layoutReady && mode === "paginated" && spreadCtx.spread && /* @__PURE__ */ jsx12(
2056
+ MejiroSpread,
2057
+ {
2058
+ singlePage: effectiveSingle,
2059
+ onSwipe: (dir) => dir === "next" ? spreadCtx.next() : spreadCtx.prev(),
2060
+ onSurfaceTap: enableSurfaceTap ? () => setChromeHidden((v) => !v) : void 0,
2061
+ spread: spreadCtx.spread,
2062
+ pageWidth: layoutCtx.pageWidth,
2063
+ pageHeight: layoutCtx.pageHeight,
2064
+ contentHeight: layoutCtx.contentHeight,
2065
+ fontFamily: bookOptions.fontFamily,
2066
+ fontSize: bookOptions.fontSize,
2067
+ lineSpacing: bookOptions.lineSpacing,
2068
+ turning: spreadCtx.turning,
2069
+ rightHeader: {
2070
+ title: runningTitleRight,
2071
+ pageNumber: showRightNum ? rightPage : null
2072
+ },
2073
+ leftHeader: {
2074
+ title: runningTitleLeft,
2075
+ pageNumber: showLeft && showLeftNum ? leftPage : null
2076
+ },
2077
+ images: imageCtx.currentImages,
2078
+ indicator: enablePageIndicator ? /* @__PURE__ */ jsx12(
2079
+ MejiroPageIndicator,
2080
+ {
2081
+ current: spreadCtx.spreadIdx + 1,
2082
+ total: spreadCtx.totalSpreads
2083
+ }
2084
+ ) : null,
2085
+ onPrev: spreadCtx.prev,
2086
+ onNext: spreadCtx.next,
2087
+ onImagePointerDown: imageCtx.onOverlayPointerDown,
2088
+ onImageResizePointerDown: imageCtx.onResizePointerDown,
2089
+ onImageClose: imageCtx.removeImage,
2090
+ selectionRects: annotationRects.length ? annotationRects : void 0
2091
+ },
2092
+ `${chapter}-${spreadCtx.spreadIdx}-${layoutCtx.pageWidth}x${layoutCtx.pageHeight}`
2093
+ )
2094
+ ] })
2095
+ ] }),
2096
+ /* @__PURE__ */ jsx12(
2097
+ "input",
2098
+ {
2099
+ ref: fileRef,
2100
+ type: "file",
2101
+ accept: ".epub",
2102
+ hidden: true,
2103
+ onChange: (ev) => {
2104
+ const file = ev.target.files?.[0];
2105
+ if (file) void epubCtx.loadFile(file);
2106
+ }
2107
+ }
2108
+ ),
2109
+ /* @__PURE__ */ jsx12("div", { className: "mejiro-reader-sr-only", role: "status", "aria-live": "polite", children: spreadCtx.totalSpreads > 0 ? format(resolvedMessages.spreadAnnouncement, {
2110
+ spread: spreadCtx.spreadIdx + 1,
2111
+ total: spreadCtx.totalSpreads
2112
+ }) : "" })
2113
+ ]
2114
+ }
2115
+ ) });
2116
+ }
2117
+ var MejiroReader = forwardRef(MejiroReaderInner);
2118
+ MejiroReader.displayName = "MejiroReader";
2119
+
2120
+ // src/MejiroEditor.tsx
2121
+ import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2122
+ function cloneBook(book) {
2123
+ return {
2124
+ ...book,
2125
+ chapters: book.chapters.map((chapter) => ({
2126
+ ...chapter,
2127
+ blocks: chapter.blocks.map(
2128
+ (block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
2129
+ ),
2130
+ imageAssets: new Map(chapter.imageAssets),
2131
+ originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
2132
+ paragraphs: chapter.paragraphs.map((paragraph) => ({
2133
+ ...paragraph,
2134
+ inlineAnnotations: [...paragraph.inlineAnnotations]
2135
+ })),
2136
+ paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
2137
+ })),
2138
+ packageData: {
2139
+ ...book.packageData,
2140
+ files: new Map(book.packageData.files)
2141
+ }
2142
+ };
2143
+ }
2144
+ function utf16ToCodepoint(text, utf16Offset) {
2145
+ let cp = 0;
2146
+ let i = 0;
2147
+ while (i < utf16Offset && i < text.length) {
2148
+ const ch = text.codePointAt(i);
2149
+ if (ch === void 0) break;
2150
+ i += ch > 65535 ? 2 : 1;
2151
+ cp++;
2152
+ }
2153
+ return cp;
2154
+ }
2155
+ function applyWatermark(editor, watermark) {
2156
+ editor.transaction(() => {
2157
+ for (let i = 0; i < editor.chapters.length; i++) {
2158
+ editor.insertParagraph(i, 0, {
2159
+ text: `[mejiro-watermark] ${watermark.text}`,
2160
+ inlineAnnotations: []
2161
+ });
2162
+ }
2163
+ });
2164
+ }
2165
+ function MejiroEditor({
2166
+ epubUrl,
2167
+ fonts,
2168
+ enableProofread = true,
2169
+ enableRuby = true,
2170
+ enableImages = true,
2171
+ enableExport = true,
2172
+ onBeforeExport,
2173
+ exportPolicy,
2174
+ assetResolver,
2175
+ onLoad,
2176
+ onExport,
2177
+ onError
2178
+ }) {
2179
+ const messages = useI18n();
2180
+ const imageInputRef = useRef10(null);
2181
+ const textareaRef = useRef10(null);
2182
+ const [editor, setEditor] = useState8(null);
2183
+ const [loading, setLoading] = useState8(false);
2184
+ const [error, setError] = useState8(null);
2185
+ const [revision, setRevision] = useState8(0);
2186
+ const [selection, setSelection] = useState8({ chapter: 0, paragraph: 0 });
2187
+ const [text, setText] = useState8("");
2188
+ const [rubyStart, setRubyStart] = useState8(0);
2189
+ const [rubyEnd, setRubyEnd] = useState8(1);
2190
+ const [rubyText, setRubyText] = useState8("");
2191
+ const loadRequestIdRef = useRef10(0);
2192
+ const onLoadRef = useRef10(onLoad);
2193
+ const onErrorRef = useRef10(onError);
2194
+ const book = editor?.book ?? null;
2195
+ const chapter = book?.chapters[selection.chapter] ?? null;
2196
+ const paragraph = chapter?.paragraphs[selection.paragraph] ?? null;
2197
+ const previewBook = book ? cloneBook(book) : null;
2198
+ void revision;
2199
+ useEffect7(() => {
2200
+ onLoadRef.current = onLoad;
2201
+ }, [onLoad]);
2202
+ useEffect7(() => {
2203
+ onErrorRef.current = onError;
2204
+ }, [onError]);
2205
+ const loadBufferForRequest = useCallback7(async (buffer, requestId) => {
2206
+ setLoading(true);
2207
+ setError(null);
2208
+ try {
2209
+ const next = await EditableEpub.load(buffer);
2210
+ if (requestId !== loadRequestIdRef.current) return;
2211
+ setEditor(next);
2212
+ setSelection({ chapter: 0, paragraph: 0 });
2213
+ setRevision((value) => value + 1);
2214
+ onLoadRef.current?.(next);
2215
+ } catch (err) {
2216
+ if (requestId === loadRequestIdRef.current) {
2217
+ const nextError = err instanceof Error ? err : new Error(String(err));
2218
+ setError(nextError);
2219
+ onErrorRef.current?.(nextError);
2220
+ }
2221
+ } finally {
2222
+ if (requestId === loadRequestIdRef.current) setLoading(false);
2223
+ }
2224
+ }, []);
2225
+ async function loadFile(file) {
2226
+ const requestId = ++loadRequestIdRef.current;
2227
+ setLoading(true);
2228
+ setError(null);
2229
+ try {
2230
+ await loadBufferForRequest(await file.arrayBuffer(), requestId);
2231
+ } catch (err) {
2232
+ if (requestId === loadRequestIdRef.current) {
2233
+ const nextError = err instanceof Error ? err : new Error(String(err));
2234
+ setError(nextError);
2235
+ onErrorRef.current?.(nextError);
2236
+ setLoading(false);
2237
+ }
2238
+ }
2239
+ }
2240
+ useEffect7(() => {
2241
+ if (!epubUrl) return;
2242
+ let cancelled = false;
2243
+ const requestId = ++loadRequestIdRef.current;
2244
+ void (async () => {
2245
+ setLoading(true);
2246
+ setError(null);
2247
+ try {
2248
+ const res = await fetch(epubUrl);
2249
+ if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
2250
+ const buffer = await res.arrayBuffer();
2251
+ if (cancelled || requestId !== loadRequestIdRef.current) return;
2252
+ await loadBufferForRequest(buffer, requestId);
2253
+ } catch (err) {
2254
+ if (!cancelled && requestId === loadRequestIdRef.current) {
2255
+ const nextError = err instanceof Error ? err : new Error(String(err));
2256
+ setError(nextError);
2257
+ onErrorRef.current?.(nextError);
2258
+ }
2259
+ } finally {
2260
+ if (!cancelled && requestId === loadRequestIdRef.current) setLoading(false);
2261
+ }
2262
+ })();
2263
+ return () => {
2264
+ cancelled = true;
2265
+ };
2266
+ }, [epubUrl, loadBufferForRequest]);
2267
+ useEffect7(() => {
2268
+ setText(paragraph?.text ?? "");
2269
+ setRubyStart(0);
2270
+ setRubyEnd(Math.min(1, [...paragraph?.text ?? ""].length));
2271
+ setRubyText("");
2272
+ }, [paragraph]);
2273
+ function selectParagraph(chapterIndex, paragraphIndex) {
2274
+ setSelection({ chapter: chapterIndex, paragraph: paragraphIndex });
2275
+ }
2276
+ function captureRubyRange() {
2277
+ const el = textareaRef.current;
2278
+ if (!el) return;
2279
+ const utf16Start = el.selectionStart ?? 0;
2280
+ const utf16End = el.selectionEnd ?? utf16Start;
2281
+ const start = utf16ToCodepoint(text, utf16Start);
2282
+ const end = utf16ToCodepoint(text, Math.max(utf16End, utf16Start + 1));
2283
+ setRubyStart(start);
2284
+ setRubyEnd(Math.max(start + 1, end));
2285
+ }
2286
+ function applyText() {
2287
+ if (!editor) return;
2288
+ editor.updateParagraph(selection.chapter, selection.paragraph, { text });
2289
+ setRevision((value) => value + 1);
2290
+ }
2291
+ function applyRuby() {
2292
+ if (!(editor && paragraph && rubyText.trim())) return;
2293
+ const len = [...text].length;
2294
+ const start = Math.max(0, Math.min(rubyStart, len));
2295
+ const end = Math.max(start + 1, Math.min(rubyEnd, len));
2296
+ const newRuby = {
2297
+ kind: "ruby",
2298
+ startIndex: start,
2299
+ endIndex: end,
2300
+ rubyText: rubyText.trim(),
2301
+ type: end - start === 1 ? "mono" : "group"
2302
+ };
2303
+ const nextInline = [
2304
+ ...paragraph.inlineAnnotations.filter(
2305
+ (ann) => ann.endIndex <= start || ann.startIndex >= end
2306
+ ),
2307
+ newRuby
2308
+ ].sort((a, b) => a.startIndex - b.startIndex);
2309
+ editor.updateParagraph(selection.chapter, selection.paragraph, {
2310
+ text,
2311
+ inlineAnnotations: nextInline
2312
+ });
2313
+ setRubyText("");
2314
+ setRevision((value) => value + 1);
2315
+ }
2316
+ async function addImage(file) {
2317
+ if (!(editor && chapter)) return;
2318
+ editor.addImage(selection.chapter, {
2319
+ filename: file.name,
2320
+ mediaType: file.type || "application/octet-stream",
2321
+ data: await file.arrayBuffer(),
2322
+ alt: file.name,
2323
+ afterBlockId: paragraphBlockId(chapter, selection.paragraph)
2324
+ });
2325
+ setRevision((value) => value + 1);
2326
+ }
2327
+ async function exportEpub() {
2328
+ if (!editor) return;
2329
+ if (exportPolicy?.watermark) applyWatermark(editor, exportPolicy.watermark);
2330
+ let buffer = await editor.export(assetResolver ? { assetResolver } : void 0);
2331
+ if (exportPolicy?.encrypt) buffer = await exportPolicy.encrypt(buffer);
2332
+ const decision = await onBeforeExport?.(buffer);
2333
+ onExport?.(buffer);
2334
+ if (decision === false) return;
2335
+ if (exportPolicy?.allowDownload === false) return;
2336
+ const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2337
+ const a = document.createElement("a");
2338
+ a.href = url;
2339
+ a.download = `${editor.title || "edited"}.epub`;
2340
+ a.click();
2341
+ URL.revokeObjectURL(url);
2342
+ }
2343
+ return /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor", children: [
2344
+ /* @__PURE__ */ jsxs11("main", { className: "mejiro-editor-preview", children: [
2345
+ previewBook ? /* @__PURE__ */ jsx13(
2346
+ MejiroReader,
2347
+ {
2348
+ epub: previewBook,
2349
+ fonts,
2350
+ subtitle: messages.editorPreviewSubtitle,
2351
+ chapterNavMode: "panel",
2352
+ enableImageOverlay: false,
2353
+ enableSurfaceTap: false
2354
+ }
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 })
2358
+ ] }),
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 })
2364
+ ] }),
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(
2370
+ "button",
2371
+ {
2372
+ type: "button",
2373
+ className: selection.chapter === ci && selection.paragraph === pi ? "is-active" : "",
2374
+ onClick: () => selectParagraph(ci, pi),
2375
+ children: [
2376
+ /* @__PURE__ */ jsx13("span", { children: ch.title ?? format(messages.chapterN, { n: ci + 1 }) }),
2377
+ /* @__PURE__ */ jsx13("strong", { children: block.text.slice(0, 42) })
2378
+ ]
2379
+ },
2380
+ `${ch.href}-${block.id}`
2381
+ ))
2382
+ ) })
2383
+ ] }),
2384
+ enableProofread && /* @__PURE__ */ jsxs11("div", { className: "mejiro-editor-section", children: [
2385
+ /* @__PURE__ */ jsx13("span", { className: "mejiro-editor-label", children: messages.editorProofread }),
2386
+ /* @__PURE__ */ jsx13(
2387
+ "textarea",
2388
+ {
2389
+ ref: textareaRef,
2390
+ value: text,
2391
+ onChange: (event) => setText(event.target.value),
2392
+ onSelect: captureRubyRange
2393
+ }
2394
+ ),
2395
+ /* @__PURE__ */ jsx13("button", { type: "button", className: "mejiro-editor-primary", onClick: applyText, children: messages.editorApplyText })
2396
+ ] }),
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, {
2401
+ start: rubyStart,
2402
+ end: rubyEnd,
2403
+ count: Math.max(0, rubyEnd - rubyStart)
2404
+ }) }),
2405
+ /* @__PURE__ */ jsx13(
2406
+ "input",
2407
+ {
2408
+ value: rubyText,
2409
+ placeholder: messages.editorRubyPlaceholder,
2410
+ onChange: (event) => setRubyText(event.target.value)
2411
+ }
2412
+ ),
2413
+ /* @__PURE__ */ jsx13("button", { type: "button", className: "mejiro-editor-primary", onClick: applyRuby, children: messages.editorApplyRuby })
2414
+ ] }),
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(
2419
+ "input",
2420
+ {
2421
+ ref: imageInputRef,
2422
+ type: "file",
2423
+ accept: "image/*",
2424
+ hidden: true,
2425
+ onChange: (event) => {
2426
+ const file = event.target.files?.[0];
2427
+ if (file) void addImage(file);
2428
+ }
2429
+ }
2430
+ )
2431
+ ] }),
2432
+ enableExport && /* @__PURE__ */ jsx13(
2433
+ "button",
2434
+ {
2435
+ type: "button",
2436
+ className: "mejiro-editor-export",
2437
+ onClick: () => void exportEpub(),
2438
+ children: messages.editorExportEpub
2439
+ }
2440
+ )
2441
+ ] })
2442
+ ] })
2443
+ ] });
2444
+ }
2445
+ function paragraphBlockId(chapter, paragraphIndex) {
2446
+ let current = 0;
2447
+ for (const block of chapter.blocks) {
2448
+ if (block.kind !== "paragraph") continue;
2449
+ if (current === paragraphIndex) return block.id;
2450
+ current++;
2451
+ }
2452
+ return void 0;
2453
+ }
2454
+
2455
+ // src/MejiroManuscriptEditor.tsx
2456
+ import { EpubProject } from "@libraz/mejiro/epub";
2457
+ import { useCallback as useCallback9, useEffect as useEffect9, useRef as useRef12, useState as useState10 } from "react";
2458
+
2459
+ // src/useManuscriptDraft.ts
2460
+ import { useCallback as useCallback8, useEffect as useEffect8, useRef as useRef11, useState as useState9 } from "react";
2461
+ var DEFAULT_DELAY = 800;
2462
+ function defaultChapter(index) {
2463
+ return { id: `chapter-${Date.now()}-${index}`, title: `\u7B2C${index + 1}\u8A71`, body: "" };
2464
+ }
2465
+ function useManuscriptDraft(options = {}) {
2466
+ const { onAutosave, autosaveDelay = DEFAULT_DELAY } = options;
2467
+ const [chapters, setChaptersState] = useState9(
2468
+ () => options.initialChapters?.length ? options.initialChapters : [defaultChapter(0)]
2469
+ );
2470
+ const [selected, setSelectedState] = useState9(0);
2471
+ const saveRef = useRef11(onAutosave);
2472
+ saveRef.current = onAutosave;
2473
+ const didMountRef = useRef11(false);
2474
+ useEffect8(() => {
2475
+ if (!didMountRef.current) {
2476
+ didMountRef.current = true;
2477
+ return void 0;
2478
+ }
2479
+ const callback = saveRef.current;
2480
+ if (!callback) return void 0;
2481
+ const timer = setTimeout(() => {
2482
+ void callback(chapters);
2483
+ }, autosaveDelay);
2484
+ return () => clearTimeout(timer);
2485
+ }, [chapters, autosaveDelay]);
2486
+ const setSelected = useCallback8(
2487
+ (index) => {
2488
+ setSelectedState(() => Math.max(0, Math.min(index, chapters.length - 1)));
2489
+ },
2490
+ [chapters.length]
2491
+ );
2492
+ const setChapters = useCallback8(
2493
+ (next) => {
2494
+ const normalized = next.length ? next : [defaultChapter(0)];
2495
+ setChaptersState(normalized);
2496
+ setSelectedState((current) => {
2497
+ const selectedId = chapters[current]?.id;
2498
+ const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
2499
+ return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
2500
+ });
2501
+ },
2502
+ [chapters]
2503
+ );
2504
+ const patchChapter = useCallback8((index, patch) => {
2505
+ setChaptersState(
2506
+ (current) => current.map((chapter, i) => i === index ? { ...chapter, ...patch } : chapter)
2507
+ );
2508
+ }, []);
2509
+ const addChapter = useCallback8((chapter = {}) => {
2510
+ setChaptersState((current) => {
2511
+ const generated = defaultChapter(current.length);
2512
+ const next = [
2513
+ ...current,
2514
+ {
2515
+ id: chapter.id ?? generated.id,
2516
+ title: chapter.title ?? generated.title,
2517
+ body: chapter.body ?? generated.body
2518
+ }
2519
+ ];
2520
+ setSelectedState(next.length - 1);
2521
+ return next;
2522
+ });
2523
+ }, []);
2524
+ const removeChapter = useCallback8((index) => {
2525
+ setChaptersState((current) => {
2526
+ if (current.length <= 1) return current;
2527
+ const next = current.filter((_, i) => i !== index);
2528
+ setSelectedState((prev) => {
2529
+ if (prev === index) return Math.max(0, Math.min(index, next.length - 1));
2530
+ if (index < prev) return prev - 1;
2531
+ return Math.max(0, Math.min(prev, next.length - 1));
2532
+ });
2533
+ return next;
2534
+ });
2535
+ }, []);
2536
+ const reorderChapters = useCallback8((from, to) => {
2537
+ setChaptersState((current) => {
2538
+ if (from < 0 || from >= current.length) return current;
2539
+ const next = [...current];
2540
+ const [moved] = next.splice(from, 1);
2541
+ const target = Math.max(0, Math.min(next.length, to));
2542
+ next.splice(target, 0, moved);
2543
+ setSelectedState((prev) => {
2544
+ if (prev === from) return target;
2545
+ if (from < prev && target >= prev) return prev - 1;
2546
+ if (from > prev && target <= prev) return prev + 1;
2547
+ return prev;
2548
+ });
2549
+ return next;
2550
+ });
2551
+ }, []);
2552
+ return {
2553
+ chapters,
2554
+ selected,
2555
+ setSelected,
2556
+ setChapters,
2557
+ patchChapter,
2558
+ addChapter,
2559
+ removeChapter,
2560
+ reorderChapters
2561
+ };
2562
+ }
2563
+
2564
+ // src/MejiroManuscriptEditor.tsx
2565
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2566
+ function downloadEpub(buffer, title) {
2567
+ const url = URL.createObjectURL(new Blob([buffer], { type: "application/epub+zip" }));
2568
+ const a = document.createElement("a");
2569
+ a.href = url;
2570
+ a.download = `${title || "book"}.epub`;
2571
+ a.click();
2572
+ URL.revokeObjectURL(url);
2573
+ }
2574
+ function coverExtension(mediaType) {
2575
+ switch (mediaType) {
2576
+ case "image/gif":
2577
+ return ".gif";
2578
+ case "image/jpeg":
2579
+ return ".jpg";
2580
+ case "image/png":
2581
+ return ".png";
2582
+ case "image/svg+xml":
2583
+ return ".svg";
2584
+ case "image/webp":
2585
+ return ".webp";
2586
+ default:
2587
+ return ".bin";
2588
+ }
2589
+ }
2590
+ function coverAssetHref(file) {
2591
+ const filename = file.name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
2592
+ return `OPS/Images/${filename && !/^\.+$/u.test(filename) ? filename : `cover${coverExtension(file.type)}`}`;
2593
+ }
2594
+ function defaultChapter2(messages) {
2595
+ return {
2596
+ id: `chapter-${Date.now()}-0`,
2597
+ title: format(messages.manuscriptDefaultChapterTitle, { n: 1 }),
2598
+ body: messages.manuscriptDefaultBody
2599
+ };
2600
+ }
2601
+ function MejiroManuscriptEditor({
2602
+ fonts,
2603
+ title: titleProp,
2604
+ onTitleChange,
2605
+ author: authorProp,
2606
+ onAuthorChange,
2607
+ cover: coverProp,
2608
+ onCoverChange,
2609
+ chapters: initialChapters,
2610
+ onAutosave,
2611
+ autosaveDelay,
2612
+ previewProps,
2613
+ assetResolver,
2614
+ onExport
2615
+ }) {
2616
+ const messages = useI18n();
2617
+ const coverInputRef = useRef12(null);
2618
+ const bodyTextareaRef = useRef12(null);
2619
+ const titleControlled = onTitleChange !== void 0;
2620
+ const authorControlled = onAuthorChange !== void 0;
2621
+ 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);
2625
+ const title = titleControlled ? titleProp ?? "" : titleState;
2626
+ const author = authorControlled ? authorProp ?? "" : authorState;
2627
+ const cover = coverControlled ? coverProp ?? null : coverState;
2628
+ useEffect9(() => {
2629
+ if (!titleControlled && titleProp !== void 0) setTitleState(titleProp);
2630
+ }, [titleControlled, titleProp]);
2631
+ useEffect9(() => {
2632
+ if (!authorControlled && authorProp !== void 0) setAuthorState(authorProp);
2633
+ }, [authorControlled, authorProp]);
2634
+ useEffect9(() => {
2635
+ if (!coverControlled && coverProp !== void 0) setCoverState(coverProp);
2636
+ }, [coverControlled, coverProp]);
2637
+ const setTitle = useCallback9(
2638
+ (next) => {
2639
+ if (!titleControlled) setTitleState(next);
2640
+ onTitleChange?.(next);
2641
+ },
2642
+ [titleControlled, onTitleChange]
2643
+ );
2644
+ const setAuthor = useCallback9(
2645
+ (next) => {
2646
+ if (!authorControlled) setAuthorState(next);
2647
+ onAuthorChange?.(next);
2648
+ },
2649
+ [authorControlled, onAuthorChange]
2650
+ );
2651
+ const setCover = useCallback9(
2652
+ (next) => {
2653
+ if (!coverControlled) setCoverState(next);
2654
+ onCoverChange?.(next);
2655
+ },
2656
+ [coverControlled, onCoverChange]
2657
+ );
2658
+ const draft = useManuscriptDraft({
2659
+ initialChapters: initialChapters?.length ? initialChapters : [defaultChapter2(messages)],
2660
+ onAutosave,
2661
+ autosaveDelay
2662
+ });
2663
+ const {
2664
+ chapters,
2665
+ selected,
2666
+ setSelected,
2667
+ patchChapter,
2668
+ addChapter,
2669
+ removeChapter,
2670
+ reorderChapters
2671
+ } = draft;
2672
+ const current = chapters[selected] ?? chapters[0];
2673
+ const [draggingIndex, setDraggingIndex] = useState10(null);
2674
+ function wrapSelection(open, close) {
2675
+ const el = bodyTextareaRef.current;
2676
+ if (!el) return;
2677
+ const start = el.selectionStart ?? el.value.length;
2678
+ const end = el.selectionEnd ?? start;
2679
+ const before = el.value.slice(0, start);
2680
+ const middle = el.value.slice(start, end);
2681
+ const after = el.value.slice(end);
2682
+ const next = `${before}${open}${middle}${close}${after}`;
2683
+ patchChapter(selected, { body: next });
2684
+ requestAnimationFrame(() => {
2685
+ if (!bodyTextareaRef.current) return;
2686
+ const caret = start + open.length + middle.length;
2687
+ bodyTextareaRef.current.focus();
2688
+ bodyTextareaRef.current.setSelectionRange(start + open.length, caret);
2689
+ });
2690
+ }
2691
+ const exportEpub = useCallback9(async () => {
2692
+ const project = EpubProject.fromManuscript({
2693
+ metadata: { title, author: author || void 0 },
2694
+ chapters: chapters.map((chapter) => ({
2695
+ id: chapter.id,
2696
+ title: chapter.title || messages.untitled,
2697
+ body: chapter.body
2698
+ }))
2699
+ });
2700
+ if (cover) {
2701
+ project.setCover({
2702
+ href: coverAssetHref(cover),
2703
+ mediaType: cover.type || void 0,
2704
+ data: await cover.arrayBuffer()
2705
+ });
2706
+ }
2707
+ const buffer = await project.export(assetResolver ? { assetResolver } : void 0);
2708
+ onExport?.(buffer);
2709
+ downloadEpub(buffer, title);
2710
+ }, [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(
2713
+ MejiroReader,
2714
+ {
2715
+ subtitle: messages.manuscriptPreviewSubtitle,
2716
+ chapterNavMode: "panel",
2717
+ enableSurfaceTap: false,
2718
+ ...previewProps,
2719
+ manuscript: chapters.map((chapter) => ({
2720
+ id: chapter.id,
2721
+ title: chapter.title || messages.untitled,
2722
+ body: chapter.body
2723
+ })),
2724
+ fonts,
2725
+ chapter: selected,
2726
+ onChapterChange: setSelected,
2727
+ enableImageOverlay: false
2728
+ }
2729
+ ) }),
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 })
2735
+ ] }),
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(
2742
+ "input",
2743
+ {
2744
+ ref: coverInputRef,
2745
+ type: "file",
2746
+ accept: "image/*",
2747
+ hidden: true,
2748
+ onChange: (event) => setCover(event.target.files?.[0] ?? null)
2749
+ }
2750
+ )
2751
+ ] }),
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(
2755
+ "button",
2756
+ {
2757
+ type: "button",
2758
+ className: selected === index ? "is-active" : "",
2759
+ draggable: true,
2760
+ "aria-label": format(messages.manuscriptReorderHandle, {
2761
+ title: chapter.title || messages.untitled
2762
+ }),
2763
+ onClick: () => setSelected(index),
2764
+ onDragStart: (event) => {
2765
+ setDraggingIndex(index);
2766
+ event.dataTransfer.effectAllowed = "move";
2767
+ },
2768
+ onDragEnd: () => setDraggingIndex(null),
2769
+ onDragOver: (event) => {
2770
+ if (draggingIndex === null || draggingIndex === index) return;
2771
+ event.preventDefault();
2772
+ event.dataTransfer.dropEffect = "move";
2773
+ },
2774
+ onDrop: (event) => {
2775
+ event.preventDefault();
2776
+ if (draggingIndex !== null && draggingIndex !== index) {
2777
+ reorderChapters(draggingIndex, index);
2778
+ }
2779
+ setDraggingIndex(null);
2780
+ },
2781
+ "data-dragging": draggingIndex === index ? "" : void 0,
2782
+ children: [
2783
+ /* @__PURE__ */ jsx14("span", { children: format(messages.chapterN, { n: index + 1 }) }),
2784
+ /* @__PURE__ */ jsx14("strong", { children: chapter.title || messages.untitled })
2785
+ ]
2786
+ },
2787
+ chapter.id
2788
+ )) }),
2789
+ /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-grid", children: [
2790
+ /* @__PURE__ */ jsx14(
2791
+ "button",
2792
+ {
2793
+ type: "button",
2794
+ onClick: () => addChapter({
2795
+ title: format(messages.manuscriptDefaultChapterTitle, {
2796
+ n: chapters.length + 1
2797
+ })
2798
+ }),
2799
+ children: messages.manuscriptAddChapter
2800
+ }
2801
+ ),
2802
+ /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => removeChapter(selected), children: messages.manuscriptRemove })
2803
+ ] })
2804
+ ] }),
2805
+ current && /* @__PURE__ */ jsxs12("div", { className: "mejiro-editor-section", children: [
2806
+ /* @__PURE__ */ jsx14("span", { className: "mejiro-editor-label", children: messages.manuscriptDraft }),
2807
+ /* @__PURE__ */ jsx14(
2808
+ "input",
2809
+ {
2810
+ value: current.title,
2811
+ onChange: (event) => patchChapter(selected, { title: event.target.value })
2812
+ }
2813
+ ),
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 })
2819
+ ] }),
2820
+ /* @__PURE__ */ jsx14(
2821
+ "textarea",
2822
+ {
2823
+ ref: bodyTextareaRef,
2824
+ className: "mejiro-editor-manuscript",
2825
+ value: current.body,
2826
+ onChange: (event) => patchChapter(selected, { body: event.target.value })
2827
+ }
2828
+ )
2829
+ ] }),
2830
+ /* @__PURE__ */ jsx14("button", { type: "button", className: "mejiro-editor-export", onClick: () => void exportEpub(), children: messages.editorExportEpub })
2831
+ ] })
2832
+ ] });
2833
+ }
2834
+
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
+ // src/MejiroShelf.tsx
2933
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2934
+ function MejiroShelf({
2935
+ volumes,
2936
+ currentId,
2937
+ onSelect,
2938
+ title
2939
+ }) {
2940
+ const messages = useI18n();
2941
+ 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(
2945
+ "button",
2946
+ {
2947
+ type: "button",
2948
+ className: `mejiro-shelf-card${v.id === currentId ? " is-active" : ""}`,
2949
+ onClick: () => onSelect?.(v),
2950
+ children: [
2951
+ v.cover ? /* @__PURE__ */ jsx16(
2952
+ "span",
2953
+ {
2954
+ className: "mejiro-shelf-cover",
2955
+ style: { backgroundImage: `url(${JSON.stringify(v.cover)})` },
2956
+ "aria-hidden": "true"
2957
+ }
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 })
2962
+ ] })
2963
+ ]
2964
+ }
2965
+ ) }, v.id)) })
2966
+ ] });
2967
+ }
2968
+
2969
+ // 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";
2972
+ function buildEntries(epub, messages) {
2973
+ return epub.chapters.map((ch, i) => {
2974
+ const title = ch.title ?? format(messages.chapterN, { n: i + 1 });
2975
+ const headings = ch.paragraphs.filter((p) => p.headingLevel && p.text.trim() && p.text.trim() !== title).map((p) => p.text.trim());
2976
+ return { index: i, title, headings };
2977
+ });
2978
+ }
2979
+ function MejiroToc({
2980
+ epub,
2981
+ currentAnchor,
2982
+ searchable = false,
2983
+ title,
2984
+ searchPlaceholder,
2985
+ onSelect
2986
+ }) {
2987
+ const messages = useI18n();
2988
+ const resolvedTitle = title ?? messages.tocTitle;
2989
+ const resolvedSearchPlaceholder = searchPlaceholder ?? messages.tocSearchPlaceholder;
2990
+ const [query, setQuery] = useState11("");
2991
+ const entries = useMemo6(() => buildEntries(epub, messages), [epub, messages]);
2992
+ const filtered = useMemo6(() => {
2993
+ if (!query) return entries;
2994
+ const needle = query.toLowerCase();
2995
+ return entries.filter(
2996
+ (e) => e.title.toLowerCase().includes(needle) || e.headings.some((h) => h.toLowerCase().includes(needle))
2997
+ );
2998
+ }, [entries, query]);
2999
+ 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 })
3004
+ ] }),
3005
+ searchable && /* @__PURE__ */ jsx17("div", { className: "mejiro-toc-search", children: /* @__PURE__ */ jsx17(
3006
+ "input",
3007
+ {
3008
+ type: "search",
3009
+ value: query,
3010
+ placeholder: resolvedSearchPlaceholder,
3011
+ onChange: (e) => setQuery(e.target.value),
3012
+ "aria-label": resolvedSearchPlaceholder
3013
+ }
3014
+ ) }),
3015
+ /* @__PURE__ */ jsx17("ol", { className: "mejiro-toc-list", children: filtered.map((entry) => /* @__PURE__ */ jsxs15("li", { className: "mejiro-toc-item", children: [
3016
+ /* @__PURE__ */ jsxs15(
3017
+ "button",
3018
+ {
3019
+ type: "button",
3020
+ className: `mejiro-toc-link${entry.index === activeIndex ? " is-active" : ""}`,
3021
+ "aria-current": entry.index === activeIndex ? "true" : void 0,
3022
+ onClick: () => onSelect?.(entry.index),
3023
+ 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 })
3026
+ ]
3027
+ }
3028
+ ),
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)) })
3030
+ ] }, entry.index)) }),
3031
+ filtered.length === 0 && /* @__PURE__ */ jsx17("div", { className: "mejiro-toc-empty", role: "status", children: format(messages.tocEmpty, { query }) })
3032
+ ] });
3033
+ }
3034
+
3035
+ // src/useAnnotations.ts
3036
+ import { useCallback as useCallback10, useEffect as useEffect10, useRef as useRef14, useState as useState12 } from "react";
3037
+ var STORAGE_VERSION = 1;
3038
+ function resolveDefaultStorage() {
3039
+ if (typeof globalThis === "undefined") return null;
3040
+ const w = globalThis.localStorage;
3041
+ return w ?? null;
3042
+ }
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
+ function useAnnotations(options) {
3070
+ const { key, throttleMs = 250 } = options;
3071
+ const storage = options.storage ?? resolveDefaultStorage();
3072
+ const [annotations, setAnnotations] = useState12(() => {
3073
+ if (!storage) return [];
3074
+ try {
3075
+ return sort(parse(storage.getItem(key)));
3076
+ } catch {
3077
+ return [];
3078
+ }
3079
+ });
3080
+ useEffect10(() => {
3081
+ if (!storage) {
3082
+ setAnnotations([]);
3083
+ return;
3084
+ }
3085
+ try {
3086
+ setAnnotations(sort(parse(storage.getItem(key))));
3087
+ } catch {
3088
+ setAnnotations([]);
3089
+ }
3090
+ }, [storage, key]);
3091
+ const timerRef = useRef14(null);
3092
+ useEffect10(
3093
+ () => () => {
3094
+ if (timerRef.current) clearTimeout(timerRef.current);
3095
+ },
3096
+ []
3097
+ );
3098
+ const onChangeRef = useRef14(options.onChange);
3099
+ onChangeRef.current = options.onChange;
3100
+ const commit = useCallback10(
3101
+ (next) => {
3102
+ if (storage) {
3103
+ if (timerRef.current) clearTimeout(timerRef.current);
3104
+ timerRef.current = setTimeout(() => {
3105
+ try {
3106
+ storage.setItem(key, serialize(next));
3107
+ } catch {
3108
+ }
3109
+ }, throttleMs);
3110
+ }
3111
+ onChangeRef.current?.(next);
3112
+ },
3113
+ [storage, key, throttleMs]
3114
+ );
3115
+ const add = useCallback10(
3116
+ (input) => {
3117
+ const annotation = {
3118
+ ...input,
3119
+ id: input.id ?? makeId(),
3120
+ createdAt: input.createdAt ?? Date.now()
3121
+ };
3122
+ setAnnotations((current) => {
3123
+ const next = sort([...current, annotation]);
3124
+ commit(next);
3125
+ return next;
3126
+ });
3127
+ return annotation;
3128
+ },
3129
+ [commit]
3130
+ );
3131
+ const remove = useCallback10(
3132
+ (id) => {
3133
+ setAnnotations((current) => {
3134
+ const next = current.filter((annotation) => annotation.id !== id);
3135
+ if (next.length !== current.length) commit(next);
3136
+ return next;
3137
+ });
3138
+ },
3139
+ [commit]
3140
+ );
3141
+ const update = useCallback10(
3142
+ (id, patch) => {
3143
+ setAnnotations((current) => {
3144
+ let changed = false;
3145
+ const next = current.map((annotation) => {
3146
+ if (annotation.id !== id) return annotation;
3147
+ changed = true;
3148
+ return { ...annotation, ...patch, id };
3149
+ });
3150
+ if (!changed) return current;
3151
+ const sorted = sort(next);
3152
+ commit(sorted);
3153
+ return sorted;
3154
+ });
3155
+ },
3156
+ [commit]
3157
+ );
3158
+ const clear = useCallback10(() => {
3159
+ setAnnotations([]);
3160
+ if (storage) {
3161
+ if (timerRef.current) clearTimeout(timerRef.current);
3162
+ try {
3163
+ storage.removeItem(key);
3164
+ } catch {
3165
+ }
3166
+ }
3167
+ onChangeRef.current?.([]);
3168
+ }, [storage, key]);
3169
+ return { annotations, add, remove, update, clear };
3170
+ }
3171
+
3172
+ // src/useEditableEpub.ts
3173
+ import {
3174
+ EditableEpub as EditableEpub2
3175
+ } from "@libraz/mejiro/epub";
3176
+ import { useCallback as useCallback11, useEffect as useEffect11, useRef as useRef15, useState as useState13 } from "react";
3177
+ 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({
3184
+ chapter: 0,
3185
+ paragraph: 0
3186
+ });
3187
+ const requestIdRef = useRef15(0);
3188
+ const onLoadRef = useRef15(options.onLoad);
3189
+ const onErrorRef = useRef15(options.onError);
3190
+ const onExportRef = useRef15(options.onExport);
3191
+ onLoadRef.current = options.onLoad;
3192
+ onErrorRef.current = options.onError;
3193
+ onExportRef.current = options.onExport;
3194
+ const loadBufferWithRequest = useCallback11(
3195
+ async (buffer, requestId) => {
3196
+ setLoading(true);
3197
+ setError(null);
3198
+ try {
3199
+ const next = await EditableEpub2.load(buffer);
3200
+ if (requestId !== requestIdRef.current) return null;
3201
+ setEditor(next);
3202
+ setSelectionState({ chapter: 0, paragraph: 0 });
3203
+ setRevision((value) => value + 1);
3204
+ onLoadRef.current?.(next);
3205
+ return next;
3206
+ } catch (err) {
3207
+ if (requestId === requestIdRef.current) {
3208
+ const nextError = err instanceof Error ? err : new Error(String(err));
3209
+ setError(nextError);
3210
+ onErrorRef.current?.(nextError);
3211
+ }
3212
+ return null;
3213
+ } finally {
3214
+ if (requestId === requestIdRef.current) setLoading(false);
3215
+ }
3216
+ },
3217
+ []
3218
+ );
3219
+ const loadBuffer = useCallback11(
3220
+ async (buffer) => {
3221
+ const requestId = ++requestIdRef.current;
3222
+ return loadBufferWithRequest(buffer, requestId);
3223
+ },
3224
+ [loadBufferWithRequest]
3225
+ );
3226
+ const loadFile = useCallback11(
3227
+ async (file) => {
3228
+ const requestId = ++requestIdRef.current;
3229
+ setLoading(true);
3230
+ setError(null);
3231
+ try {
3232
+ return await loadBufferWithRequest(await file.arrayBuffer(), requestId);
3233
+ } catch (err) {
3234
+ if (requestId === requestIdRef.current) {
3235
+ const nextError = err instanceof Error ? err : new Error(String(err));
3236
+ setError(nextError);
3237
+ onErrorRef.current?.(nextError);
3238
+ setLoading(false);
3239
+ }
3240
+ return null;
3241
+ }
3242
+ },
3243
+ [loadBufferWithRequest]
3244
+ );
3245
+ const loadUrl = useCallback11(
3246
+ async (url) => {
3247
+ const requestId = ++requestIdRef.current;
3248
+ setLoading(true);
3249
+ setError(null);
3250
+ try {
3251
+ const res = await fetch(url);
3252
+ if (!res.ok) throw new Error(`Failed to load EPUB: ${res.status}`);
3253
+ return await loadBufferWithRequest(await res.arrayBuffer(), requestId);
3254
+ } catch (err) {
3255
+ if (requestId === requestIdRef.current) {
3256
+ const nextError = err instanceof Error ? err : new Error(String(err));
3257
+ setError(nextError);
3258
+ onErrorRef.current?.(nextError);
3259
+ setLoading(false);
3260
+ }
3261
+ return null;
3262
+ }
3263
+ },
3264
+ [loadBufferWithRequest]
3265
+ );
3266
+ useEffect11(() => {
3267
+ if (options.defaultUrl) void loadUrl(options.defaultUrl);
3268
+ }, [options.defaultUrl, loadUrl]);
3269
+ const book = editor?.book ?? null;
3270
+ const selectedParagraph = book?.chapters[selection.chapter]?.paragraphs[selection.paragraph] ?? null;
3271
+ const previewBook = book ? cloneBook2(book) : null;
3272
+ const history = editor?.history ?? null;
3273
+ const setSelection = useCallback11(
3274
+ (nextSelection) => {
3275
+ setSelectionState(clampSelection(book, nextSelection));
3276
+ },
3277
+ [book]
3278
+ );
3279
+ const updateParagraph = useCallback11(
3280
+ (text, inlineAnnotations) => {
3281
+ if (!editor) return;
3282
+ editor.updateParagraph(selection.chapter, selection.paragraph, {
3283
+ text,
3284
+ inlineAnnotations
3285
+ });
3286
+ setRevision((value) => value + 1);
3287
+ },
3288
+ [editor, selection.chapter, selection.paragraph]
3289
+ );
3290
+ const setInlineAnnotations = useCallback11(
3291
+ (inlineAnnotations) => {
3292
+ if (!editor) return;
3293
+ editor.setInlineAnnotations(selection.chapter, selection.paragraph, inlineAnnotations);
3294
+ setRevision((value) => value + 1);
3295
+ },
3296
+ [editor, selection.chapter, selection.paragraph]
3297
+ );
3298
+ const addImage = useCallback11(
3299
+ (image) => {
3300
+ if (!editor) return;
3301
+ editor.addImage(selection.chapter, image);
3302
+ setRevision((value) => value + 1);
3303
+ },
3304
+ [editor, selection.chapter]
3305
+ );
3306
+ const undo = useCallback11(() => {
3307
+ if (!editor) return false;
3308
+ const changed = editor.undo();
3309
+ if (changed) setRevision((value) => value + 1);
3310
+ return changed;
3311
+ }, [editor]);
3312
+ const redo = useCallback11(() => {
3313
+ if (!editor) return false;
3314
+ const changed = editor.redo();
3315
+ if (changed) setRevision((value) => value + 1);
3316
+ return changed;
3317
+ }, [editor]);
3318
+ const exportEpub = useCallback11(
3319
+ async (options2) => {
3320
+ if (!editor) return null;
3321
+ setExporting(true);
3322
+ try {
3323
+ const buffer = await editor.export(options2);
3324
+ onExportRef.current?.(buffer);
3325
+ return buffer;
3326
+ } finally {
3327
+ setExporting(false);
3328
+ }
3329
+ },
3330
+ [editor]
3331
+ );
3332
+ return {
3333
+ editor,
3334
+ book,
3335
+ previewBook,
3336
+ loading,
3337
+ exporting,
3338
+ error,
3339
+ revision,
3340
+ history,
3341
+ selection,
3342
+ selectedParagraph,
3343
+ setSelection,
3344
+ loadBuffer,
3345
+ loadFile,
3346
+ loadUrl,
3347
+ updateParagraph,
3348
+ setInlineAnnotations,
3349
+ addImage,
3350
+ undo,
3351
+ redo,
3352
+ exportEpub
3353
+ };
3354
+ }
3355
+ function cloneBook2(book) {
3356
+ return {
3357
+ ...book,
3358
+ chapters: book.chapters.map((chapter) => ({
3359
+ ...chapter,
3360
+ blocks: chapter.blocks.map(
3361
+ (block) => block.kind === "paragraph" ? { ...block, inlineAnnotations: [...block.inlineAnnotations] } : { ...block }
3362
+ ),
3363
+ imageAssets: new Map(chapter.imageAssets),
3364
+ originalImageHrefs: chapter.originalImageHrefs ? [...chapter.originalImageHrefs] : void 0,
3365
+ paragraphs: chapter.paragraphs.map((paragraph) => ({
3366
+ ...paragraph,
3367
+ inlineAnnotations: [...paragraph.inlineAnnotations]
3368
+ })),
3369
+ paragraphRefs: chapter.paragraphRefs ? [...chapter.paragraphRefs] : void 0
3370
+ })),
3371
+ packageData: {
3372
+ ...book.packageData,
3373
+ files: new Map(book.packageData.files)
3374
+ }
3375
+ };
3376
+ }
3377
+ function clampSelection(book, selection) {
3378
+ if (!book?.chapters.length) return { chapter: 0, paragraph: 0 };
3379
+ const chapter = clampInteger(selection.chapter, 0, book.chapters.length - 1);
3380
+ const paragraphCount = book.chapters[chapter]?.paragraphs.length ?? 0;
3381
+ const paragraph = paragraphCount ? clampInteger(selection.paragraph, 0, paragraphCount - 1) : 0;
3382
+ return { chapter, paragraph };
3383
+ }
3384
+ function clampInteger(value, min, max) {
3385
+ if (!Number.isFinite(value)) return min;
3386
+ return Math.min(max, Math.max(min, Math.trunc(value)));
3387
+ }
3388
+
3389
+ // src/useEpubProject.ts
3390
+ import {
3391
+ EpubProject as EpubProject2,
3392
+ parseEpub as parseEpub2
3393
+ } from "@libraz/mejiro/epub";
3394
+ import { useCallback as useCallback12, useEffect as useEffect12, useMemo as useMemo7, useRef as useRef16, useState as useState14 } from "react";
3395
+ function useEpubProject(options = {}) {
3396
+ const [metadata, setMetadataState] = useState14({
3397
+ title: "\u65B0\u3057\u3044\u4F5C\u54C1",
3398
+ language: "ja",
3399
+ ...options.metadata
3400
+ });
3401
+ const [chapters, setChaptersState] = useState14(
3402
+ options.chapters?.length ? options.chapters : [defaultChapter3(0)]
3403
+ );
3404
+ const [selectedChapter, setSelectedChapterState] = useState14(0);
3405
+ const [previewBook, setPreviewBook] = useState14(null);
3406
+ const [previewError, setPreviewError] = useState14(null);
3407
+ const [previewing, setPreviewing] = useState14(false);
3408
+ const previewRequestIdRef = useRef16(0);
3409
+ const onPreviewRef = useRef16(options.onPreview);
3410
+ const onExportRef = useRef16(options.onExport);
3411
+ onPreviewRef.current = options.onPreview;
3412
+ onExportRef.current = options.onExport;
3413
+ const buildProject = useCallback12(
3414
+ () => EpubProject2.fromManuscript({
3415
+ metadata,
3416
+ includeTitlePage: false,
3417
+ includeTitleInFirstChapter: true,
3418
+ chapters: chapters.map((chapter) => ({
3419
+ id: chapter.id,
3420
+ title: chapter.title || "Untitled",
3421
+ body: chapter.body
3422
+ }))
3423
+ }),
3424
+ [chapters, metadata]
3425
+ );
3426
+ const assetResolverRef = useRef16(options.assetResolver);
3427
+ assetResolverRef.current = options.assetResolver;
3428
+ useEffect12(() => {
3429
+ const requestId = ++previewRequestIdRef.current;
3430
+ let cancelled = false;
3431
+ setPreviewing(true);
3432
+ const timer = setTimeout(() => {
3433
+ void (async () => {
3434
+ try {
3435
+ const resolver = assetResolverRef.current;
3436
+ const book = await parseEpub2(
3437
+ await buildProject().export(resolver ? { assetResolver: resolver } : void 0)
3438
+ );
3439
+ if (cancelled || requestId !== previewRequestIdRef.current) return;
3440
+ setPreviewBook(book);
3441
+ setPreviewError(null);
3442
+ onPreviewRef.current?.(book);
3443
+ } catch (err) {
3444
+ if (!cancelled && requestId === previewRequestIdRef.current) {
3445
+ setPreviewError(err instanceof Error ? err : new Error(String(err)));
3446
+ }
3447
+ } finally {
3448
+ if (!cancelled && requestId === previewRequestIdRef.current) setPreviewing(false);
3449
+ }
3450
+ })();
3451
+ }, options.debounceMs ?? 250);
3452
+ return () => {
3453
+ cancelled = true;
3454
+ previewRequestIdRef.current++;
3455
+ clearTimeout(timer);
3456
+ };
3457
+ }, [buildProject, options.debounceMs]);
3458
+ const currentChapter = chapters[selectedChapter] ?? chapters[0] ?? null;
3459
+ const setMetadata = useCallback12((patch) => {
3460
+ setMetadataState((current) => ({ ...current, ...patch }));
3461
+ }, []);
3462
+ const setChapters = useCallback12(
3463
+ (next) => {
3464
+ const normalized = next.length ? next : [defaultChapter3(0)];
3465
+ setChaptersState(normalized);
3466
+ setSelectedChapterState((current) => {
3467
+ const selectedId = chapters[current]?.id;
3468
+ const nextIndex = selectedId ? normalized.findIndex((chapter) => chapter.id === selectedId) : -1;
3469
+ return nextIndex >= 0 ? nextIndex : Math.max(0, Math.min(current, normalized.length - 1));
3470
+ });
3471
+ },
3472
+ [chapters]
3473
+ );
3474
+ const setSelectedChapter = useCallback12(
3475
+ (index) => {
3476
+ setSelectedChapterState(Math.max(0, Math.min(index, chapters.length - 1)));
3477
+ },
3478
+ [chapters.length]
3479
+ );
3480
+ const patchChapter = useCallback12((index, patch) => {
3481
+ setChaptersState(
3482
+ (current) => current.map(
3483
+ (chapter, chapterIndex) => chapterIndex === index ? { ...chapter, ...patch } : chapter
3484
+ )
3485
+ );
3486
+ }, []);
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
+ }, []);
3502
+ const removeChapter = useCallback12(
3503
+ (index = selectedChapter) => {
3504
+ setChaptersState((current) => {
3505
+ if (current.length <= 1) return current;
3506
+ const next = current.filter((_, chapterIndex) => chapterIndex !== index);
3507
+ setSelectedChapterState((currentSelected) => {
3508
+ if (currentSelected === index) return Math.max(0, Math.min(index, next.length - 1));
3509
+ if (index < currentSelected) return currentSelected - 1;
3510
+ return Math.max(0, Math.min(currentSelected, next.length - 1));
3511
+ });
3512
+ return next;
3513
+ });
3514
+ },
3515
+ [selectedChapter]
3516
+ );
3517
+ const reorderChapters = useCallback12((from, to) => {
3518
+ setChaptersState((current) => {
3519
+ if (from < 0 || from >= current.length) return current;
3520
+ const next = [...current];
3521
+ const [moved] = next.splice(from, 1);
3522
+ const target = Math.max(0, Math.min(next.length, to));
3523
+ next.splice(target, 0, moved);
3524
+ setSelectedChapterState((currentSelected) => {
3525
+ if (currentSelected === from) return target;
3526
+ if (from < currentSelected && target >= currentSelected) return currentSelected - 1;
3527
+ if (from > currentSelected && target <= currentSelected) return currentSelected + 1;
3528
+ return currentSelected;
3529
+ });
3530
+ return next;
3531
+ });
3532
+ }, []);
3533
+ const exportEpub = useCallback12(async () => {
3534
+ const resolver = assetResolverRef.current;
3535
+ const buffer = await buildProject().export(resolver ? { assetResolver: resolver } : void 0);
3536
+ onExportRef.current?.(buffer);
3537
+ return buffer;
3538
+ }, [buildProject]);
3539
+ return useMemo7(
3540
+ () => ({
3541
+ metadata,
3542
+ chapters,
3543
+ selectedChapter,
3544
+ currentChapter,
3545
+ previewBook,
3546
+ previewError,
3547
+ previewing,
3548
+ setMetadata,
3549
+ setChapters,
3550
+ setSelectedChapter,
3551
+ patchChapter,
3552
+ addChapter,
3553
+ removeChapter,
3554
+ reorderChapters,
3555
+ buildProject,
3556
+ exportEpub
3557
+ }),
3558
+ [
3559
+ addChapter,
3560
+ buildProject,
3561
+ chapters,
3562
+ currentChapter,
3563
+ exportEpub,
3564
+ metadata,
3565
+ patchChapter,
3566
+ previewBook,
3567
+ previewError,
3568
+ previewing,
3569
+ reorderChapters,
3570
+ removeChapter,
3571
+ selectedChapter,
3572
+ setChapters,
3573
+ setMetadata,
3574
+ setSelectedChapter
3575
+ ]
3576
+ );
3577
+ }
3578
+ function defaultChapter3(index) {
3579
+ return {
3580
+ 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" : ""
3583
+ };
3584
+ }
3585
+
3586
+ // src/useImageOverlay.ts
3587
+ import { moveImageOverlayRect as moveImageOverlayRect2, resizeImageOverlayRect as resizeImageOverlayRect2 } from "@libraz/mejiro";
3588
+ import {
3589
+ useCallback as useCallback13,
3590
+ useEffect as useEffect13,
3591
+ useRef as useRef17,
3592
+ useState as useState15
3593
+ } from "react";
85
3594
  function useImageOverlay(layout, spreadIdx, onUpdate, options) {
86
3595
  const defW = options?.defaultWidth ?? 120;
87
3596
  const defH = options?.defaultHeight ?? 160;
88
3597
  const defX = options?.defaultX ?? 80;
89
3598
  const defY = options?.defaultY ?? 100;
90
3599
  const margin = options?.margin;
91
- const [imageRect, setImageRect] = useState(null);
92
- const rectRef = useRef(imageRect);
3600
+ const [imageRect, setImageRect] = useState15(null);
3601
+ const rectRef = useRef17(imageRect);
93
3602
  rectRef.current = imageRect;
94
- const layoutRef = useRef(layout);
3603
+ const layoutRef = useRef17(layout);
95
3604
  layoutRef.current = layout;
96
- const spreadRef = useRef(spreadIdx);
3605
+ const spreadRef = useRef17(spreadIdx);
97
3606
  spreadRef.current = spreadIdx;
98
- const onUpdateRef = useRef(onUpdate);
3607
+ const onUpdateRef = useRef17(onUpdate);
99
3608
  onUpdateRef.current = onUpdate;
100
- const syncToLayout = useCallback(
3609
+ const activeDragCleanupsRef = useRef17(/* @__PURE__ */ new Set());
3610
+ useEffect13(
3611
+ () => () => {
3612
+ for (const cleanup of activeDragCleanupsRef.current) cleanup();
3613
+ activeDragCleanupsRef.current.clear();
3614
+ },
3615
+ []
3616
+ );
3617
+ const syncToLayout = useCallback13(
101
3618
  (rect) => {
102
3619
  const lo = layoutRef.current;
103
3620
  if (!lo) return;
@@ -107,7 +3624,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
107
3624
  },
108
3625
  [margin]
109
3626
  );
110
- const toggleImage = useCallback(() => {
3627
+ const toggleImage = useCallback13(() => {
111
3628
  if (rectRef.current) {
112
3629
  setImageRect(null);
113
3630
  syncToLayout(null);
@@ -117,7 +3634,7 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
117
3634
  syncToLayout(r);
118
3635
  }
119
3636
  }, [defX, defY, defW, defH, syncToLayout]);
120
- const onOverlayPointerDown = useCallback(
3637
+ const onOverlayPointerDown = useCallback13(
121
3638
  (e) => {
122
3639
  e.preventDefault();
123
3640
  const current = rectRef.current;
@@ -132,24 +3649,29 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
132
3649
  const onMove = (me) => {
133
3650
  const dx = me.clientX - startX;
134
3651
  const dy = me.clientY - startY;
135
- const r = moveImageOverlayRect(start, dx, dy);
3652
+ const r = moveImageOverlayRect2(start, dx, dy);
136
3653
  cancelAnimationFrame(rafId);
137
3654
  rafId = requestAnimationFrame(() => {
138
3655
  setImageRect(r);
139
3656
  syncToLayout(r);
140
3657
  });
141
3658
  };
142
- const onUp = () => {
3659
+ let cleanup;
3660
+ const onUp = () => cleanup();
3661
+ cleanup = () => {
3662
+ cancelAnimationFrame(rafId);
143
3663
  target.classList.remove("dragging");
144
3664
  document.removeEventListener("pointermove", onMove);
145
3665
  document.removeEventListener("pointerup", onUp);
3666
+ activeDragCleanupsRef.current.delete(cleanup);
146
3667
  };
147
3668
  document.addEventListener("pointermove", onMove);
148
3669
  document.addEventListener("pointerup", onUp);
3670
+ activeDragCleanupsRef.current.add(cleanup);
149
3671
  },
150
3672
  [syncToLayout]
151
3673
  );
152
- const onResizePointerDown = useCallback(
3674
+ const onResizePointerDown = useCallback13(
153
3675
  (e) => {
154
3676
  e.preventDefault();
155
3677
  e.stopPropagation();
@@ -165,20 +3687,25 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
165
3687
  const onMove = (me) => {
166
3688
  const dx = me.clientX - startX;
167
3689
  const dy = me.clientY - startY;
168
- const r = resizeImageOverlayRect(start, dx, dy);
3690
+ const r = resizeImageOverlayRect2(start, dx, dy);
169
3691
  cancelAnimationFrame(rafId);
170
3692
  rafId = requestAnimationFrame(() => {
171
3693
  setImageRect(r);
172
3694
  syncToLayout(r);
173
3695
  });
174
3696
  };
175
- const onUp = () => {
3697
+ let cleanup;
3698
+ const onUp = () => cleanup();
3699
+ cleanup = () => {
3700
+ cancelAnimationFrame(rafId);
176
3701
  target.parentElement?.classList.remove("dragging");
177
3702
  document.removeEventListener("pointermove", onMove);
178
3703
  document.removeEventListener("pointerup", onUp);
3704
+ activeDragCleanupsRef.current.delete(cleanup);
179
3705
  };
180
3706
  document.addEventListener("pointermove", onMove);
181
3707
  document.addEventListener("pointerup", onUp);
3708
+ activeDragCleanupsRef.current.add(cleanup);
182
3709
  },
183
3710
  [syncToLayout]
184
3711
  );
@@ -190,8 +3717,298 @@ function useImageOverlay(layout, spreadIdx, onUpdate, options) {
190
3717
  onResizePointerDown
191
3718
  };
192
3719
  }
3720
+
3721
+ // src/useLibrary.ts
3722
+ import { useCallback as useCallback14, useEffect as useEffect14, useMemo as useMemo8, useState as useState16 } from "react";
3723
+ function useLibrary(options) {
3724
+ const { volumes, initialVolumeId, onChange } = options;
3725
+ const initialIndex = initialVolumeId != null ? Math.max(
3726
+ 0,
3727
+ volumes.findIndex((v) => v.id === initialVolumeId)
3728
+ ) : 0;
3729
+ const [currentId, setCurrentId] = useState16(
3730
+ volumes.length > 0 ? volumes[initialIndex]?.id ?? volumes[0]?.id ?? null : null
3731
+ );
3732
+ useEffect14(() => {
3733
+ if (volumes.length === 0) {
3734
+ if (currentId !== null) setCurrentId(null);
3735
+ return;
3736
+ }
3737
+ if (currentId != null && volumes.some((v) => v.id === currentId)) return;
3738
+ const fallback = volumes[0] ?? null;
3739
+ setCurrentId(fallback?.id ?? null);
3740
+ if (fallback) onChange?.(fallback);
3741
+ }, [currentId, volumes, onChange]);
3742
+ const index = useMemo8(() => {
3743
+ if (volumes.length === 0) return -1;
3744
+ if (currentId == null) return 0;
3745
+ const found = volumes.findIndex((v) => v.id === currentId);
3746
+ return found >= 0 ? found : 0;
3747
+ }, [currentId, volumes]);
3748
+ const current = useMemo8(
3749
+ () => index >= 0 && index < volumes.length ? volumes[index] : null,
3750
+ [index, volumes]
3751
+ );
3752
+ const fire = useCallback14(
3753
+ (i) => {
3754
+ const v = volumes[i];
3755
+ if (v) onChange?.(v);
3756
+ },
3757
+ [volumes, onChange]
3758
+ );
3759
+ const next = useCallback14(() => {
3760
+ if (index < 0) return;
3761
+ const ni = Math.min(volumes.length - 1, index + 1);
3762
+ if (ni !== index) {
3763
+ setCurrentId(volumes[ni]?.id ?? null);
3764
+ fire(ni);
3765
+ }
3766
+ }, [index, volumes, fire]);
3767
+ const prev = useCallback14(() => {
3768
+ if (index < 0) return;
3769
+ const ni = Math.max(0, index - 1);
3770
+ if (ni !== index) {
3771
+ setCurrentId(volumes[ni]?.id ?? null);
3772
+ fire(ni);
3773
+ }
3774
+ }, [index, volumes, fire]);
3775
+ const goTo = useCallback14(
3776
+ (id) => {
3777
+ const ni = volumes.findIndex((v) => v.id === id);
3778
+ if (ni < 0) return;
3779
+ if (ni !== index) {
3780
+ setCurrentId(id);
3781
+ fire(ni);
3782
+ }
3783
+ },
3784
+ [index, volumes, fire]
3785
+ );
3786
+ return { list: volumes, current, currentIndex: index, next, prev, goTo };
3787
+ }
3788
+
3789
+ // src/useManuscriptLayout.ts
3790
+ import { useCallback as useCallback15, useEffect as useEffect15, useLayoutEffect as useLayoutEffect5, useRef as useRef18, useState as useState17 } from "react";
3791
+ function useManuscriptLayout(book, chapter, surface, options = {}) {
3792
+ const dialect = options.dialect ?? "mejiro";
3793
+ const enableResize = options.enableResize ?? true;
3794
+ 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);
3801
+ const requestIdRef = useRef18(0);
3802
+ layoutRef.current = layout;
3803
+ const recompute = useCallback15(async () => {
3804
+ const requestId = ++requestIdRef.current;
3805
+ if (!(chapter && surface.current)) {
3806
+ setLayout(null);
3807
+ setPageWidth(0);
3808
+ setPageHeight(0);
3809
+ setContentHeight(0);
3810
+ setElapsedMs(0);
3811
+ return;
3812
+ }
3813
+ setLayout(null);
3814
+ const dims = book.computePageSize(surface.current);
3815
+ setPageWidth(dims.pageWidth);
3816
+ setPageHeight(dims.pageHeight);
3817
+ setContentHeight(dims.contentHeight);
3818
+ const t0 = performance.now();
3819
+ const layouts = await book.layoutManuscript({ chapters: [chapter], dialect });
3820
+ if (requestId !== requestIdRef.current) return;
3821
+ const next = layouts.values().next().value ?? null;
3822
+ setLayout(next);
3823
+ setElapsedMs(performance.now() - t0);
3824
+ }, [book, chapter, dialect, surface]);
3825
+ useLayoutEffect5(() => {
3826
+ requestIdRef.current++;
3827
+ setLayout(null);
3828
+ setPageWidth(0);
3829
+ setPageHeight(0);
3830
+ setContentHeight(0);
3831
+ setElapsedMs(0);
3832
+ }, [recompute]);
3833
+ useEffect15(() => {
3834
+ void recompute();
3835
+ return () => {
3836
+ requestIdRef.current++;
3837
+ };
3838
+ }, [recompute]);
3839
+ useEffect15(() => {
3840
+ if (!enableResize) return;
3841
+ let timer = null;
3842
+ const onResize = () => {
3843
+ if (!(surface.current && layoutRef.current)) return;
3844
+ if (timer) clearTimeout(timer);
3845
+ 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
+ });
3855
+ }, resizeDebounce);
3856
+ };
3857
+ window.addEventListener("resize", onResize);
3858
+ return () => {
3859
+ window.removeEventListener("resize", onResize);
3860
+ if (timer) clearTimeout(timer);
3861
+ };
3862
+ }, [book, surface, enableResize, resizeDebounce]);
3863
+ return { layout, pageWidth, pageHeight, contentHeight, elapsedMs, recompute };
3864
+ }
3865
+
3866
+ // src/useReadingPosition.ts
3867
+ import { useCallback as useCallback16, useEffect as useEffect16, useRef as useRef19, useState as useState18 } from "react";
3868
+ var STORAGE_VERSION2 = 2;
3869
+ function resolveDefaultStorage2() {
3870
+ if (typeof globalThis === "undefined") return null;
3871
+ const w = globalThis.localStorage;
3872
+ return w ?? null;
3873
+ }
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
+ function useReadingPosition(options) {
3909
+ const { key, throttleMs = 250 } = options;
3910
+ const storage = options.storage ?? resolveDefaultStorage2();
3911
+ const [position, setPosition] = useState18(() => {
3912
+ if (!storage) return null;
3913
+ try {
3914
+ return parsePosition(storage.getItem(key));
3915
+ } catch {
3916
+ return null;
3917
+ }
3918
+ });
3919
+ useEffect16(() => {
3920
+ if (!storage) {
3921
+ setPosition(null);
3922
+ return;
3923
+ }
3924
+ try {
3925
+ setPosition(parsePosition(storage.getItem(key)));
3926
+ } catch {
3927
+ setPosition(null);
3928
+ }
3929
+ }, [storage, key]);
3930
+ const timerRef = useRef19(null);
3931
+ useEffect16(
3932
+ () => () => {
3933
+ if (timerRef.current) clearTimeout(timerRef.current);
3934
+ },
3935
+ []
3936
+ );
3937
+ const onChangeRef = useRef19(options.onChange);
3938
+ onChangeRef.current = options.onChange;
3939
+ const save = useCallback16(
3940
+ (next) => {
3941
+ setPosition(next);
3942
+ if (storage) {
3943
+ if (timerRef.current) clearTimeout(timerRef.current);
3944
+ timerRef.current = setTimeout(() => {
3945
+ try {
3946
+ storage.setItem(key, serializePosition(next));
3947
+ } catch {
3948
+ }
3949
+ }, throttleMs);
3950
+ }
3951
+ onChangeRef.current?.(next);
3952
+ },
3953
+ [storage, key, throttleMs]
3954
+ );
3955
+ const clear = useCallback16(() => {
3956
+ setPosition(null);
3957
+ if (timerRef.current) clearTimeout(timerRef.current);
3958
+ if (storage) {
3959
+ try {
3960
+ storage.removeItem(key);
3961
+ } catch {
3962
+ }
3963
+ }
3964
+ onChangeRef.current?.(null);
3965
+ }, [storage, key]);
3966
+ return { position, save, clear };
3967
+ }
3968
+
3969
+ // src/index.ts
3970
+ function loadMejiroStyles() {
3971
+ if (typeof document === "undefined") return;
3972
+ void import("@libraz/mejiro/render/mejiro.css");
3973
+ void import("@libraz/mejiro/render/mejiro-reader.css");
3974
+ void import("@libraz/mejiro/render/mejiro-editor.css");
3975
+ }
3976
+ loadMejiroStyles();
193
3977
  export {
3978
+ MejiroChapterNav,
3979
+ MejiroDropZone,
3980
+ MejiroEditor,
3981
+ MejiroI18nProvider,
3982
+ MejiroImageOverlay,
3983
+ MejiroManuscriptEditor,
3984
+ MejiroNotationHighlighter,
194
3985
  MejiroPage,
3986
+ MejiroPageIndicator,
195
3987
  MejiroPageView,
196
- useImageOverlay
3988
+ MejiroReader,
3989
+ MejiroScrollView,
3990
+ MejiroSelectionLayer,
3991
+ MejiroSettingsPanel,
3992
+ MejiroShelf,
3993
+ MejiroSpread,
3994
+ MejiroStats,
3995
+ MejiroToc,
3996
+ enMessages,
3997
+ format,
3998
+ jaMessages,
3999
+ resolveMessages,
4000
+ useAnnotations,
4001
+ useChapterLayout,
4002
+ useEditableEpub,
4003
+ useEpub,
4004
+ useEpubProject,
4005
+ useI18n,
4006
+ useImageOverlay,
4007
+ useLibrary,
4008
+ useManuscriptDraft,
4009
+ useManuscriptLayout,
4010
+ useMejiroBook,
4011
+ useMultiImageOverlay,
4012
+ useReadingPosition,
4013
+ useSpread
197
4014
  };