@somecat/epub-reader 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -5,7 +5,7 @@ require('foliate-js/view.js');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
7
7
  // src/react/EBookReader.tsx
8
- var getContentCSS = (fontSize, isDark, extraCSS) => `
8
+ var getContentCSS = (fontSize, isDark, lineHeight, letterSpacing, extraCSS) => `
9
9
  @namespace epub "http://www.idpf.org/2007/ops";
10
10
  html {
11
11
  color-scheme: ${isDark ? "dark" : "light"} !important;
@@ -14,9 +14,11 @@ body {
14
14
  background-color: transparent !important;
15
15
  color: ${isDark ? "#e0e0e0" : "black"} !important;
16
16
  font-size: ${fontSize}% !important;
17
+ line-height: ${lineHeight} !important;
18
+ letter-spacing: ${letterSpacing}em !important;
17
19
  }
18
20
  p {
19
- line-height: 1.6;
21
+ line-height: inherit !important;
20
22
  margin-bottom: 1em;
21
23
  }
22
24
  a {
@@ -36,6 +38,8 @@ function createEBookReader(container, options = {}) {
36
38
  const {
37
39
  darkMode: initialDarkMode = false,
38
40
  fontSize: initialFontSize = 100,
41
+ lineHeight: initialLineHeight = 1.6,
42
+ letterSpacing: initialLetterSpacing = 0,
39
43
  extraContentCSS,
40
44
  onReady,
41
45
  onError,
@@ -48,6 +52,8 @@ function createEBookReader(container, options = {}) {
48
52
  let toc = [];
49
53
  let fontSize = initialFontSize;
50
54
  let darkMode = initialDarkMode;
55
+ let lineHeight = initialLineHeight;
56
+ let letterSpacing = initialLetterSpacing;
51
57
  let searchToken = 0;
52
58
  container.innerHTML = "";
53
59
  const viewer = document.createElement("foliate-view");
@@ -59,7 +65,7 @@ function createEBookReader(container, options = {}) {
59
65
  const applyStyles = () => {
60
66
  if (destroyed) return;
61
67
  if (!viewer.renderer?.setStyles) return;
62
- viewer.renderer.setStyles(getContentCSS(fontSize, darkMode, extraContentCSS));
68
+ viewer.renderer.setStyles(getContentCSS(fontSize, darkMode, lineHeight, letterSpacing, extraContentCSS));
63
69
  requestAnimationFrame(() => {
64
70
  setTimeout(() => {
65
71
  if (destroyed) return;
@@ -135,6 +141,16 @@ function createEBookReader(container, options = {}) {
135
141
  fontSize = safe;
136
142
  applyStyles();
137
143
  },
144
+ setLineHeight(nextLineHeight) {
145
+ const safe = Math.min(3, Math.max(1, nextLineHeight));
146
+ lineHeight = safe;
147
+ applyStyles();
148
+ },
149
+ setLetterSpacing(nextLetterSpacing) {
150
+ const safe = Math.min(0.3, Math.max(0, nextLetterSpacing));
151
+ letterSpacing = safe;
152
+ applyStyles();
153
+ },
138
154
  async search(query, opts = {}) {
139
155
  const normalized = query.trim();
140
156
  if (!normalized) {
@@ -197,6 +213,36 @@ function createEBookReader(container, options = {}) {
197
213
  onReady?.(handle);
198
214
  return handle;
199
215
  }
216
+ var TocTree = ({ items, onSelect }) => {
217
+ return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "epub-reader__toc-list", children: items.map((item, idx) => {
218
+ const key = item.href || `${item.label ?? "item"}-${idx}`;
219
+ const hasChildren = Boolean(item.subitems?.length);
220
+ const label = item.label || item.href || "\u672A\u547D\u540D";
221
+ if (!hasChildren) {
222
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "epub-reader__toc-item", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__toc-btn", onClick: () => onSelect(item.href), children: label }) }, key);
223
+ }
224
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "epub-reader__toc-item", children: /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "epub-reader__toc-details", children: [
225
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { className: "epub-reader__toc-summary", children: label }),
226
+ /* @__PURE__ */ jsxRuntime.jsx(TocTree, { items: item.subitems ?? [], onSelect })
227
+ ] }) }, key);
228
+ }) });
229
+ };
230
+ var SearchResultList = ({ results, onSelect }) => {
231
+ return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "epub-reader__search-list", children: results.map((r, idx) => /* @__PURE__ */ jsxRuntime.jsx("li", { className: "epub-reader__search-item", children: /* @__PURE__ */ jsxRuntime.jsxs(
232
+ "button",
233
+ {
234
+ type: "button",
235
+ className: "epub-reader__search-btn",
236
+ onClick: () => {
237
+ if (r.cfi) onSelect(r.cfi);
238
+ },
239
+ children: [
240
+ r.label ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__search-label", children: r.label }) : null,
241
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__search-excerpt", children: typeof r.excerpt === "string" ? r.excerpt : `${r.excerpt?.pre ?? ""}${r.excerpt?.match ?? ""}${r.excerpt?.post ?? ""}` })
242
+ ]
243
+ }
244
+ ) }, `${r.cfi ?? "no-cfi"}-${idx}`)) });
245
+ };
200
246
 
201
247
  // src/core/icons.ts
202
248
  var icons = {
@@ -241,226 +287,16 @@ var SvgIcon = ({ name, size = 24, color = "currentColor", className }) => {
241
287
  }
242
288
  );
243
289
  };
244
- var DesktopToolbar = ({
245
- onToggleToc,
246
- onToggleSearch,
247
- onPrevSection,
248
- onPrevPage,
249
- onNextPage,
250
- onNextSection,
251
- darkMode,
252
- onToggleDarkMode,
253
- fontSize,
254
- onFontSizeChange
255
- }) => {
256
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__toolbar", children: [
257
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__panel", children: [
258
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onToggleToc, title: "\u76EE\u5F55", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "list" }) }),
259
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onToggleSearch, title: "\u641C\u7D22", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "search" }) }),
260
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__divider" }),
261
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onPrevSection, title: "\u4E0A\u4E00\u7AE0", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "chevrons-left" }) }),
262
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onPrevPage, title: "\u4E0A\u4E00\u9875", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "chevron-left" }) }),
263
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onNextPage, title: "\u4E0B\u4E00\u9875", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "chevron-right" }) }),
264
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onNextSection, title: "\u4E0B\u4E00\u7AE0", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "chevrons-right" }) })
265
- ] }),
266
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__panel", children: [
267
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onToggleDarkMode, title: "\u4E3B\u9898", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: darkMode ? "sun" : "moon" }) }),
268
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__divider" }),
269
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onFontSizeChange(fontSize + 10), title: "\u589E\u5927\u5B57\u53F7", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "plus" }) }),
270
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__font", children: [
271
- fontSize,
272
- "%"
273
- ] }),
274
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onFontSizeChange(fontSize - 10), title: "\u51CF\u5C0F\u5B57\u53F7", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "minus" }) })
275
- ] })
276
- ] });
277
- };
278
- var DesktopBottomBar = ({
279
- status,
280
- errorText,
281
- sectionLabel,
282
- displayedPercent,
283
- onSeekStart,
284
- onSeekChange,
285
- onSeekEnd,
286
- onSeekCommit
287
- }) => {
288
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__bottom", children: [
289
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__bottom-left", children: [
290
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "epub-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "" }),
291
- sectionLabel ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "epub-reader__section", children: sectionLabel }) : null
292
- ] }),
293
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__bottom-right", children: [
294
- /* @__PURE__ */ jsxRuntime.jsx(
295
- "input",
296
- {
297
- className: "epub-reader__range",
298
- type: "range",
299
- min: 0,
300
- max: 100,
301
- step: 1,
302
- value: displayedPercent,
303
- onChange: (e) => {
304
- onSeekStart();
305
- onSeekChange(Number(e.target.value));
306
- },
307
- onPointerUp: (e) => {
308
- const v = Number(e.target.value);
309
- onSeekEnd(v);
310
- },
311
- onKeyUp: (e) => {
312
- if (e.key !== "Enter") return;
313
- const v = Number(e.target.value);
314
- onSeekCommit(v);
315
- }
316
- }
317
- ),
318
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "epub-reader__percent", children: [
319
- displayedPercent,
320
- "%"
321
- ] })
322
- ] })
323
- ] });
324
- };
325
- var TocTree = ({ items, onSelect }) => {
326
- return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "epub-reader__toc-list", children: items.map((item, idx) => {
327
- const key = item.href || `${item.label ?? "item"}-${idx}`;
328
- const hasChildren = Boolean(item.subitems?.length);
329
- const label = item.label || item.href || "\u672A\u547D\u540D";
330
- if (!hasChildren) {
331
- return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "epub-reader__toc-item", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__toc-btn", onClick: () => onSelect(item.href), children: label }) }, key);
332
- }
333
- return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "epub-reader__toc-item", children: /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "epub-reader__toc-details", children: [
334
- /* @__PURE__ */ jsxRuntime.jsx("summary", { className: "epub-reader__toc-summary", children: label }),
335
- /* @__PURE__ */ jsxRuntime.jsx(TocTree, { items: item.subitems ?? [], onSelect })
336
- ] }) }, key);
337
- }) });
338
- };
339
- var TocDrawer = ({ isOpen, onClose, toc, onSelect }) => {
340
- return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: `epub-reader__drawer ${isOpen ? "is-open" : ""}`, "aria-hidden": !isOpen, children: [
341
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__drawer-header", children: [
342
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__drawer-title", children: "\u76EE\u5F55" }),
343
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "x" }) })
344
- ] }),
345
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__drawer-body", children: toc.length ? /* @__PURE__ */ jsxRuntime.jsx(
346
- TocTree,
347
- {
348
- items: toc,
349
- onSelect: (href) => {
350
- onSelect(href);
351
- onClose();
352
- }
353
- }
354
- ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__empty", children: "\u672A\u627E\u5230\u76EE\u5F55" }) })
355
- ] });
356
- };
357
- var SearchResultList = ({ results, onSelect }) => {
358
- return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "epub-reader__search-list", children: results.map((r, idx) => /* @__PURE__ */ jsxRuntime.jsx("li", { className: "epub-reader__search-item", children: /* @__PURE__ */ jsxRuntime.jsxs(
359
- "button",
360
- {
361
- type: "button",
362
- className: "epub-reader__search-btn",
363
- onClick: () => {
364
- if (r.cfi) onSelect(r.cfi);
365
- },
366
- children: [
367
- r.label ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__search-label", children: r.label }) : null,
368
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__search-excerpt", children: typeof r.excerpt === "string" ? r.excerpt : `${r.excerpt?.pre ?? ""}${r.excerpt?.match ?? ""}${r.excerpt?.post ?? ""}` })
369
- ]
370
- }
371
- ) }, `${r.cfi ?? "no-cfi"}-${idx}`)) });
372
- };
373
- var SearchDrawer = ({
374
- isOpen,
375
- onClose,
376
- status,
377
- search,
378
- onSearch,
379
- onQueryChange,
380
- onOptionChange,
381
- onCancelSearch,
382
- onResultSelect
383
- }) => {
384
- return /* @__PURE__ */ jsxRuntime.jsxs("aside", { className: `epub-reader__drawer right ${isOpen ? "is-open" : ""}`, "aria-hidden": !isOpen, children: [
385
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__drawer-header", children: [
386
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__drawer-title", children: "\u641C\u7D22" }),
387
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "x" }) })
388
- ] }),
389
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__drawer-body", children: [
390
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__field", children: [
391
- /* @__PURE__ */ jsxRuntime.jsx(
392
- "input",
393
- {
394
- className: "epub-reader__input",
395
- placeholder: "\u8F93\u5165\u5173\u952E\u8BCD",
396
- value: search.query,
397
- onChange: (e) => {
398
- const v = e.target.value;
399
- onQueryChange(v);
400
- if (!v.trim()) onSearch("");
401
- },
402
- disabled: status !== "ready",
403
- onKeyDown: (e) => {
404
- if (e.key === "Enter") onSearch(search.query);
405
- }
406
- }
407
- ),
408
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onSearch(search.query), disabled: status !== "ready", children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "search" }) })
409
- ] }),
410
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__checks", children: [
411
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "epub-reader__check", children: [
412
- /* @__PURE__ */ jsxRuntime.jsx(
413
- "input",
414
- {
415
- type: "checkbox",
416
- checked: Boolean(search.options.matchCase),
417
- onChange: (e) => onOptionChange({ matchCase: e.target.checked })
418
- }
419
- ),
420
- "\u533A\u5206\u5927\u5C0F\u5199"
421
- ] }),
422
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "epub-reader__check", children: [
423
- /* @__PURE__ */ jsxRuntime.jsx(
424
- "input",
425
- {
426
- type: "checkbox",
427
- checked: Boolean(search.options.wholeWords),
428
- onChange: (e) => onOptionChange({ wholeWords: e.target.checked })
429
- }
430
- ),
431
- "\u5168\u8BCD\u5339\u914D"
432
- ] }),
433
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "epub-reader__check", children: [
434
- /* @__PURE__ */ jsxRuntime.jsx(
435
- "input",
436
- {
437
- type: "checkbox",
438
- checked: Boolean(search.options.matchDiacritics),
439
- onChange: (e) => onOptionChange({ matchDiacritics: e.target.checked })
440
- }
441
- ),
442
- "\u533A\u5206\u53D8\u97F3"
443
- ] })
444
- ] }),
445
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__meta", children: [
446
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
447
- "\u8FDB\u5EA6 ",
448
- search.progressPercent,
449
- "%"
450
- ] }),
451
- search.searching ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\u641C\u7D22\u4E2D\u2026" }) : null,
452
- search.searching ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "epub-reader__link", onClick: onCancelSearch, children: "\u53D6\u6D88" }) : null
453
- ] }),
454
- search.results.length ? /* @__PURE__ */ jsxRuntime.jsx(SearchResultList, { results: search.results, onSelect: onResultSelect }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__empty", children: search.query.trim() ? "\u65E0\u5339\u914D\u7ED3\u679C" : "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" })
455
- ] })
456
- ] });
457
- };
458
290
  var MobileUI = ({
459
291
  barVisible,
460
292
  activePanel,
461
293
  onTogglePanel,
462
294
  onClosePanel,
463
295
  toolbarRight,
296
+ onPrevSection,
297
+ onPrevPage,
298
+ onNextPage,
299
+ onNextSection,
464
300
  toc,
465
301
  search,
466
302
  status,
@@ -469,6 +305,8 @@ var MobileUI = ({
469
305
  displayedPercent,
470
306
  darkMode,
471
307
  fontSize,
308
+ lineHeight,
309
+ letterSpacing,
472
310
  onTocSelect,
473
311
  onSearch,
474
312
  onSearchQueryChange,
@@ -480,7 +318,9 @@ var MobileUI = ({
480
318
  onSeekEnd,
481
319
  onSeekCommit,
482
320
  onToggleDarkMode,
483
- onFontSizeChange
321
+ onFontSizeChange,
322
+ onLineHeightChange,
323
+ onLetterSpacingChange
484
324
  }) => {
485
325
  const mobileTitle = activePanel === "menu" ? "\u76EE\u5F55" : activePanel === "search" ? "\u641C\u7D22" : activePanel === "progress" ? "\u8FDB\u5EA6" : activePanel === "settings" ? "\u8BBE\u7F6E" : "";
486
326
  const displayedFontSize = Math.min(40, Math.max(10, Math.round(fontSize / 5)));
@@ -632,65 +472,63 @@ var MobileUI = ({
632
472
  children: tooltip.text
633
473
  }
634
474
  ),
635
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mbar-left", children: [
636
- /* @__PURE__ */ jsxRuntime.jsx(
637
- "button",
638
- {
639
- type: "button",
640
- className: "epub-reader__btn",
641
- onClick: () => togglePanelSafe("menu"),
642
- "aria-pressed": activePanel === "menu",
643
- onTouchStart: (e) => handleTouchStart(e, "\u76EE\u5F55"),
644
- onTouchEnd: handleTouchEnd,
645
- onTouchCancel: handleTouchEnd,
646
- title: "\u76EE\u5F55",
647
- children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "list" })
648
- }
649
- ),
650
- /* @__PURE__ */ jsxRuntime.jsx(
651
- "button",
652
- {
653
- type: "button",
654
- className: "epub-reader__btn",
655
- onClick: () => togglePanelSafe("search"),
656
- "aria-pressed": activePanel === "search",
657
- onTouchStart: (e) => handleTouchStart(e, "\u641C\u7D22"),
658
- onTouchEnd: handleTouchEnd,
659
- onTouchCancel: handleTouchEnd,
660
- title: "\u641C\u7D22",
661
- children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "search" })
662
- }
663
- ),
664
- /* @__PURE__ */ jsxRuntime.jsx(
665
- "button",
666
- {
667
- type: "button",
668
- className: "epub-reader__btn",
669
- onClick: () => togglePanelSafe("progress"),
670
- "aria-pressed": activePanel === "progress",
671
- onTouchStart: (e) => handleTouchStart(e, "\u8FDB\u5EA6"),
672
- onTouchEnd: handleTouchEnd,
673
- onTouchCancel: handleTouchEnd,
674
- title: "\u8FDB\u5EA6",
675
- children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "sliders" })
676
- }
677
- ),
678
- /* @__PURE__ */ jsxRuntime.jsx(
679
- "button",
680
- {
681
- type: "button",
682
- className: "epub-reader__btn",
683
- onClick: () => togglePanelSafe("settings"),
684
- "aria-pressed": activePanel === "settings",
685
- onTouchStart: (e) => handleTouchStart(e, "\u8BBE\u7F6E"),
686
- onTouchEnd: handleTouchEnd,
687
- onTouchCancel: handleTouchEnd,
688
- title: "\u8BBE\u7F6E",
689
- children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "settings" })
690
- }
691
- )
692
- ] }),
693
- toolbarRight ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__mbar-right", children: toolbarRight }) : null
475
+ /* @__PURE__ */ jsxRuntime.jsx(
476
+ "button",
477
+ {
478
+ type: "button",
479
+ className: "epub-reader__btn",
480
+ onClick: () => togglePanelSafe("menu"),
481
+ "aria-pressed": activePanel === "menu",
482
+ onTouchStart: (e) => handleTouchStart(e, "\u76EE\u5F55"),
483
+ onTouchEnd: handleTouchEnd,
484
+ onTouchCancel: handleTouchEnd,
485
+ title: "\u76EE\u5F55",
486
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "list" })
487
+ }
488
+ ),
489
+ /* @__PURE__ */ jsxRuntime.jsx(
490
+ "button",
491
+ {
492
+ type: "button",
493
+ className: "epub-reader__btn",
494
+ onClick: () => togglePanelSafe("search"),
495
+ "aria-pressed": activePanel === "search",
496
+ onTouchStart: (e) => handleTouchStart(e, "\u641C\u7D22"),
497
+ onTouchEnd: handleTouchEnd,
498
+ onTouchCancel: handleTouchEnd,
499
+ title: "\u641C\u7D22",
500
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "search" })
501
+ }
502
+ ),
503
+ /* @__PURE__ */ jsxRuntime.jsx(
504
+ "button",
505
+ {
506
+ type: "button",
507
+ className: "epub-reader__btn",
508
+ onClick: () => togglePanelSafe("progress"),
509
+ "aria-pressed": activePanel === "progress",
510
+ onTouchStart: (e) => handleTouchStart(e, "\u8FDB\u5EA6"),
511
+ onTouchEnd: handleTouchEnd,
512
+ onTouchCancel: handleTouchEnd,
513
+ title: "\u8FDB\u5EA6",
514
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "sliders" })
515
+ }
516
+ ),
517
+ /* @__PURE__ */ jsxRuntime.jsx(
518
+ "button",
519
+ {
520
+ type: "button",
521
+ className: "epub-reader__btn",
522
+ onClick: () => togglePanelSafe("settings"),
523
+ "aria-pressed": activePanel === "settings",
524
+ onTouchStart: (e) => handleTouchStart(e, "\u8BBE\u7F6E"),
525
+ onTouchEnd: handleTouchEnd,
526
+ onTouchCancel: handleTouchEnd,
527
+ title: "\u8BBE\u7F6E",
528
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "settings" })
529
+ }
530
+ ),
531
+ toolbarRight ?? null
694
532
  ] }),
695
533
  activePanel ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__moverlay", onClick: closePanelSafe }) : null,
696
534
  /* @__PURE__ */ jsxRuntime.jsxs(
@@ -796,6 +634,60 @@ var MobileUI = ({
796
634
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "epub-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "" }),
797
635
  sectionLabel ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: sectionLabel }) : null
798
636
  ] }),
637
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mnav", children: [
638
+ /* @__PURE__ */ jsxRuntime.jsx(
639
+ "button",
640
+ {
641
+ type: "button",
642
+ className: "epub-reader__btn",
643
+ onClick: onPrevSection,
644
+ onTouchStart: (e) => handleTouchStart(e, "\u4E0A\u4E00\u7AE0"),
645
+ onTouchEnd: handleTouchEnd,
646
+ onTouchCancel: handleTouchEnd,
647
+ title: "\u4E0A\u4E00\u7AE0",
648
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "chevrons-left" })
649
+ }
650
+ ),
651
+ /* @__PURE__ */ jsxRuntime.jsx(
652
+ "button",
653
+ {
654
+ type: "button",
655
+ className: "epub-reader__btn",
656
+ onClick: onPrevPage,
657
+ onTouchStart: (e) => handleTouchStart(e, "\u4E0A\u4E00\u9875"),
658
+ onTouchEnd: handleTouchEnd,
659
+ onTouchCancel: handleTouchEnd,
660
+ title: "\u4E0A\u4E00\u9875",
661
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "chevron-left" })
662
+ }
663
+ ),
664
+ /* @__PURE__ */ jsxRuntime.jsx(
665
+ "button",
666
+ {
667
+ type: "button",
668
+ className: "epub-reader__btn",
669
+ onClick: onNextPage,
670
+ onTouchStart: (e) => handleTouchStart(e, "\u4E0B\u4E00\u9875"),
671
+ onTouchEnd: handleTouchEnd,
672
+ onTouchCancel: handleTouchEnd,
673
+ title: "\u4E0B\u4E00\u9875",
674
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "chevron-right" })
675
+ }
676
+ ),
677
+ /* @__PURE__ */ jsxRuntime.jsx(
678
+ "button",
679
+ {
680
+ type: "button",
681
+ className: "epub-reader__btn",
682
+ onClick: onNextSection,
683
+ onTouchStart: (e) => handleTouchStart(e, "\u4E0B\u4E00\u7AE0"),
684
+ onTouchEnd: handleTouchEnd,
685
+ onTouchCancel: handleTouchEnd,
686
+ title: "\u4E0B\u4E00\u7AE0",
687
+ children: /* @__PURE__ */ jsxRuntime.jsx(SvgIcon, { name: "chevrons-right" })
688
+ }
689
+ )
690
+ ] }),
799
691
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__mprogress", children: [
800
692
  /* @__PURE__ */ jsxRuntime.jsx(
801
693
  "input",
@@ -868,6 +760,47 @@ var MobileUI = ({
868
760
  ] }),
869
761
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__mfont-a is-big", children: "A" })
870
762
  ] }),
763
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__msetting", children: [
764
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__msetting-head", children: [
765
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__msetting-label", children: "\u884C\u9AD8" }),
766
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__msetting-value", children: lineHeight.toFixed(2) })
767
+ ] }),
768
+ /* @__PURE__ */ jsxRuntime.jsx(
769
+ "input",
770
+ {
771
+ className: "epub-reader__range",
772
+ type: "range",
773
+ min: 1,
774
+ max: 3,
775
+ step: 0.05,
776
+ value: lineHeight,
777
+ "aria-label": "\u884C\u9AD8",
778
+ onChange: (e) => onLineHeightChange(Number(e.target.value))
779
+ }
780
+ )
781
+ ] }),
782
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__msetting", children: [
783
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__msetting-head", children: [
784
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__msetting-label", children: "\u5B57\u95F4\u8DDD" }),
785
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "epub-reader__msetting-value", children: [
786
+ letterSpacing.toFixed(2),
787
+ "em"
788
+ ] })
789
+ ] }),
790
+ /* @__PURE__ */ jsxRuntime.jsx(
791
+ "input",
792
+ {
793
+ className: "epub-reader__range",
794
+ type: "range",
795
+ min: 0,
796
+ max: 0.3,
797
+ step: 0.01,
798
+ value: letterSpacing,
799
+ "aria-label": "\u5B57\u95F4\u8DDD",
800
+ onChange: (e) => onLetterSpacingChange(Number(e.target.value))
801
+ }
802
+ )
803
+ ] }),
871
804
  /* @__PURE__ */ jsxRuntime.jsx(
872
805
  "button",
873
806
  {
@@ -935,10 +868,17 @@ var EBookReader = react.forwardRef(function EBookReader2({
935
868
  fileUrl,
936
869
  className,
937
870
  style,
871
+ themeColor,
938
872
  mobileToolbarRight,
939
873
  defaultFontSize = 100,
940
874
  fontSize: controlledFontSize,
941
875
  onFontSizeChange,
876
+ defaultLineHeight = 1.6,
877
+ lineHeight: controlledLineHeight,
878
+ onLineHeightChange,
879
+ defaultLetterSpacing = 0,
880
+ letterSpacing: controlledLetterSpacing,
881
+ onLetterSpacingChange,
942
882
  defaultDarkMode = false,
943
883
  darkMode: controlledDarkMode,
944
884
  onDarkModeChange,
@@ -954,16 +894,18 @@ var EBookReader = react.forwardRef(function EBookReader2({
954
894
  const [status, setStatus] = react.useState("idle");
955
895
  const [errorText, setErrorText] = react.useState("");
956
896
  const [toc, setToc] = react.useState([]);
957
- const [tocOpen, setTocOpen] = react.useState(false);
958
- const [searchOpen, setSearchOpen] = react.useState(false);
959
897
  const [progressInfo, setProgressInfo] = react.useState(null);
960
898
  const [isSeeking, setIsSeeking] = react.useState(false);
961
899
  const [seekPercent, setSeekPercent] = react.useState(0);
962
900
  const [downloadLoading, setDownloadLoading] = react.useState(null);
963
901
  const [uncontrolledFontSize, setUncontrolledFontSize] = react.useState(defaultFontSize);
964
902
  const [uncontrolledDarkMode, setUncontrolledDarkMode] = react.useState(defaultDarkMode);
903
+ const [uncontrolledLineHeight, setUncontrolledLineHeight] = react.useState(defaultLineHeight);
904
+ const [uncontrolledLetterSpacing, setUncontrolledLetterSpacing] = react.useState(defaultLetterSpacing);
965
905
  const fontSize = controlledFontSize ?? uncontrolledFontSize;
966
906
  const darkMode = controlledDarkMode ?? uncontrolledDarkMode;
907
+ const lineHeight = controlledLineHeight ?? uncontrolledLineHeight;
908
+ const letterSpacing = controlledLetterSpacing ?? uncontrolledLetterSpacing;
967
909
  const [search, setSearch] = react.useState({
968
910
  query: "",
969
911
  options: defaultSearchOptions,
@@ -977,6 +919,7 @@ var EBookReader = react.forwardRef(function EBookReader2({
977
919
  const layoutRef = react.useRef(layout);
978
920
  const boundDocsRef = react.useRef(/* @__PURE__ */ new WeakSet());
979
921
  const gestureRef = react.useRef({ startX: 0, startY: 0, startAt: 0, tracking: false, moved: false, actionTaken: false });
922
+ const pcDragRef = react.useRef({ startX: 0, startY: 0, tracking: false, actionTaken: false });
980
923
  const isDraggingRef = react.useRef(false);
981
924
  const percentage = react.useMemo(() => Math.round((progressInfo?.fraction ?? 0) * 100), [progressInfo]);
982
925
  const displayedPercent = isSeeking ? seekPercent : percentage;
@@ -1006,11 +949,19 @@ var EBookReader = react.forwardRef(function EBookReader2({
1006
949
  setMobilePanel((prev) => prev === panel ? null : panel);
1007
950
  }, []);
1008
951
  const onPointerDown = react.useCallback((e) => {
1009
- if (layoutRef.current !== "mobile") return;
1010
952
  const t = e.target;
1011
953
  if (!t) return;
1012
954
  if (t.closest(".epub-reader__mbar") || t.closest(".epub-reader__msheet")) return;
1013
955
  if (t.closest('a,button,input,textarea,select,label,[role="button"],[contenteditable="true"]')) return;
956
+ if (layoutRef.current !== "mobile") {
957
+ if (e.pointerType !== "mouse") return;
958
+ if ((e.buttons & 1) !== 1) return;
959
+ pcDragRef.current.tracking = true;
960
+ pcDragRef.current.actionTaken = false;
961
+ pcDragRef.current.startX = e.screenX;
962
+ pcDragRef.current.startY = e.screenY;
963
+ return;
964
+ }
1014
965
  gestureRef.current.tracking = true;
1015
966
  gestureRef.current.moved = false;
1016
967
  gestureRef.current.actionTaken = false;
@@ -1019,6 +970,26 @@ var EBookReader = react.forwardRef(function EBookReader2({
1019
970
  gestureRef.current.startY = e.screenY;
1020
971
  }, []);
1021
972
  const onPointerMove = react.useCallback((e) => {
973
+ if (layoutRef.current !== "mobile") {
974
+ if (!pcDragRef.current.tracking) return;
975
+ if (e.pointerType !== "mouse" || (e.buttons & 1) !== 1) {
976
+ pcDragRef.current.tracking = false;
977
+ return;
978
+ }
979
+ const dx2 = e.screenX - pcDragRef.current.startX;
980
+ const dy2 = e.screenY - pcDragRef.current.startY;
981
+ if (Math.abs(dy2) > Math.abs(dx2) && Math.abs(dy2) >= 16) {
982
+ pcDragRef.current.tracking = false;
983
+ return;
984
+ }
985
+ if (Math.abs(dx2) >= 60 && Math.abs(dx2) > Math.abs(dy2)) {
986
+ pcDragRef.current.actionTaken = true;
987
+ pcDragRef.current.tracking = false;
988
+ if (dx2 > 0) readerRef.current?.prevPage();
989
+ else readerRef.current?.nextPage();
990
+ }
991
+ return;
992
+ }
1022
993
  if (!gestureRef.current.tracking) return;
1023
994
  const dx = e.screenX - gestureRef.current.startX;
1024
995
  const dy = e.screenY - gestureRef.current.startY;
@@ -1043,7 +1014,8 @@ var EBookReader = react.forwardRef(function EBookReader2({
1043
1014
  }, []);
1044
1015
  const onPointerEnd = react.useCallback((e) => {
1045
1016
  if (layoutRef.current !== "mobile") {
1046
- gestureRef.current.tracking = false;
1017
+ pcDragRef.current.tracking = false;
1018
+ pcDragRef.current.actionTaken = false;
1047
1019
  return;
1048
1020
  }
1049
1021
  if (gestureRef.current.actionTaken) {
@@ -1101,9 +1073,26 @@ var EBookReader = react.forwardRef(function EBookReader2({
1101
1073
  },
1102
1074
  [controlledFontSize, onFontSizeChange]
1103
1075
  );
1104
- const closeDrawers = react.useCallback(() => {
1105
- setTocOpen(false);
1106
- setSearchOpen(false);
1076
+ const setLineHeightInternal = react.useCallback(
1077
+ (next) => {
1078
+ const safe = clamp(next, 1, 3);
1079
+ if (controlledLineHeight == null) setUncontrolledLineHeight(safe);
1080
+ onLineHeightChange?.(safe);
1081
+ readerRef.current?.setLineHeight(safe);
1082
+ },
1083
+ [controlledLineHeight, onLineHeightChange]
1084
+ );
1085
+ const setLetterSpacingInternal = react.useCallback(
1086
+ (next) => {
1087
+ const safe = clamp(next, 0, 0.3);
1088
+ if (controlledLetterSpacing == null) setUncontrolledLetterSpacing(safe);
1089
+ onLetterSpacingChange?.(safe);
1090
+ readerRef.current?.setLetterSpacing(safe);
1091
+ },
1092
+ [controlledLetterSpacing, onLetterSpacingChange]
1093
+ );
1094
+ const closePanels = react.useCallback(() => {
1095
+ setMobilePanel(null);
1107
1096
  }, []);
1108
1097
  const prepareOpen = react.useCallback(() => {
1109
1098
  setStatus("opening");
@@ -1163,6 +1152,8 @@ var EBookReader = react.forwardRef(function EBookReader2({
1163
1152
  const reader = createEBookReader(host, {
1164
1153
  darkMode,
1165
1154
  fontSize,
1155
+ lineHeight,
1156
+ letterSpacing,
1166
1157
  onReady: (h) => onReady?.(h),
1167
1158
  onError: (e) => onError?.(e),
1168
1159
  onProgress: (info) => {
@@ -1237,6 +1228,12 @@ var EBookReader = react.forwardRef(function EBookReader2({
1237
1228
  react.useEffect(() => {
1238
1229
  readerRef.current?.setFontSize(fontSize);
1239
1230
  }, [fontSize]);
1231
+ react.useEffect(() => {
1232
+ readerRef.current?.setLineHeight(lineHeight);
1233
+ }, [lineHeight]);
1234
+ react.useEffect(() => {
1235
+ readerRef.current?.setLetterSpacing(letterSpacing);
1236
+ }, [letterSpacing]);
1240
1237
  react.useEffect(() => {
1241
1238
  if (!enableKeyboardNav) return;
1242
1239
  const root = rootRef.current;
@@ -1244,13 +1241,13 @@ var EBookReader = react.forwardRef(function EBookReader2({
1244
1241
  const handleKeyDown = (e) => {
1245
1242
  if (e.key === "ArrowLeft") readerRef.current?.prevPage();
1246
1243
  if (e.key === "ArrowRight") readerRef.current?.nextPage();
1247
- if (e.key === "Escape") closeDrawers();
1244
+ if (e.key === "Escape") closePanels();
1248
1245
  };
1249
1246
  root.addEventListener("keydown", handleKeyDown);
1250
1247
  return () => {
1251
1248
  root.removeEventListener("keydown", handleKeyDown);
1252
1249
  };
1253
- }, [closeDrawers, enableKeyboardNav]);
1250
+ }, [closePanels, enableKeyboardNav]);
1254
1251
  react.useImperativeHandle(
1255
1252
  ref,
1256
1253
  () => ({
@@ -1281,12 +1278,19 @@ var EBookReader = react.forwardRef(function EBookReader2({
1281
1278
  const handleSearchResultSelect = react.useCallback((cfi) => {
1282
1279
  if (cfi) readerRef.current?.goTo(cfi);
1283
1280
  }, []);
1281
+ const rootStyle = react.useMemo(() => {
1282
+ if (!themeColor) return style;
1283
+ return {
1284
+ ...style,
1285
+ ["--epub-reader-accent"]: themeColor
1286
+ };
1287
+ }, [style, themeColor]);
1284
1288
  return /* @__PURE__ */ jsxRuntime.jsxs(
1285
1289
  "div",
1286
1290
  {
1287
1291
  ref: rootRef,
1288
1292
  className: mergeClassName("epub-reader", className),
1289
- style,
1293
+ style: rootStyle,
1290
1294
  "data-theme": darkMode ? "dark" : "light",
1291
1295
  "data-layout": layout,
1292
1296
  tabIndex: 0,
@@ -1297,14 +1301,18 @@ var EBookReader = react.forwardRef(function EBookReader2({
1297
1301
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__spinner", "aria-hidden": "true" }),
1298
1302
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__loading-text", children: downloadLoading === "download" ? "\u52A0\u8F7D\u4E2D\u2026" : "\u6E32\u67D3\u4E2D\u2026" })
1299
1303
  ] }) : null,
1300
- layout === "mobile" ? /* @__PURE__ */ jsxRuntime.jsx(
1304
+ /* @__PURE__ */ jsxRuntime.jsx(
1301
1305
  MobileUI,
1302
1306
  {
1303
- barVisible: mobileBarVisible,
1307
+ barVisible: layout === "mobile" ? mobileBarVisible : true,
1304
1308
  activePanel: mobilePanel,
1305
1309
  onTogglePanel: toggleMobilePanel,
1306
1310
  onClosePanel: closeMobileSheet,
1307
1311
  toolbarRight: mobileToolbarRight,
1312
+ onPrevSection: () => readerRef.current?.prevSection(),
1313
+ onPrevPage: () => readerRef.current?.prevPage(),
1314
+ onNextPage: () => readerRef.current?.nextPage(),
1315
+ onNextSection: () => readerRef.current?.nextSection(),
1308
1316
  toc,
1309
1317
  search,
1310
1318
  status,
@@ -1313,6 +1321,8 @@ var EBookReader = react.forwardRef(function EBookReader2({
1313
1321
  displayedPercent,
1314
1322
  darkMode,
1315
1323
  fontSize,
1324
+ lineHeight,
1325
+ letterSpacing,
1316
1326
  onTocSelect: handleTocSelect,
1317
1327
  onSearch: (q) => void runSearch(q),
1318
1328
  onSearchQueryChange: (v) => setSearch((prev) => ({ ...prev, query: v })),
@@ -1324,62 +1334,11 @@ var EBookReader = react.forwardRef(function EBookReader2({
1324
1334
  onSeekEnd: handleSeekEnd,
1325
1335
  onSeekCommit: handleSeekEnd,
1326
1336
  onToggleDarkMode: setDarkModeInternal,
1327
- onFontSizeChange: setFontSizeInternal
1337
+ onFontSizeChange: setFontSizeInternal,
1338
+ onLineHeightChange: setLineHeightInternal,
1339
+ onLetterSpacingChange: setLetterSpacingInternal
1328
1340
  }
1329
- ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1330
- /* @__PURE__ */ jsxRuntime.jsx(
1331
- DesktopToolbar,
1332
- {
1333
- onToggleToc: () => setTocOpen(true),
1334
- onToggleSearch: () => setSearchOpen(true),
1335
- onPrevSection: () => readerRef.current?.prevSection(),
1336
- onPrevPage: () => readerRef.current?.prevPage(),
1337
- onNextPage: () => readerRef.current?.nextPage(),
1338
- onNextSection: () => readerRef.current?.nextSection(),
1339
- darkMode,
1340
- onToggleDarkMode: () => setDarkModeInternal(!darkMode),
1341
- fontSize,
1342
- onFontSizeChange: setFontSizeInternal
1343
- }
1344
- ),
1345
- (tocOpen || searchOpen) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "epub-reader__overlay", onClick: closeDrawers }),
1346
- /* @__PURE__ */ jsxRuntime.jsx(
1347
- TocDrawer,
1348
- {
1349
- isOpen: tocOpen,
1350
- onClose: () => setTocOpen(false),
1351
- toc,
1352
- onSelect: handleTocSelect
1353
- }
1354
- ),
1355
- /* @__PURE__ */ jsxRuntime.jsx(
1356
- SearchDrawer,
1357
- {
1358
- isOpen: searchOpen,
1359
- onClose: () => setSearchOpen(false),
1360
- status,
1361
- search,
1362
- onSearch: (q) => void runSearch(q),
1363
- onQueryChange: (v) => setSearch((prev) => ({ ...prev, query: v })),
1364
- onOptionChange: (opt) => setSearch((prev) => ({ ...prev, options: { ...prev.options, ...opt } })),
1365
- onCancelSearch: () => readerRef.current?.cancelSearch(),
1366
- onResultSelect: handleSearchResultSelect
1367
- }
1368
- ),
1369
- /* @__PURE__ */ jsxRuntime.jsx(
1370
- DesktopBottomBar,
1371
- {
1372
- status,
1373
- errorText,
1374
- sectionLabel,
1375
- displayedPercent,
1376
- onSeekStart: handleSeekStart,
1377
- onSeekChange: handleSeekChange,
1378
- onSeekEnd: handleSeekEnd,
1379
- onSeekCommit: handleSeekEnd
1380
- }
1381
- )
1382
- ] })
1341
+ )
1383
1342
  ]
1384
1343
  }
1385
1344
  );