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