@somecat/epub-reader 0.1.3 → 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.js CHANGED
@@ -2,504 +2,8 @@ import { forwardRef, useRef, useState, useMemo, useEffect, useCallback, useImper
2
2
  import 'foliate-js/view.js';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
 
5
- // src/styles/epub-reader.cssText.ts
6
- var epubReaderCssText = `
7
- :root {
8
- --epub-reader-panel-bg: #ffffff;
9
- --epub-reader-panel-fg: #1f2937;
10
- --epub-reader-border: rgba(0, 0, 0, 0.12);
11
- --epub-reader-accent: #2563eb;
12
- --epub-reader-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
13
- --epub-reader-radius: 10px;
14
- --epub-reader-bottom-bar-height: 40px;
15
- }
16
-
17
- .epub-reader-icon {
18
- display: inline-block;
19
- vertical-align: middle;
20
- }
21
-
22
- .epub-reader[data-theme='dark'] {
23
- --epub-reader-panel-bg: #141414;
24
- --epub-reader-panel-fg: #e5e7eb;
25
- --epub-reader-border: rgba(255, 255, 255, 0.14);
26
- --epub-reader-accent: #60a5fa;
27
- --epub-reader-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
28
- }
29
-
30
- .epub-reader {
31
- position: relative;
32
- width: 100%;
33
- height: 100%;
34
- outline: none;
35
- overflow: hidden;
36
- }
37
-
38
- .epub-reader__viewer {
39
- width: 100%;
40
- height: calc(100% - var(--epub-reader-bottom-bar-height));
41
- background: var(--epub-reader-panel-bg);
42
- transition: background-color 0.2s ease;
43
- }
44
-
45
- .epub-reader__toolbar {
46
- position: absolute;
47
- top: 16px;
48
- right: 16px;
49
- z-index: 10;
50
- display: flex;
51
- flex-direction: column;
52
- gap: 10px;
53
- }
54
-
55
- .epub-reader[data-layout='wide'] .epub-reader__toolbar {
56
- top: 50%;
57
- transform: translateY(-50%);
58
- }
59
-
60
- .epub-reader__panel {
61
- width: 88px;
62
- padding: 8px;
63
- border: 1px solid var(--epub-reader-border);
64
- border-radius: var(--epub-reader-radius);
65
- background: color-mix(in srgb, var(--epub-reader-panel-bg) 92%, transparent);
66
- color: var(--epub-reader-panel-fg);
67
- box-shadow: var(--epub-reader-shadow);
68
- backdrop-filter: blur(10px);
69
- display: flex;
70
- flex-direction: column;
71
- gap: 6px;
72
- align-items: stretch;
73
- }
74
-
75
- .epub-reader__btn {
76
- appearance: none;
77
- border: 1px solid var(--epub-reader-border);
78
- background: transparent;
79
- color: inherit;
80
- padding: 6px 8px;
81
- border-radius: 8px;
82
- font-size: 12px;
83
- line-height: 1;
84
- cursor: pointer;
85
- user-select: none;
86
- }
87
-
88
- .epub-reader__btn:disabled {
89
- opacity: 0.6;
90
- cursor: not-allowed;
91
- }
92
-
93
- .epub-reader__btn:hover:not(:disabled) {
94
- border-color: color-mix(in srgb, var(--epub-reader-accent) 60%, var(--epub-reader-border));
95
- }
96
-
97
- .epub-reader__divider {
98
- height: 1px;
99
- background: var(--epub-reader-border);
100
- margin: 2px 0;
101
- }
102
-
103
- .epub-reader__font {
104
- text-align: center;
105
- font-size: 12px;
106
- font-weight: 700;
107
- color: var(--epub-reader-accent);
108
- padding: 2px 0;
109
- }
110
-
111
- .epub-reader__overlay {
112
- position: absolute;
113
- inset: 0;
114
- background: rgba(0, 0, 0, 0.35);
115
- z-index: 20;
116
- }
117
-
118
- .epub-reader__drawer {
119
- position: absolute;
120
- top: 0;
121
- bottom: 0;
122
- left: 0;
123
- width: 320px;
124
- background: var(--epub-reader-panel-bg);
125
- color: var(--epub-reader-panel-fg);
126
- border-right: 1px solid var(--epub-reader-border);
127
- transform: translateX(-100%);
128
- transition: transform 0.2s ease;
129
- z-index: 30;
130
- display: flex;
131
- flex-direction: column;
132
- }
133
-
134
- .epub-reader__drawer.right {
135
- left: auto;
136
- right: 0;
137
- border-right: none;
138
- border-left: 1px solid var(--epub-reader-border);
139
- transform: translateX(100%);
140
- }
141
-
142
- .epub-reader__drawer.is-open {
143
- transform: translateX(0);
144
- }
145
-
146
- .epub-reader__drawer-header {
147
- display: flex;
148
- align-items: center;
149
- justify-content: space-between;
150
- gap: 8px;
151
- padding: 12px;
152
- border-bottom: 1px solid var(--epub-reader-border);
153
- }
154
-
155
- .epub-reader__drawer-title {
156
- font-size: 14px;
157
- font-weight: 700;
158
- }
159
-
160
- .epub-reader__drawer-body {
161
- padding: 12px;
162
- overflow: auto;
163
- flex: 1;
164
- }
165
-
166
- .epub-reader__empty {
167
- padding: 20px 8px;
168
- opacity: 0.7;
169
- text-align: center;
170
- font-size: 12px;
171
- }
172
-
173
- .epub-reader__toc-list {
174
- list-style: none;
175
- padding: 0;
176
- margin: 0;
177
- display: flex;
178
- flex-direction: column;
179
- gap: 4px;
180
- }
181
-
182
- .epub-reader__toc-item {
183
- margin: 0;
184
- }
185
-
186
- .epub-reader__toc-btn {
187
- width: 100%;
188
- text-align: left;
189
- padding: 8px 10px;
190
- border-radius: 8px;
191
- border: 1px solid var(--epub-reader-border);
192
- background: transparent;
193
- color: inherit;
194
- cursor: pointer;
195
- }
196
-
197
- .epub-reader__toc-btn:hover {
198
- border-color: color-mix(in srgb, var(--epub-reader-accent) 60%, var(--epub-reader-border));
199
- }
200
-
201
- .epub-reader__toc-details {
202
- border-radius: 8px;
203
- }
204
-
205
- .epub-reader__toc-summary {
206
- padding: 8px 10px;
207
- border-radius: 8px;
208
- cursor: pointer;
209
- border: 1px solid var(--epub-reader-border);
210
- user-select: none;
211
- }
212
-
213
- .epub-reader__toc-details[open] > .epub-reader__toc-summary {
214
- border-color: color-mix(in srgb, var(--epub-reader-accent) 60%, var(--epub-reader-border));
215
- }
216
-
217
- .epub-reader__toc-details > .epub-reader__toc-list {
218
- padding-left: 12px;
219
- margin-top: 6px;
220
- }
221
-
222
- .epub-reader__field {
223
- display: flex;
224
- align-items: center;
225
- gap: 8px;
226
- }
227
-
228
- .epub-reader__input {
229
- width: 100%;
230
- padding: 8px 10px;
231
- border-radius: 8px;
232
- border: 1px solid var(--epub-reader-border);
233
- background: transparent;
234
- color: inherit;
235
- outline: none;
236
- }
237
-
238
- .epub-reader__checks {
239
- display: flex;
240
- flex-wrap: wrap;
241
- gap: 10px;
242
- margin: 10px 0;
243
- font-size: 12px;
244
- }
245
-
246
- .epub-reader__check {
247
- display: inline-flex;
248
- align-items: center;
249
- gap: 6px;
250
- user-select: none;
251
- }
252
-
253
- .epub-reader__meta {
254
- display: flex;
255
- align-items: center;
256
- justify-content: space-between;
257
- gap: 8px;
258
- font-size: 12px;
259
- opacity: 0.9;
260
- margin: 6px 0 10px;
261
- }
262
-
263
- .epub-reader__link {
264
- appearance: none;
265
- border: none;
266
- background: transparent;
267
- color: var(--epub-reader-accent);
268
- cursor: pointer;
269
- padding: 0;
270
- }
271
-
272
- .epub-reader__search-list {
273
- list-style: none;
274
- padding: 0;
275
- margin: 0;
276
- display: flex;
277
- flex-direction: column;
278
- gap: 6px;
279
- }
280
-
281
- .epub-reader__search-item {
282
- margin: 0;
283
- }
284
-
285
- .epub-reader__search-btn {
286
- width: 100%;
287
- text-align: left;
288
- padding: 10px;
289
- border-radius: 10px;
290
- border: 1px solid var(--epub-reader-border);
291
- background: transparent;
292
- color: inherit;
293
- cursor: pointer;
294
- display: flex;
295
- flex-direction: column;
296
- gap: 6px;
297
- }
298
-
299
- .epub-reader__search-btn:hover {
300
- border-color: color-mix(in srgb, var(--epub-reader-accent) 60%, var(--epub-reader-border));
301
- }
302
-
303
- .epub-reader__search-label {
304
- font-size: 12px;
305
- opacity: 0.7;
306
- }
307
-
308
- .epub-reader__search-excerpt {
309
- font-size: 12px;
310
- line-height: 1.4;
311
- word-break: break-word;
312
- }
313
-
314
- .epub-reader__bottom {
315
- position: absolute;
316
- left: 0;
317
- right: 0;
318
- bottom: 0;
319
- height: var(--epub-reader-bottom-bar-height);
320
- border-top: 1px solid var(--epub-reader-border);
321
- background: color-mix(in srgb, var(--epub-reader-panel-bg) 92%, transparent);
322
- display: flex;
323
- align-items: center;
324
- justify-content: space-between;
325
- gap: 10px;
326
- padding: 0 12px;
327
- z-index: 10;
328
- backdrop-filter: blur(10px);
329
- }
330
-
331
- .epub-reader__bottom-left {
332
- display: flex;
333
- align-items: center;
334
- gap: 8px;
335
- min-width: 0;
336
- }
337
-
338
- .epub-reader__status {
339
- font-size: 12px;
340
- opacity: 0.8;
341
- white-space: nowrap;
342
- }
343
-
344
- .epub-reader__section {
345
- font-size: 12px;
346
- font-weight: 600;
347
- white-space: nowrap;
348
- overflow: hidden;
349
- text-overflow: ellipsis;
350
- max-width: 280px;
351
- }
352
-
353
- .epub-reader__bottom-right {
354
- display: flex;
355
- align-items: center;
356
- gap: 10px;
357
- min-width: 260px;
358
- }
359
-
360
- .epub-reader__range {
361
- width: 260px;
362
- }
363
-
364
- .epub-reader__percent {
365
- font-size: 12px;
366
- font-variant-numeric: tabular-nums;
367
- min-width: 42px;
368
- text-align: right;
369
- }
370
-
371
- .epub-reader[data-layout='mobile'] .epub-reader__viewer {
372
- height: 100%;
373
- touch-action: pan-x;
374
- }
375
-
376
- .epub-reader[data-layout='mobile'] .epub-reader__toolbar,
377
- .epub-reader[data-layout='mobile'] .epub-reader__bottom,
378
- .epub-reader[data-layout='mobile'] .epub-reader__drawer,
379
- .epub-reader[data-layout='mobile'] .epub-reader__overlay {
380
- display: none;
381
- }
382
-
383
- .epub-reader__mbar {
384
- position: absolute;
385
- left: 10px;
386
- right: 10px;
387
- bottom: 0;
388
- z-index: 15;
389
- display: flex;
390
- align-items: center;
391
- gap: 8px;
392
- padding: 10px;
393
- border: 1px solid var(--epub-reader-border);
394
- border-radius: calc(var(--epub-reader-radius) + 6px);
395
- background: color-mix(in srgb, var(--epub-reader-panel-bg) 92%, transparent);
396
- color: var(--epub-reader-panel-fg);
397
- box-shadow: var(--epub-reader-shadow);
398
- backdrop-filter: blur(10px);
399
- transform: translateY(calc(100% + 10px));
400
- transition: transform 0.2s ease;
401
- }
402
-
403
- .epub-reader__mbar.is-visible {
404
- transform: translateY(-4px);
405
- }
406
-
407
- .epub-reader__mbar .epub-reader__btn {
408
- flex: 1 1 0;
409
- text-align: center;
410
- padding: 10px 8px;
411
- font-size: 13px;
412
- }
413
-
414
- .epub-reader__moverlay {
415
- position: absolute;
416
- inset: 0;
417
- background: rgba(0, 0, 0, 0.35);
418
- z-index: 20;
419
- }
420
-
421
- .epub-reader__msheet {
422
- position: absolute;
423
- left: 0;
424
- right: 0;
425
- bottom: 0;
426
- height: 60%;
427
- max-height: calc(100% - 70px);
428
- background: var(--epub-reader-panel-bg);
429
- color: var(--epub-reader-panel-fg);
430
- border-top: 1px solid var(--epub-reader-border);
431
- border-top-left-radius: calc(var(--epub-reader-radius) + 10px);
432
- border-top-right-radius: calc(var(--epub-reader-radius) + 10px);
433
- transform: translateY(100%);
434
- transition: transform 0.25s ease;
435
- z-index: 30;
436
- display: flex;
437
- flex-direction: column;
438
- pointer-events: none;
439
- }
440
-
441
- .epub-reader__msheet.is-open {
442
- transform: translateY(0);
443
- pointer-events: auto;
444
- }
445
-
446
- .epub-reader__msheet-header {
447
- display: flex;
448
- align-items: center;
449
- justify-content: space-between;
450
- gap: 8px;
451
- padding: 12px 12px 10px;
452
- border-bottom: 1px solid var(--epub-reader-border);
453
- }
454
-
455
- .epub-reader__msheet-title {
456
- font-size: 14px;
457
- font-weight: 800;
458
- }
459
-
460
- .epub-reader__msheet-body {
461
- padding: 12px;
462
- overflow: auto;
463
- flex: 1;
464
- }
465
-
466
- .epub-reader__mprogress {
467
- display: flex;
468
- flex-direction: column;
469
- gap: 10px;
470
- margin-top: 10px;
471
- }
472
-
473
- .epub-reader__mprogress .epub-reader__range {
474
- width: 100%;
475
- }
476
-
477
- .epub-reader__mprogress-percent {
478
- text-align: center;
479
- font-weight: 800;
480
- color: var(--epub-reader-accent);
481
- }
482
-
483
- .epub-reader__mfont {
484
- display: flex;
485
- align-items: center;
486
- justify-content: center;
487
- gap: 12px;
488
- }
489
-
490
- `;
491
-
492
- // src/styles/ensureStyle.ts
493
- var STYLE_ELEMENT_ID = "somecat-epub-reader-style";
494
- var ensureEpubReaderStyle = () => {
495
- if (typeof document === "undefined") return;
496
- if (document.getElementById(STYLE_ELEMENT_ID)) return;
497
- const style = document.createElement("style");
498
- style.id = STYLE_ELEMENT_ID;
499
- style.textContent = epubReaderCssText;
500
- (document.head ?? document.documentElement).append(style);
501
- };
502
- var getContentCSS = (fontSize, isDark, extraCSS) => `
5
+ // src/react/EBookReader.tsx
6
+ var getContentCSS = (fontSize, isDark, lineHeight, letterSpacing, extraCSS) => `
503
7
  @namespace epub "http://www.idpf.org/2007/ops";
504
8
  html {
505
9
  color-scheme: ${isDark ? "dark" : "light"} !important;
@@ -508,9 +12,11 @@ body {
508
12
  background-color: transparent !important;
509
13
  color: ${isDark ? "#e0e0e0" : "black"} !important;
510
14
  font-size: ${fontSize}% !important;
15
+ line-height: ${lineHeight} !important;
16
+ letter-spacing: ${letterSpacing}em !important;
511
17
  }
512
18
  p {
513
- line-height: 1.6;
19
+ line-height: inherit !important;
514
20
  margin-bottom: 1em;
515
21
  }
516
22
  a {
@@ -530,6 +36,8 @@ function createEBookReader(container, options = {}) {
530
36
  const {
531
37
  darkMode: initialDarkMode = false,
532
38
  fontSize: initialFontSize = 100,
39
+ lineHeight: initialLineHeight = 1.6,
40
+ letterSpacing: initialLetterSpacing = 0,
533
41
  extraContentCSS,
534
42
  onReady,
535
43
  onError,
@@ -542,6 +50,8 @@ function createEBookReader(container, options = {}) {
542
50
  let toc = [];
543
51
  let fontSize = initialFontSize;
544
52
  let darkMode = initialDarkMode;
53
+ let lineHeight = initialLineHeight;
54
+ let letterSpacing = initialLetterSpacing;
545
55
  let searchToken = 0;
546
56
  container.innerHTML = "";
547
57
  const viewer = document.createElement("foliate-view");
@@ -553,7 +63,7 @@ function createEBookReader(container, options = {}) {
553
63
  const applyStyles = () => {
554
64
  if (destroyed) return;
555
65
  if (!viewer.renderer?.setStyles) return;
556
- viewer.renderer.setStyles(getContentCSS(fontSize, darkMode, extraContentCSS));
66
+ viewer.renderer.setStyles(getContentCSS(fontSize, darkMode, lineHeight, letterSpacing, extraContentCSS));
557
67
  requestAnimationFrame(() => {
558
68
  setTimeout(() => {
559
69
  if (destroyed) return;
@@ -629,6 +139,16 @@ function createEBookReader(container, options = {}) {
629
139
  fontSize = safe;
630
140
  applyStyles();
631
141
  },
142
+ setLineHeight(nextLineHeight) {
143
+ const safe = Math.min(3, Math.max(1, nextLineHeight));
144
+ lineHeight = safe;
145
+ applyStyles();
146
+ },
147
+ setLetterSpacing(nextLetterSpacing) {
148
+ const safe = Math.min(0.3, Math.max(0, nextLetterSpacing));
149
+ letterSpacing = safe;
150
+ applyStyles();
151
+ },
632
152
  async search(query, opts = {}) {
633
153
  const normalized = query.trim();
634
154
  if (!normalized) {
@@ -691,6 +211,36 @@ function createEBookReader(container, options = {}) {
691
211
  onReady?.(handle);
692
212
  return handle;
693
213
  }
214
+ var TocTree = ({ items, onSelect }) => {
215
+ return /* @__PURE__ */ jsx("ul", { className: "epub-reader__toc-list", children: items.map((item, idx) => {
216
+ const key = item.href || `${item.label ?? "item"}-${idx}`;
217
+ const hasChildren = Boolean(item.subitems?.length);
218
+ const label = item.label || item.href || "\u672A\u547D\u540D";
219
+ if (!hasChildren) {
220
+ return /* @__PURE__ */ jsx("li", { className: "epub-reader__toc-item", children: /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__toc-btn", onClick: () => onSelect(item.href), children: label }) }, key);
221
+ }
222
+ return /* @__PURE__ */ jsx("li", { className: "epub-reader__toc-item", children: /* @__PURE__ */ jsxs("details", { className: "epub-reader__toc-details", children: [
223
+ /* @__PURE__ */ jsx("summary", { className: "epub-reader__toc-summary", children: label }),
224
+ /* @__PURE__ */ jsx(TocTree, { items: item.subitems ?? [], onSelect })
225
+ ] }) }, key);
226
+ }) });
227
+ };
228
+ var SearchResultList = ({ results, onSelect }) => {
229
+ return /* @__PURE__ */ jsx("ul", { className: "epub-reader__search-list", children: results.map((r, idx) => /* @__PURE__ */ jsx("li", { className: "epub-reader__search-item", children: /* @__PURE__ */ jsxs(
230
+ "button",
231
+ {
232
+ type: "button",
233
+ className: "epub-reader__search-btn",
234
+ onClick: () => {
235
+ if (r.cfi) onSelect(r.cfi);
236
+ },
237
+ children: [
238
+ r.label ? /* @__PURE__ */ jsx("div", { className: "epub-reader__search-label", children: r.label }) : null,
239
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__search-excerpt", children: typeof r.excerpt === "string" ? r.excerpt : `${r.excerpt?.pre ?? ""}${r.excerpt?.match ?? ""}${r.excerpt?.post ?? ""}` })
240
+ ]
241
+ }
242
+ ) }, `${r.cfi ?? "no-cfi"}-${idx}`)) });
243
+ };
694
244
 
695
245
  // src/core/icons.ts
696
246
  var icons = {
@@ -706,7 +256,8 @@ var icons = {
706
256
  minus: '<path d="M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
707
257
  x: '<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
708
258
  type: '<path d="M4 7V4h16v3M9 20h6M12 4v16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
709
- sliders: '<path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
259
+ sliders: '<path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
260
+ settings: '<path fill="currentColor" fill-rule="evenodd" d="M12.563 3.2h-1.126l-.645 2.578l-.647.2a6.3 6.3 0 0 0-1.091.452l-.599.317l-2.28-1.368l-.796.797l1.368 2.28l-.317.598a6.3 6.3 0 0 0-.453 1.091l-.199.647l-2.578.645v1.126l2.578.645l.2.647q.173.568.452 1.091l.317.599l-1.368 2.28l.797.796l2.28-1.368l.598.317q.523.278 1.091.453l.647.199l.645 2.578h1.126l.645-2.578l.647-.2a6.3 6.3 0 0 0 1.091-.452l.599-.317l2.28 1.368l.796-.797l-1.368-2.28l.317-.598q.278-.523.453-1.091l.199-.647l2.578-.645v-1.126l-2.578-.645l-.2-.647a6.3 6.3 0 0 0-.452-1.091l-.317-.599l1.368-2.28l-.797-.796l-2.28 1.368l-.598-.317a6.3 6.3 0 0 0-1.091-.453l-.647-.199zm2.945 2.17l1.833-1.1a1 1 0 0 1 1.221.15l1.018 1.018a1 1 0 0 1 .15 1.221l-1.1 1.833q.33.62.54 1.3l2.073.519a1 1 0 0 1 .757.97v1.438a1 1 0 0 1-.757.97l-2.073.519q-.21.68-.54 1.3l1.1 1.833a1 1 0 0 1-.15 1.221l-1.018 1.018a1 1 0 0 1-1.221.15l-1.833-1.1q-.62.33-1.3.54l-.519 2.073a1 1 0 0 1-.97.757h-1.438a1 1 0 0 1-.97-.757l-.519-2.073a7.5 7.5 0 0 1-1.3-.54l-1.833 1.1a1 1 0 0 1-1.221-.15L4.42 18.562a1 1 0 0 1-.15-1.221l1.1-1.833a7.5 7.5 0 0 1-.54-1.3l-2.073-.519A1 1 0 0 1 2 12.72v-1.438a1 1 0 0 1 .757-.97l2.073-.519q.21-.68.54-1.3L4.27 6.66a1 1 0 0 1 .15-1.221L5.438 4.42a1 1 0 0 1 1.221-.15l1.833 1.1q.62-.33 1.3-.54l.519-2.073A1 1 0 0 1 11.28 2h1.438a1 1 0 0 1 .97.757l.519 2.073q.68.21 1.3.54zM12 14.8a2.8 2.8 0 1 0 0-5.6a2.8 2.8 0 0 0 0 5.6m0 1.2a4 4 0 1 1 0-8a4 4 0 0 1 0 8"/>'
710
261
  };
711
262
  var SvgIcon = ({ name, size = 24, color = "currentColor", className }) => {
712
263
  const iconPath = icons[name] || "";
@@ -734,225 +285,16 @@ var SvgIcon = ({ name, size = 24, color = "currentColor", className }) => {
734
285
  }
735
286
  );
736
287
  };
737
- var DesktopToolbar = ({
738
- onToggleToc,
739
- onToggleSearch,
740
- onPrevSection,
741
- onPrevPage,
742
- onNextPage,
743
- onNextSection,
744
- darkMode,
745
- onToggleDarkMode,
746
- fontSize,
747
- onFontSizeChange
748
- }) => {
749
- return /* @__PURE__ */ jsxs("div", { className: "epub-reader__toolbar", children: [
750
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__panel", children: [
751
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onToggleToc, title: "\u76EE\u5F55", children: /* @__PURE__ */ jsx(SvgIcon, { name: "list" }) }),
752
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onToggleSearch, title: "\u641C\u7D22", children: /* @__PURE__ */ jsx(SvgIcon, { name: "search" }) }),
753
- /* @__PURE__ */ jsx("div", { className: "epub-reader__divider" }),
754
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onPrevSection, title: "\u4E0A\u4E00\u7AE0", children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevrons-left" }) }),
755
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onPrevPage, title: "\u4E0A\u4E00\u9875", children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevron-left" }) }),
756
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onNextPage, title: "\u4E0B\u4E00\u9875", children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevron-right" }) }),
757
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onNextSection, title: "\u4E0B\u4E00\u7AE0", children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevrons-right" }) })
758
- ] }),
759
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__panel", children: [
760
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onToggleDarkMode, title: "\u4E3B\u9898", children: /* @__PURE__ */ jsx(SvgIcon, { name: darkMode ? "sun" : "moon" }) }),
761
- /* @__PURE__ */ jsx("div", { className: "epub-reader__divider" }),
762
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onFontSizeChange(fontSize + 10), title: "\u589E\u5927\u5B57\u53F7", children: /* @__PURE__ */ jsx(SvgIcon, { name: "plus" }) }),
763
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__font", children: [
764
- fontSize,
765
- "%"
766
- ] }),
767
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onFontSizeChange(fontSize - 10), title: "\u51CF\u5C0F\u5B57\u53F7", children: /* @__PURE__ */ jsx(SvgIcon, { name: "minus" }) })
768
- ] })
769
- ] });
770
- };
771
- var DesktopBottomBar = ({
772
- status,
773
- errorText,
774
- sectionLabel,
775
- displayedPercent,
776
- onSeekStart,
777
- onSeekChange,
778
- onSeekEnd,
779
- onSeekCommit
780
- }) => {
781
- return /* @__PURE__ */ jsxs("div", { className: "epub-reader__bottom", children: [
782
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__bottom-left", children: [
783
- /* @__PURE__ */ jsx("span", { className: "epub-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "\u5C31\u7EEA" }),
784
- sectionLabel ? /* @__PURE__ */ jsx("span", { className: "epub-reader__section", children: sectionLabel }) : null
785
- ] }),
786
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__bottom-right", children: [
787
- /* @__PURE__ */ jsx(
788
- "input",
789
- {
790
- className: "epub-reader__range",
791
- type: "range",
792
- min: 0,
793
- max: 100,
794
- step: 1,
795
- value: displayedPercent,
796
- onChange: (e) => {
797
- onSeekStart();
798
- onSeekChange(Number(e.target.value));
799
- },
800
- onPointerUp: (e) => {
801
- const v = Number(e.target.value);
802
- onSeekEnd(v);
803
- },
804
- onKeyUp: (e) => {
805
- if (e.key !== "Enter") return;
806
- const v = Number(e.target.value);
807
- onSeekCommit(v);
808
- }
809
- }
810
- ),
811
- /* @__PURE__ */ jsxs("span", { className: "epub-reader__percent", children: [
812
- displayedPercent,
813
- "%"
814
- ] })
815
- ] })
816
- ] });
817
- };
818
- var TocTree = ({ items, onSelect }) => {
819
- return /* @__PURE__ */ jsx("ul", { className: "epub-reader__toc-list", children: items.map((item, idx) => {
820
- const key = item.href || `${item.label ?? "item"}-${idx}`;
821
- const hasChildren = Boolean(item.subitems?.length);
822
- const label = item.label || item.href || "\u672A\u547D\u540D";
823
- if (!hasChildren) {
824
- return /* @__PURE__ */ jsx("li", { className: "epub-reader__toc-item", children: /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__toc-btn", onClick: () => onSelect(item.href), children: label }) }, key);
825
- }
826
- return /* @__PURE__ */ jsx("li", { className: "epub-reader__toc-item", children: /* @__PURE__ */ jsxs("details", { className: "epub-reader__toc-details", children: [
827
- /* @__PURE__ */ jsx("summary", { className: "epub-reader__toc-summary", children: label }),
828
- /* @__PURE__ */ jsx(TocTree, { items: item.subitems ?? [], onSelect })
829
- ] }) }, key);
830
- }) });
831
- };
832
- var TocDrawer = ({ isOpen, onClose, toc, onSelect }) => {
833
- return /* @__PURE__ */ jsxs("aside", { className: `epub-reader__drawer ${isOpen ? "is-open" : ""}`, "aria-hidden": !isOpen, children: [
834
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__drawer-header", children: [
835
- /* @__PURE__ */ jsx("div", { className: "epub-reader__drawer-title", children: "\u76EE\u5F55" }),
836
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onClose, children: /* @__PURE__ */ jsx(SvgIcon, { name: "x" }) })
837
- ] }),
838
- /* @__PURE__ */ jsx("div", { className: "epub-reader__drawer-body", children: toc.length ? /* @__PURE__ */ jsx(
839
- TocTree,
840
- {
841
- items: toc,
842
- onSelect: (href) => {
843
- onSelect(href);
844
- onClose();
845
- }
846
- }
847
- ) : /* @__PURE__ */ jsx("div", { className: "epub-reader__empty", children: "\u672A\u627E\u5230\u76EE\u5F55" }) })
848
- ] });
849
- };
850
- var SearchResultList = ({ results, onSelect }) => {
851
- return /* @__PURE__ */ jsx("ul", { className: "epub-reader__search-list", children: results.map((r, idx) => /* @__PURE__ */ jsx("li", { className: "epub-reader__search-item", children: /* @__PURE__ */ jsxs(
852
- "button",
853
- {
854
- type: "button",
855
- className: "epub-reader__search-btn",
856
- onClick: () => {
857
- if (r.cfi) onSelect(r.cfi);
858
- },
859
- children: [
860
- r.label ? /* @__PURE__ */ jsx("div", { className: "epub-reader__search-label", children: r.label }) : null,
861
- /* @__PURE__ */ jsx("div", { className: "epub-reader__search-excerpt", children: typeof r.excerpt === "string" ? r.excerpt : `${r.excerpt?.pre ?? ""}${r.excerpt?.match ?? ""}${r.excerpt?.post ?? ""}` })
862
- ]
863
- }
864
- ) }, `${r.cfi ?? "no-cfi"}-${idx}`)) });
865
- };
866
- var SearchDrawer = ({
867
- isOpen,
868
- onClose,
869
- status,
870
- search,
871
- onSearch,
872
- onQueryChange,
873
- onOptionChange,
874
- onCancelSearch,
875
- onResultSelect
876
- }) => {
877
- return /* @__PURE__ */ jsxs("aside", { className: `epub-reader__drawer right ${isOpen ? "is-open" : ""}`, "aria-hidden": !isOpen, children: [
878
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__drawer-header", children: [
879
- /* @__PURE__ */ jsx("div", { className: "epub-reader__drawer-title", children: "\u641C\u7D22" }),
880
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onClose, children: /* @__PURE__ */ jsx(SvgIcon, { name: "x" }) })
881
- ] }),
882
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__drawer-body", children: [
883
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__field", children: [
884
- /* @__PURE__ */ jsx(
885
- "input",
886
- {
887
- className: "epub-reader__input",
888
- placeholder: "\u8F93\u5165\u5173\u952E\u8BCD",
889
- value: search.query,
890
- onChange: (e) => {
891
- const v = e.target.value;
892
- onQueryChange(v);
893
- if (!v.trim()) onSearch("");
894
- },
895
- disabled: status !== "ready",
896
- onKeyDown: (e) => {
897
- if (e.key === "Enter") onSearch(search.query);
898
- }
899
- }
900
- ),
901
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onSearch(search.query), disabled: status !== "ready", children: /* @__PURE__ */ jsx(SvgIcon, { name: "search" }) })
902
- ] }),
903
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__checks", children: [
904
- /* @__PURE__ */ jsxs("label", { className: "epub-reader__check", children: [
905
- /* @__PURE__ */ jsx(
906
- "input",
907
- {
908
- type: "checkbox",
909
- checked: Boolean(search.options.matchCase),
910
- onChange: (e) => onOptionChange({ matchCase: e.target.checked })
911
- }
912
- ),
913
- "\u533A\u5206\u5927\u5C0F\u5199"
914
- ] }),
915
- /* @__PURE__ */ jsxs("label", { className: "epub-reader__check", children: [
916
- /* @__PURE__ */ jsx(
917
- "input",
918
- {
919
- type: "checkbox",
920
- checked: Boolean(search.options.wholeWords),
921
- onChange: (e) => onOptionChange({ wholeWords: e.target.checked })
922
- }
923
- ),
924
- "\u5168\u8BCD\u5339\u914D"
925
- ] }),
926
- /* @__PURE__ */ jsxs("label", { className: "epub-reader__check", children: [
927
- /* @__PURE__ */ jsx(
928
- "input",
929
- {
930
- type: "checkbox",
931
- checked: Boolean(search.options.matchDiacritics),
932
- onChange: (e) => onOptionChange({ matchDiacritics: e.target.checked })
933
- }
934
- ),
935
- "\u533A\u5206\u53D8\u97F3"
936
- ] })
937
- ] }),
938
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__meta", children: [
939
- /* @__PURE__ */ jsxs("span", { children: [
940
- "\u8FDB\u5EA6 ",
941
- search.progressPercent,
942
- "%"
943
- ] }),
944
- search.searching ? /* @__PURE__ */ jsx("span", { children: "\u641C\u7D22\u4E2D\u2026" }) : null,
945
- search.searching ? /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__link", onClick: onCancelSearch, children: "\u53D6\u6D88" }) : null
946
- ] }),
947
- search.results.length ? /* @__PURE__ */ jsx(SearchResultList, { results: search.results, onSelect: onResultSelect }) : /* @__PURE__ */ jsx("div", { className: "epub-reader__empty", children: search.query.trim() ? "\u65E0\u5339\u914D\u7ED3\u679C" : "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" })
948
- ] })
949
- ] });
950
- };
951
288
  var MobileUI = ({
952
289
  barVisible,
953
290
  activePanel,
954
291
  onTogglePanel,
955
292
  onClosePanel,
293
+ toolbarRight,
294
+ onPrevSection,
295
+ onPrevPage,
296
+ onNextPage,
297
+ onNextSection,
956
298
  toc,
957
299
  search,
958
300
  status,
@@ -961,6 +303,8 @@ var MobileUI = ({
961
303
  displayedPercent,
962
304
  darkMode,
963
305
  fontSize,
306
+ lineHeight,
307
+ letterSpacing,
964
308
  onTocSelect,
965
309
  onSearch,
966
310
  onSearchQueryChange,
@@ -972,11 +316,84 @@ var MobileUI = ({
972
316
  onSeekEnd,
973
317
  onSeekCommit,
974
318
  onToggleDarkMode,
975
- onFontSizeChange
319
+ onFontSizeChange,
320
+ onLineHeightChange,
321
+ onLetterSpacingChange
976
322
  }) => {
977
- const mobileTitle = activePanel === "menu" ? "\u76EE\u5F55" : activePanel === "search" ? "\u641C\u7D22" : activePanel === "progress" ? "\u8FDB\u5EA6" : activePanel === "theme" ? "\u660E\u6697" : activePanel === "font" ? "\u5B57\u53F7" : "";
323
+ const mobileTitle = activePanel === "menu" ? "\u76EE\u5F55" : activePanel === "search" ? "\u641C\u7D22" : activePanel === "progress" ? "\u8FDB\u5EA6" : activePanel === "settings" ? "\u8BBE\u7F6E" : "";
324
+ const displayedFontSize = Math.min(40, Math.max(10, Math.round(fontSize / 5)));
325
+ const [fontSliderValue, setFontSliderValue] = useState(displayedFontSize);
326
+ const [isFontDragging, setIsFontDragging] = useState(false);
327
+ const fontDebounceRef = useRef(null);
328
+ const fontPendingRef = useRef(displayedFontSize);
329
+ const fontSliderWrapRef = useRef(null);
330
+ const [fontSliderWidth, setFontSliderWidth] = useState(0);
331
+ const fontThumbSize = 34;
332
+ const fontMin = 10;
333
+ const fontMax = 40;
334
+ useEffect(() => {
335
+ setFontSliderValue(displayedFontSize);
336
+ fontPendingRef.current = displayedFontSize;
337
+ }, [displayedFontSize]);
338
+ useEffect(() => {
339
+ return () => {
340
+ if (fontDebounceRef.current) {
341
+ clearTimeout(fontDebounceRef.current);
342
+ fontDebounceRef.current = null;
343
+ }
344
+ };
345
+ }, []);
346
+ useEffect(() => {
347
+ if (activePanel !== "settings") return;
348
+ const el = fontSliderWrapRef.current;
349
+ if (!el) return;
350
+ const update = () => setFontSliderWidth(el.getBoundingClientRect().width);
351
+ update();
352
+ const ro = new ResizeObserver(() => update());
353
+ ro.observe(el);
354
+ return () => ro.disconnect();
355
+ }, [activePanel]);
356
+ const fontProgressPercent = (fontSliderValue - fontMin) / (fontMax - fontMin) * 100;
357
+ const fontThumbLeft = (() => {
358
+ if (!fontSliderWidth) return 0;
359
+ const percent = (fontSliderValue - fontMin) / (fontMax - fontMin);
360
+ const half = fontThumbSize / 2;
361
+ return Math.min(fontSliderWidth - half, Math.max(half, half + percent * (fontSliderWidth - fontThumbSize)));
362
+ })();
363
+ const flushFontSize = () => {
364
+ if (fontDebounceRef.current) {
365
+ clearTimeout(fontDebounceRef.current);
366
+ fontDebounceRef.current = null;
367
+ }
368
+ onFontSizeChange(fontPendingRef.current * 5);
369
+ };
370
+ const scheduleFontSize = (next) => {
371
+ fontPendingRef.current = next;
372
+ if (fontDebounceRef.current) clearTimeout(fontDebounceRef.current);
373
+ fontDebounceRef.current = window.setTimeout(() => {
374
+ fontDebounceRef.current = null;
375
+ onFontSizeChange(fontPendingRef.current * 5);
376
+ }, 80);
377
+ };
978
378
  const [tooltip, setTooltip] = useState(null);
979
379
  const timerRef = useRef(null);
380
+ const ignoreToggleRef = useRef(false);
381
+ const markIgnoreToggle = () => {
382
+ ignoreToggleRef.current = true;
383
+ window.setTimeout(() => {
384
+ ignoreToggleRef.current = false;
385
+ }, 350);
386
+ };
387
+ const closePanelSafe = () => {
388
+ onClosePanel();
389
+ handleTouchEnd();
390
+ const el = document.activeElement;
391
+ if (el && el instanceof HTMLElement) el.blur();
392
+ };
393
+ const togglePanelSafe = (panel) => {
394
+ if (ignoreToggleRef.current) return;
395
+ onTogglePanel(panel);
396
+ };
980
397
  const handleTouchStart = (e, text) => {
981
398
  const rect = e.currentTarget.getBoundingClientRect();
982
399
  timerRef.current = window.setTimeout(() => {
@@ -993,6 +410,42 @@ var MobileUI = ({
993
410
  }
994
411
  setTooltip(null);
995
412
  };
413
+ const sheetRef = useRef(null);
414
+ const dragRef = useRef({ startY: 0, currentY: 0, isDragging: false });
415
+ const handleHeaderTouchStart = (e) => {
416
+ handleTouchEnd();
417
+ dragRef.current.startY = e.touches[0].clientY;
418
+ dragRef.current.isDragging = true;
419
+ if (sheetRef.current) {
420
+ sheetRef.current.style.transition = "none";
421
+ }
422
+ };
423
+ const handleHeaderTouchMove = (e) => {
424
+ if (!dragRef.current.isDragging) return;
425
+ e.preventDefault();
426
+ const deltaY = e.touches[0].clientY - dragRef.current.startY;
427
+ if (deltaY > 0 && sheetRef.current) {
428
+ sheetRef.current.style.transform = `translateY(${deltaY}px)`;
429
+ dragRef.current.currentY = deltaY;
430
+ }
431
+ };
432
+ const handleHeaderTouchEnd = () => {
433
+ if (!dragRef.current.isDragging) return;
434
+ dragRef.current.isDragging = false;
435
+ if (sheetRef.current) {
436
+ sheetRef.current.style.transition = "";
437
+ if (dragRef.current.currentY > 80) {
438
+ markIgnoreToggle();
439
+ closePanelSafe();
440
+ setTimeout(() => {
441
+ if (sheetRef.current) sheetRef.current.style.transform = "";
442
+ }, 300);
443
+ } else {
444
+ sheetRef.current.style.transform = "";
445
+ }
446
+ }
447
+ dragRef.current.currentY = 0;
448
+ };
996
449
  return /* @__PURE__ */ jsxs(Fragment, { children: [
997
450
  /* @__PURE__ */ jsxs("div", { className: `epub-reader__mbar ${barVisible ? "is-visible" : ""}`, children: [
998
451
  tooltip && /* @__PURE__ */ jsx(
@@ -1022,7 +475,7 @@ var MobileUI = ({
1022
475
  {
1023
476
  type: "button",
1024
477
  className: "epub-reader__btn",
1025
- onClick: () => onTogglePanel("menu"),
478
+ onClick: () => togglePanelSafe("menu"),
1026
479
  "aria-pressed": activePanel === "menu",
1027
480
  onTouchStart: (e) => handleTouchStart(e, "\u76EE\u5F55"),
1028
481
  onTouchEnd: handleTouchEnd,
@@ -1036,7 +489,7 @@ var MobileUI = ({
1036
489
  {
1037
490
  type: "button",
1038
491
  className: "epub-reader__btn",
1039
- onClick: () => onTogglePanel("search"),
492
+ onClick: () => togglePanelSafe("search"),
1040
493
  "aria-pressed": activePanel === "search",
1041
494
  onTouchStart: (e) => handleTouchStart(e, "\u641C\u7D22"),
1042
495
  onTouchEnd: handleTouchEnd,
@@ -1050,7 +503,7 @@ var MobileUI = ({
1050
503
  {
1051
504
  type: "button",
1052
505
  className: "epub-reader__btn",
1053
- onClick: () => onTogglePanel("progress"),
506
+ onClick: () => togglePanelSafe("progress"),
1054
507
  "aria-pressed": activePanel === "progress",
1055
508
  onTouchStart: (e) => handleTouchStart(e, "\u8FDB\u5EA6"),
1056
509
  onTouchEnd: handleTouchEnd,
@@ -1064,161 +517,305 @@ var MobileUI = ({
1064
517
  {
1065
518
  type: "button",
1066
519
  className: "epub-reader__btn",
1067
- onClick: () => onTogglePanel("theme"),
1068
- "aria-pressed": activePanel === "theme",
1069
- onTouchStart: (e) => handleTouchStart(e, "\u660E\u6697"),
520
+ onClick: () => togglePanelSafe("settings"),
521
+ "aria-pressed": activePanel === "settings",
522
+ onTouchStart: (e) => handleTouchStart(e, "\u8BBE\u7F6E"),
1070
523
  onTouchEnd: handleTouchEnd,
1071
524
  onTouchCancel: handleTouchEnd,
1072
- title: "\u660E\u6697",
1073
- children: /* @__PURE__ */ jsx(SvgIcon, { name: "sun" })
525
+ title: "\u8BBE\u7F6E",
526
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "settings" })
1074
527
  }
1075
528
  ),
1076
- /* @__PURE__ */ jsx(
1077
- "button",
1078
- {
1079
- type: "button",
1080
- className: "epub-reader__btn",
1081
- onClick: () => onTogglePanel("font"),
1082
- "aria-pressed": activePanel === "font",
1083
- onTouchStart: (e) => handleTouchStart(e, "\u5B57\u53F7"),
1084
- onTouchEnd: handleTouchEnd,
1085
- onTouchCancel: handleTouchEnd,
1086
- title: "\u5B57\u53F7",
1087
- children: /* @__PURE__ */ jsx(SvgIcon, { name: "type" })
1088
- }
1089
- )
529
+ toolbarRight ?? null
1090
530
  ] }),
1091
- activePanel ? /* @__PURE__ */ jsx("div", { className: "epub-reader__moverlay", onClick: onClosePanel }) : null,
1092
- /* @__PURE__ */ jsxs("div", { className: `epub-reader__msheet ${activePanel ? "is-open" : ""}`, "aria-hidden": !activePanel, children: [
1093
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__msheet-header", children: [
1094
- /* @__PURE__ */ jsx("div", { className: "epub-reader__msheet-title", children: mobileTitle }),
1095
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: onClosePanel, children: /* @__PURE__ */ jsx(SvgIcon, { name: "x" }) })
1096
- ] }),
1097
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__msheet-body", children: [
1098
- activePanel === "menu" ? toc.length ? /* @__PURE__ */ jsx(
1099
- TocTree,
1100
- {
1101
- items: toc,
1102
- onSelect: (href) => {
1103
- onTocSelect(href);
1104
- onClosePanel();
531
+ activePanel ? /* @__PURE__ */ jsx("div", { className: "epub-reader__moverlay", onClick: closePanelSafe }) : null,
532
+ /* @__PURE__ */ jsxs(
533
+ "div",
534
+ {
535
+ ref: sheetRef,
536
+ className: `epub-reader__msheet ${activePanel ? "is-open" : ""}`,
537
+ "aria-hidden": !activePanel,
538
+ children: [
539
+ /* @__PURE__ */ jsxs(
540
+ "div",
541
+ {
542
+ className: "epub-reader__msheet-header",
543
+ onTouchStart: handleHeaderTouchStart,
544
+ onTouchMove: handleHeaderTouchMove,
545
+ onTouchEnd: handleHeaderTouchEnd,
546
+ children: [
547
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__msheet-title", children: mobileTitle }),
548
+ /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: closePanelSafe, children: /* @__PURE__ */ jsx(SvgIcon, { name: "x" }) })
549
+ ]
1105
550
  }
1106
- }
1107
- ) : /* @__PURE__ */ jsx("div", { className: "epub-reader__empty", children: "\u672A\u627E\u5230\u76EE\u5F55" }) : null,
1108
- activePanel === "search" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1109
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__field", children: [
1110
- /* @__PURE__ */ jsx(
1111
- "input",
551
+ ),
552
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__msheet-body", children: [
553
+ activePanel === "menu" ? toc.length ? /* @__PURE__ */ jsx(
554
+ TocTree,
1112
555
  {
1113
- className: "epub-reader__input",
1114
- placeholder: "\u8F93\u5165\u5173\u952E\u8BCD",
1115
- value: search.query,
1116
- onChange: (e) => {
1117
- const v = e.target.value;
1118
- onSearchQueryChange(v);
1119
- if (!v.trim()) onSearch("");
1120
- },
1121
- disabled: status !== "ready",
1122
- onKeyDown: (e) => {
1123
- if (e.key === "Enter") onSearch(search.query);
556
+ items: toc,
557
+ onSelect: (href) => {
558
+ onTocSelect(href);
559
+ closePanelSafe();
1124
560
  }
1125
561
  }
1126
- ),
1127
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onSearch(search.query), disabled: status !== "ready", children: "\u641C\u7D22" })
1128
- ] }),
1129
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__checks", children: [
1130
- /* @__PURE__ */ jsxs("label", { className: "epub-reader__check", children: [
1131
- /* @__PURE__ */ jsx(
1132
- "input",
1133
- {
1134
- type: "checkbox",
1135
- checked: Boolean(search.options.matchCase),
1136
- onChange: (e) => onSearchOptionChange({ matchCase: e.target.checked })
1137
- }
1138
- ),
1139
- "\u533A\u5206\u5927\u5C0F\u5199"
1140
- ] }),
1141
- /* @__PURE__ */ jsxs("label", { className: "epub-reader__check", children: [
562
+ ) : /* @__PURE__ */ jsx("div", { className: "epub-reader__empty", children: "\u672A\u627E\u5230\u76EE\u5F55" }) : null,
563
+ activePanel === "search" ? /* @__PURE__ */ jsxs(Fragment, { children: [
564
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__field", children: [
565
+ /* @__PURE__ */ jsx(
566
+ "input",
567
+ {
568
+ className: "epub-reader__input",
569
+ placeholder: "\u8F93\u5165\u5173\u952E\u8BCD",
570
+ value: search.query,
571
+ onChange: (e) => {
572
+ const v = e.target.value;
573
+ onSearchQueryChange(v);
574
+ if (!v.trim()) onSearch("");
575
+ },
576
+ disabled: status !== "ready",
577
+ onKeyDown: (e) => {
578
+ if (e.key === "Enter") onSearch(search.query);
579
+ }
580
+ }
581
+ ),
582
+ /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onSearch(search.query), disabled: status !== "ready", children: "\u641C\u7D22" })
583
+ ] }),
584
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__checks", children: [
585
+ /* @__PURE__ */ jsxs("label", { className: "epub-reader__check", children: [
586
+ /* @__PURE__ */ jsx(
587
+ "input",
588
+ {
589
+ type: "checkbox",
590
+ checked: Boolean(search.options.matchCase),
591
+ onChange: (e) => onSearchOptionChange({ matchCase: e.target.checked })
592
+ }
593
+ ),
594
+ "\u533A\u5206\u5927\u5C0F\u5199"
595
+ ] }),
596
+ /* @__PURE__ */ jsxs("label", { className: "epub-reader__check", children: [
597
+ /* @__PURE__ */ jsx(
598
+ "input",
599
+ {
600
+ type: "checkbox",
601
+ checked: Boolean(search.options.wholeWords),
602
+ onChange: (e) => onSearchOptionChange({ wholeWords: e.target.checked })
603
+ }
604
+ ),
605
+ "\u5168\u8BCD\u5339\u914D"
606
+ ] }),
607
+ /* @__PURE__ */ jsxs("label", { className: "epub-reader__check", children: [
608
+ /* @__PURE__ */ jsx(
609
+ "input",
610
+ {
611
+ type: "checkbox",
612
+ checked: Boolean(search.options.matchDiacritics),
613
+ onChange: (e) => onSearchOptionChange({ matchDiacritics: e.target.checked })
614
+ }
615
+ ),
616
+ "\u533A\u5206\u53D8\u97F3"
617
+ ] })
618
+ ] }),
619
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__meta", children: [
620
+ /* @__PURE__ */ jsxs("span", { children: [
621
+ "\u8FDB\u5EA6 ",
622
+ search.progressPercent,
623
+ "%"
624
+ ] }),
625
+ search.searching ? /* @__PURE__ */ jsx("span", { children: "\u641C\u7D22\u4E2D\u2026" }) : null,
626
+ search.searching ? /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__link", onClick: onCancelSearch, children: "\u53D6\u6D88" }) : null
627
+ ] }),
628
+ search.results.length ? /* @__PURE__ */ jsx(SearchResultList, { results: search.results, onSelect: onSearchResultSelect }) : /* @__PURE__ */ jsx("div", { className: "epub-reader__empty", children: search.query.trim() ? "\u65E0\u5339\u914D\u7ED3\u679C" : "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" })
629
+ ] }) : null,
630
+ activePanel === "progress" ? /* @__PURE__ */ jsxs(Fragment, { children: [
631
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__meta", children: [
632
+ /* @__PURE__ */ jsx("span", { className: "epub-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "" }),
633
+ sectionLabel ? /* @__PURE__ */ jsx("span", { children: sectionLabel }) : null
634
+ ] }),
635
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__mnav", children: [
636
+ /* @__PURE__ */ jsx(
637
+ "button",
638
+ {
639
+ type: "button",
640
+ className: "epub-reader__btn",
641
+ onClick: onPrevSection,
642
+ onTouchStart: (e) => handleTouchStart(e, "\u4E0A\u4E00\u7AE0"),
643
+ onTouchEnd: handleTouchEnd,
644
+ onTouchCancel: handleTouchEnd,
645
+ title: "\u4E0A\u4E00\u7AE0",
646
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevrons-left" })
647
+ }
648
+ ),
649
+ /* @__PURE__ */ jsx(
650
+ "button",
651
+ {
652
+ type: "button",
653
+ className: "epub-reader__btn",
654
+ onClick: onPrevPage,
655
+ onTouchStart: (e) => handleTouchStart(e, "\u4E0A\u4E00\u9875"),
656
+ onTouchEnd: handleTouchEnd,
657
+ onTouchCancel: handleTouchEnd,
658
+ title: "\u4E0A\u4E00\u9875",
659
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevron-left" })
660
+ }
661
+ ),
662
+ /* @__PURE__ */ jsx(
663
+ "button",
664
+ {
665
+ type: "button",
666
+ className: "epub-reader__btn",
667
+ onClick: onNextPage,
668
+ onTouchStart: (e) => handleTouchStart(e, "\u4E0B\u4E00\u9875"),
669
+ onTouchEnd: handleTouchEnd,
670
+ onTouchCancel: handleTouchEnd,
671
+ title: "\u4E0B\u4E00\u9875",
672
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevron-right" })
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsx(
676
+ "button",
677
+ {
678
+ type: "button",
679
+ className: "epub-reader__btn",
680
+ onClick: onNextSection,
681
+ onTouchStart: (e) => handleTouchStart(e, "\u4E0B\u4E00\u7AE0"),
682
+ onTouchEnd: handleTouchEnd,
683
+ onTouchCancel: handleTouchEnd,
684
+ title: "\u4E0B\u4E00\u7AE0",
685
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: "chevrons-right" })
686
+ }
687
+ )
688
+ ] }),
689
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__mprogress", children: [
690
+ /* @__PURE__ */ jsx(
691
+ "input",
692
+ {
693
+ className: "epub-reader__range",
694
+ type: "range",
695
+ min: 0,
696
+ max: 100,
697
+ step: 1,
698
+ value: displayedPercent,
699
+ onChange: (e) => {
700
+ onSeekStart();
701
+ onSeekChange(Number(e.target.value));
702
+ },
703
+ onPointerUp: (e) => {
704
+ const v = Number(e.target.value);
705
+ onSeekEnd(v);
706
+ },
707
+ onKeyUp: (e) => {
708
+ if (e.key !== "Enter") return;
709
+ const v = Number(e.target.value);
710
+ onSeekCommit(v);
711
+ }
712
+ }
713
+ ),
714
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__mprogress-percent", children: [
715
+ displayedPercent,
716
+ "%"
717
+ ] })
718
+ ] })
719
+ ] }) : null,
720
+ activePanel === "settings" ? /* @__PURE__ */ jsxs("div", { className: "epub-reader__msettings", children: [
721
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__mfont-range", children: [
722
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__mfont-a is-small", children: "A" }),
723
+ /* @__PURE__ */ jsxs("div", { ref: fontSliderWrapRef, className: `epub-reader__mfont-slider ${isFontDragging ? "is-dragging" : ""}`, children: [
724
+ /* @__PURE__ */ jsx(
725
+ "input",
726
+ {
727
+ className: "epub-reader__range",
728
+ type: "range",
729
+ min: fontMin,
730
+ max: fontMax,
731
+ step: 1,
732
+ value: fontSliderValue,
733
+ onChange: (e) => {
734
+ const next = Number(e.target.value);
735
+ setFontSliderValue(next);
736
+ scheduleFontSize(next);
737
+ },
738
+ onPointerDown: () => setIsFontDragging(true),
739
+ onPointerUp: () => {
740
+ setIsFontDragging(false);
741
+ flushFontSize();
742
+ },
743
+ onPointerCancel: () => {
744
+ setIsFontDragging(false);
745
+ flushFontSize();
746
+ },
747
+ onKeyUp: (e) => {
748
+ if (e.key !== "Enter") return;
749
+ flushFontSize();
750
+ },
751
+ style: {
752
+ background: `linear-gradient(to right, var(--epub-reader-range-fill) 0%, var(--epub-reader-range-fill) ${fontProgressPercent}%, var(--epub-reader-range-track) ${fontProgressPercent}%, var(--epub-reader-range-track) 100%)`
753
+ },
754
+ "aria-label": "\u5B57\u53F7"
755
+ }
756
+ ),
757
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__mfont-thumb", style: { left: `${fontThumbLeft}px`, width: `${fontThumbSize}px`, height: `${fontThumbSize}px` }, children: fontSliderValue })
758
+ ] }),
759
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__mfont-a is-big", children: "A" })
760
+ ] }),
761
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__msetting", children: [
762
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__msetting-head", children: [
763
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__msetting-label", children: "\u884C\u9AD8" }),
764
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__msetting-value", children: lineHeight.toFixed(2) })
765
+ ] }),
766
+ /* @__PURE__ */ jsx(
767
+ "input",
768
+ {
769
+ className: "epub-reader__range",
770
+ type: "range",
771
+ min: 1,
772
+ max: 3,
773
+ step: 0.05,
774
+ value: lineHeight,
775
+ "aria-label": "\u884C\u9AD8",
776
+ onChange: (e) => onLineHeightChange(Number(e.target.value))
777
+ }
778
+ )
779
+ ] }),
780
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__msetting", children: [
781
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__msetting-head", children: [
782
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__msetting-label", children: "\u5B57\u95F4\u8DDD" }),
783
+ /* @__PURE__ */ jsxs("div", { className: "epub-reader__msetting-value", children: [
784
+ letterSpacing.toFixed(2),
785
+ "em"
786
+ ] })
787
+ ] }),
788
+ /* @__PURE__ */ jsx(
789
+ "input",
790
+ {
791
+ className: "epub-reader__range",
792
+ type: "range",
793
+ min: 0,
794
+ max: 0.3,
795
+ step: 0.01,
796
+ value: letterSpacing,
797
+ "aria-label": "\u5B57\u95F4\u8DDD",
798
+ onChange: (e) => onLetterSpacingChange(Number(e.target.value))
799
+ }
800
+ )
801
+ ] }),
1142
802
  /* @__PURE__ */ jsx(
1143
- "input",
803
+ "button",
1144
804
  {
1145
- type: "checkbox",
1146
- checked: Boolean(search.options.wholeWords),
1147
- onChange: (e) => onSearchOptionChange({ wholeWords: e.target.checked })
805
+ type: "button",
806
+ className: "epub-reader__btn",
807
+ onClick: () => onToggleDarkMode(!darkMode),
808
+ "aria-pressed": darkMode,
809
+ "aria-label": darkMode ? "\u6697\u9ED1\u6A21\u5F0F\uFF1A\u5F00\uFF0C\u70B9\u51FB\u5207\u6362\u5230\u4EAE\u8272" : "\u6697\u9ED1\u6A21\u5F0F\uFF1A\u5173\uFF0C\u70B9\u51FB\u5207\u6362\u5230\u6697\u9ED1",
810
+ title: darkMode ? "\u5207\u6362\u5230\u4EAE\u8272" : "\u5207\u6362\u5230\u6697\u9ED1",
811
+ children: /* @__PURE__ */ jsx(SvgIcon, { name: darkMode ? "sun" : "moon" })
1148
812
  }
1149
- ),
1150
- "\u5168\u8BCD\u5339\u914D"
1151
- ] }),
1152
- /* @__PURE__ */ jsxs("label", { className: "epub-reader__check", children: [
1153
- /* @__PURE__ */ jsx(
1154
- "input",
1155
- {
1156
- type: "checkbox",
1157
- checked: Boolean(search.options.matchDiacritics),
1158
- onChange: (e) => onSearchOptionChange({ matchDiacritics: e.target.checked })
1159
- }
1160
- ),
1161
- "\u533A\u5206\u53D8\u97F3"
1162
- ] })
1163
- ] }),
1164
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__meta", children: [
1165
- /* @__PURE__ */ jsxs("span", { children: [
1166
- "\u8FDB\u5EA6 ",
1167
- search.progressPercent,
1168
- "%"
1169
- ] }),
1170
- search.searching ? /* @__PURE__ */ jsx("span", { children: "\u641C\u7D22\u4E2D\u2026" }) : null,
1171
- search.searching ? /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__link", onClick: onCancelSearch, children: "\u53D6\u6D88" }) : null
1172
- ] }),
1173
- search.results.length ? /* @__PURE__ */ jsx(SearchResultList, { results: search.results, onSelect: onSearchResultSelect }) : /* @__PURE__ */ jsx("div", { className: "epub-reader__empty", children: search.query.trim() ? "\u65E0\u5339\u914D\u7ED3\u679C" : "\u8BF7\u8F93\u5165\u5173\u952E\u8BCD" })
1174
- ] }) : null,
1175
- activePanel === "progress" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1176
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__meta", children: [
1177
- /* @__PURE__ */ jsx("span", { className: "epub-reader__status", children: status === "error" ? errorText || "\u9519\u8BEF" : status === "opening" ? "\u6B63\u5728\u6253\u5F00\u2026" : "\u5C31\u7EEA" }),
1178
- sectionLabel ? /* @__PURE__ */ jsx("span", { children: sectionLabel }) : null
1179
- ] }),
1180
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__mprogress", children: [
1181
- /* @__PURE__ */ jsx(
1182
- "input",
1183
- {
1184
- className: "epub-reader__range",
1185
- type: "range",
1186
- min: 0,
1187
- max: 100,
1188
- step: 1,
1189
- value: displayedPercent,
1190
- onChange: (e) => {
1191
- onSeekStart();
1192
- onSeekChange(Number(e.target.value));
1193
- },
1194
- onPointerUp: (e) => {
1195
- const v = Number(e.target.value);
1196
- onSeekEnd(v);
1197
- },
1198
- onKeyUp: (e) => {
1199
- if (e.key !== "Enter") return;
1200
- const v = Number(e.target.value);
1201
- onSeekCommit(v);
1202
- }
1203
- }
1204
- ),
1205
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__mprogress-percent", children: [
1206
- displayedPercent,
1207
- "%"
1208
- ] })
813
+ )
814
+ ] }) : null
1209
815
  ] })
1210
- ] }) : null,
1211
- activePanel === "theme" ? /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onToggleDarkMode(!darkMode), children: darkMode ? "\u5207\u6362\u5230\u4EAE\u8272" : "\u5207\u6362\u5230\u6697\u9ED1" }) : null,
1212
- activePanel === "font" ? /* @__PURE__ */ jsxs("div", { className: "epub-reader__mfont", children: [
1213
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onFontSizeChange(fontSize - 10), children: "A-" }),
1214
- /* @__PURE__ */ jsxs("div", { className: "epub-reader__font", children: [
1215
- fontSize,
1216
- "%"
1217
- ] }),
1218
- /* @__PURE__ */ jsx("button", { type: "button", className: "epub-reader__btn", onClick: () => onFontSizeChange(fontSize + 10), children: "A+" })
1219
- ] }) : null
1220
- ] })
1221
- ] })
816
+ ]
817
+ }
818
+ )
1222
819
  ] });
1223
820
  };
1224
821
  var MOBILE_MAX_WIDTH = 768;
@@ -1269,9 +866,17 @@ var EBookReader = forwardRef(function EBookReader2({
1269
866
  fileUrl,
1270
867
  className,
1271
868
  style,
869
+ themeColor,
870
+ mobileToolbarRight,
1272
871
  defaultFontSize = 100,
1273
872
  fontSize: controlledFontSize,
1274
873
  onFontSizeChange,
874
+ defaultLineHeight = 1.6,
875
+ lineHeight: controlledLineHeight,
876
+ onLineHeightChange,
877
+ defaultLetterSpacing = 0,
878
+ letterSpacing: controlledLetterSpacing,
879
+ onLetterSpacingChange,
1275
880
  defaultDarkMode = false,
1276
881
  darkMode: controlledDarkMode,
1277
882
  onDarkModeChange,
@@ -1287,15 +892,18 @@ var EBookReader = forwardRef(function EBookReader2({
1287
892
  const [status, setStatus] = useState("idle");
1288
893
  const [errorText, setErrorText] = useState("");
1289
894
  const [toc, setToc] = useState([]);
1290
- const [tocOpen, setTocOpen] = useState(false);
1291
- const [searchOpen, setSearchOpen] = useState(false);
1292
895
  const [progressInfo, setProgressInfo] = useState(null);
1293
896
  const [isSeeking, setIsSeeking] = useState(false);
1294
897
  const [seekPercent, setSeekPercent] = useState(0);
898
+ const [downloadLoading, setDownloadLoading] = useState(null);
1295
899
  const [uncontrolledFontSize, setUncontrolledFontSize] = useState(defaultFontSize);
1296
900
  const [uncontrolledDarkMode, setUncontrolledDarkMode] = useState(defaultDarkMode);
901
+ const [uncontrolledLineHeight, setUncontrolledLineHeight] = useState(defaultLineHeight);
902
+ const [uncontrolledLetterSpacing, setUncontrolledLetterSpacing] = useState(defaultLetterSpacing);
1297
903
  const fontSize = controlledFontSize ?? uncontrolledFontSize;
1298
904
  const darkMode = controlledDarkMode ?? uncontrolledDarkMode;
905
+ const lineHeight = controlledLineHeight ?? uncontrolledLineHeight;
906
+ const letterSpacing = controlledLetterSpacing ?? uncontrolledLetterSpacing;
1299
907
  const [search, setSearch] = useState({
1300
908
  query: "",
1301
909
  options: defaultSearchOptions,
@@ -1309,6 +917,8 @@ var EBookReader = forwardRef(function EBookReader2({
1309
917
  const layoutRef = useRef(layout);
1310
918
  const boundDocsRef = useRef(/* @__PURE__ */ new WeakSet());
1311
919
  const gestureRef = useRef({ startX: 0, startY: 0, startAt: 0, tracking: false, moved: false, actionTaken: false });
920
+ const pcDragRef = useRef({ startX: 0, startY: 0, tracking: false, actionTaken: false });
921
+ const isDraggingRef = useRef(false);
1312
922
  const percentage = useMemo(() => Math.round((progressInfo?.fraction ?? 0) * 100), [progressInfo]);
1313
923
  const displayedPercent = isSeeking ? seekPercent : percentage;
1314
924
  const sectionLabel = progressInfo?.tocItem?.label ?? "";
@@ -1337,11 +947,19 @@ var EBookReader = forwardRef(function EBookReader2({
1337
947
  setMobilePanel((prev) => prev === panel ? null : panel);
1338
948
  }, []);
1339
949
  const onPointerDown = useCallback((e) => {
1340
- if (layoutRef.current !== "mobile") return;
1341
950
  const t = e.target;
1342
951
  if (!t) return;
1343
952
  if (t.closest(".epub-reader__mbar") || t.closest(".epub-reader__msheet")) return;
1344
953
  if (t.closest('a,button,input,textarea,select,label,[role="button"],[contenteditable="true"]')) return;
954
+ if (layoutRef.current !== "mobile") {
955
+ if (e.pointerType !== "mouse") return;
956
+ if ((e.buttons & 1) !== 1) return;
957
+ pcDragRef.current.tracking = true;
958
+ pcDragRef.current.actionTaken = false;
959
+ pcDragRef.current.startX = e.screenX;
960
+ pcDragRef.current.startY = e.screenY;
961
+ return;
962
+ }
1345
963
  gestureRef.current.tracking = true;
1346
964
  gestureRef.current.moved = false;
1347
965
  gestureRef.current.actionTaken = false;
@@ -1350,6 +968,26 @@ var EBookReader = forwardRef(function EBookReader2({
1350
968
  gestureRef.current.startY = e.screenY;
1351
969
  }, []);
1352
970
  const onPointerMove = useCallback((e) => {
971
+ if (layoutRef.current !== "mobile") {
972
+ if (!pcDragRef.current.tracking) return;
973
+ if (e.pointerType !== "mouse" || (e.buttons & 1) !== 1) {
974
+ pcDragRef.current.tracking = false;
975
+ return;
976
+ }
977
+ const dx2 = e.screenX - pcDragRef.current.startX;
978
+ const dy2 = e.screenY - pcDragRef.current.startY;
979
+ if (Math.abs(dy2) > Math.abs(dx2) && Math.abs(dy2) >= 16) {
980
+ pcDragRef.current.tracking = false;
981
+ return;
982
+ }
983
+ if (Math.abs(dx2) >= 60 && Math.abs(dx2) > Math.abs(dy2)) {
984
+ pcDragRef.current.actionTaken = true;
985
+ pcDragRef.current.tracking = false;
986
+ if (dx2 > 0) readerRef.current?.prevPage();
987
+ else readerRef.current?.nextPage();
988
+ }
989
+ return;
990
+ }
1353
991
  if (!gestureRef.current.tracking) return;
1354
992
  const dx = e.screenX - gestureRef.current.startX;
1355
993
  const dy = e.screenY - gestureRef.current.startY;
@@ -1374,7 +1012,8 @@ var EBookReader = forwardRef(function EBookReader2({
1374
1012
  }, []);
1375
1013
  const onPointerEnd = useCallback((e) => {
1376
1014
  if (layoutRef.current !== "mobile") {
1377
- gestureRef.current.tracking = false;
1015
+ pcDragRef.current.tracking = false;
1016
+ pcDragRef.current.actionTaken = false;
1378
1017
  return;
1379
1018
  }
1380
1019
  if (gestureRef.current.actionTaken) {
@@ -1432,9 +1071,26 @@ var EBookReader = forwardRef(function EBookReader2({
1432
1071
  },
1433
1072
  [controlledFontSize, onFontSizeChange]
1434
1073
  );
1435
- const closeDrawers = useCallback(() => {
1436
- setTocOpen(false);
1437
- setSearchOpen(false);
1074
+ const setLineHeightInternal = useCallback(
1075
+ (next) => {
1076
+ const safe = clamp(next, 1, 3);
1077
+ if (controlledLineHeight == null) setUncontrolledLineHeight(safe);
1078
+ onLineHeightChange?.(safe);
1079
+ readerRef.current?.setLineHeight(safe);
1080
+ },
1081
+ [controlledLineHeight, onLineHeightChange]
1082
+ );
1083
+ const setLetterSpacingInternal = useCallback(
1084
+ (next) => {
1085
+ const safe = clamp(next, 0, 0.3);
1086
+ if (controlledLetterSpacing == null) setUncontrolledLetterSpacing(safe);
1087
+ onLetterSpacingChange?.(safe);
1088
+ readerRef.current?.setLetterSpacing(safe);
1089
+ },
1090
+ [controlledLetterSpacing, onLetterSpacingChange]
1091
+ );
1092
+ const closePanels = useCallback(() => {
1093
+ setMobilePanel(null);
1438
1094
  }, []);
1439
1095
  const prepareOpen = useCallback(() => {
1440
1096
  setStatus("opening");
@@ -1494,10 +1150,15 @@ var EBookReader = forwardRef(function EBookReader2({
1494
1150
  const reader = createEBookReader(host, {
1495
1151
  darkMode,
1496
1152
  fontSize,
1153
+ lineHeight,
1154
+ letterSpacing,
1497
1155
  onReady: (h) => onReady?.(h),
1498
1156
  onError: (e) => onError?.(e),
1499
1157
  onProgress: (info) => {
1500
1158
  setProgressInfo(info);
1159
+ if (!isDraggingRef.current) {
1160
+ setIsSeeking(false);
1161
+ }
1501
1162
  onProgress?.(info);
1502
1163
  },
1503
1164
  onToc: (items) => setToc(items),
@@ -1536,18 +1197,28 @@ var EBookReader = forwardRef(function EBookReader2({
1536
1197
  if (!nextUrl) return;
1537
1198
  const controller = new AbortController();
1538
1199
  prepareOpen();
1200
+ setDownloadLoading("download");
1201
+ let active = true;
1539
1202
  void (async () => {
1540
1203
  try {
1541
1204
  const downloaded = await downloadEpubAsFile(nextUrl, controller.signal);
1205
+ if (!active) return;
1206
+ setDownloadLoading("open");
1542
1207
  await handleOpenFile(downloaded);
1543
1208
  } catch (e) {
1544
1209
  if (e?.name === "AbortError") return;
1545
1210
  setStatus("error");
1546
1211
  setErrorText(e?.message ? String(e.message) : "\u4E0B\u8F7D\u5931\u8D25");
1547
1212
  onError?.(e);
1213
+ } finally {
1214
+ if (active) setDownloadLoading(null);
1548
1215
  }
1549
1216
  })();
1550
- return () => controller.abort();
1217
+ return () => {
1218
+ active = false;
1219
+ setDownloadLoading(null);
1220
+ controller.abort();
1221
+ };
1551
1222
  }, [file, fileUrl, handleOpenFile, onError, prepareOpen]);
1552
1223
  useEffect(() => {
1553
1224
  readerRef.current?.setDarkMode(darkMode);
@@ -1555,6 +1226,12 @@ var EBookReader = forwardRef(function EBookReader2({
1555
1226
  useEffect(() => {
1556
1227
  readerRef.current?.setFontSize(fontSize);
1557
1228
  }, [fontSize]);
1229
+ useEffect(() => {
1230
+ readerRef.current?.setLineHeight(lineHeight);
1231
+ }, [lineHeight]);
1232
+ useEffect(() => {
1233
+ readerRef.current?.setLetterSpacing(letterSpacing);
1234
+ }, [letterSpacing]);
1558
1235
  useEffect(() => {
1559
1236
  if (!enableKeyboardNav) return;
1560
1237
  const root = rootRef.current;
@@ -1562,13 +1239,13 @@ var EBookReader = forwardRef(function EBookReader2({
1562
1239
  const handleKeyDown = (e) => {
1563
1240
  if (e.key === "ArrowLeft") readerRef.current?.prevPage();
1564
1241
  if (e.key === "ArrowRight") readerRef.current?.nextPage();
1565
- if (e.key === "Escape") closeDrawers();
1242
+ if (e.key === "Escape") closePanels();
1566
1243
  };
1567
1244
  root.addEventListener("keydown", handleKeyDown);
1568
1245
  return () => {
1569
1246
  root.removeEventListener("keydown", handleKeyDown);
1570
1247
  };
1571
- }, [closeDrawers, enableKeyboardNav]);
1248
+ }, [closePanels, enableKeyboardNav]);
1572
1249
  useImperativeHandle(
1573
1250
  ref,
1574
1251
  () => ({
@@ -1584,10 +1261,13 @@ var EBookReader = forwardRef(function EBookReader2({
1584
1261
  }),
1585
1262
  []
1586
1263
  );
1587
- const handleSeekStart = useCallback(() => setIsSeeking(true), []);
1264
+ const handleSeekStart = useCallback(() => {
1265
+ setIsSeeking(true);
1266
+ isDraggingRef.current = true;
1267
+ }, []);
1588
1268
  const handleSeekChange = useCallback((v) => setSeekPercent(v), []);
1589
1269
  const handleSeekEnd = useCallback((v) => {
1590
- setIsSeeking(false);
1270
+ isDraggingRef.current = false;
1591
1271
  readerRef.current?.goToFraction(v / 100);
1592
1272
  }, []);
1593
1273
  const handleTocSelect = useCallback((href) => {
@@ -1596,24 +1276,41 @@ var EBookReader = forwardRef(function EBookReader2({
1596
1276
  const handleSearchResultSelect = useCallback((cfi) => {
1597
1277
  if (cfi) readerRef.current?.goTo(cfi);
1598
1278
  }, []);
1279
+ const rootStyle = useMemo(() => {
1280
+ if (!themeColor) return style;
1281
+ return {
1282
+ ...style,
1283
+ ["--epub-reader-accent"]: themeColor
1284
+ };
1285
+ }, [style, themeColor]);
1599
1286
  return /* @__PURE__ */ jsxs(
1600
1287
  "div",
1601
1288
  {
1602
1289
  ref: rootRef,
1603
1290
  className: mergeClassName("epub-reader", className),
1604
- style,
1291
+ style: rootStyle,
1605
1292
  "data-theme": darkMode ? "dark" : "light",
1606
1293
  "data-layout": layout,
1607
1294
  tabIndex: 0,
1295
+ "aria-busy": downloadLoading != null,
1608
1296
  children: [
1609
1297
  /* @__PURE__ */ jsx("div", { className: "epub-reader__viewer", ref: viewerHostRef }),
1610
- layout === "mobile" ? /* @__PURE__ */ jsx(
1298
+ downloadLoading ? /* @__PURE__ */ jsxs("div", { className: "epub-reader__loading", role: "status", "aria-live": "polite", children: [
1299
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__spinner", "aria-hidden": "true" }),
1300
+ /* @__PURE__ */ jsx("div", { className: "epub-reader__loading-text", children: downloadLoading === "download" ? "\u52A0\u8F7D\u4E2D\u2026" : "\u6E32\u67D3\u4E2D\u2026" })
1301
+ ] }) : null,
1302
+ /* @__PURE__ */ jsx(
1611
1303
  MobileUI,
1612
1304
  {
1613
- barVisible: mobileBarVisible,
1305
+ barVisible: layout === "mobile" ? mobileBarVisible : true,
1614
1306
  activePanel: mobilePanel,
1615
1307
  onTogglePanel: toggleMobilePanel,
1616
1308
  onClosePanel: closeMobileSheet,
1309
+ toolbarRight: mobileToolbarRight,
1310
+ onPrevSection: () => readerRef.current?.prevSection(),
1311
+ onPrevPage: () => readerRef.current?.prevPage(),
1312
+ onNextPage: () => readerRef.current?.nextPage(),
1313
+ onNextSection: () => readerRef.current?.nextSection(),
1617
1314
  toc,
1618
1315
  search,
1619
1316
  status,
@@ -1622,6 +1319,8 @@ var EBookReader = forwardRef(function EBookReader2({
1622
1319
  displayedPercent,
1623
1320
  darkMode,
1624
1321
  fontSize,
1322
+ lineHeight,
1323
+ letterSpacing,
1625
1324
  onTocSelect: handleTocSelect,
1626
1325
  onSearch: (q) => void runSearch(q),
1627
1326
  onSearchQueryChange: (v) => setSearch((prev) => ({ ...prev, query: v })),
@@ -1633,70 +1332,16 @@ var EBookReader = forwardRef(function EBookReader2({
1633
1332
  onSeekEnd: handleSeekEnd,
1634
1333
  onSeekCommit: handleSeekEnd,
1635
1334
  onToggleDarkMode: setDarkModeInternal,
1636
- onFontSizeChange: setFontSizeInternal
1335
+ onFontSizeChange: setFontSizeInternal,
1336
+ onLineHeightChange: setLineHeightInternal,
1337
+ onLetterSpacingChange: setLetterSpacingInternal
1637
1338
  }
1638
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1639
- /* @__PURE__ */ jsx(
1640
- DesktopToolbar,
1641
- {
1642
- onToggleToc: () => setTocOpen(true),
1643
- onToggleSearch: () => setSearchOpen(true),
1644
- onPrevSection: () => readerRef.current?.prevSection(),
1645
- onPrevPage: () => readerRef.current?.prevPage(),
1646
- onNextPage: () => readerRef.current?.nextPage(),
1647
- onNextSection: () => readerRef.current?.nextSection(),
1648
- darkMode,
1649
- onToggleDarkMode: () => setDarkModeInternal(!darkMode),
1650
- fontSize,
1651
- onFontSizeChange: setFontSizeInternal
1652
- }
1653
- ),
1654
- (tocOpen || searchOpen) && /* @__PURE__ */ jsx("div", { className: "epub-reader__overlay", onClick: closeDrawers }),
1655
- /* @__PURE__ */ jsx(
1656
- TocDrawer,
1657
- {
1658
- isOpen: tocOpen,
1659
- onClose: () => setTocOpen(false),
1660
- toc,
1661
- onSelect: handleTocSelect
1662
- }
1663
- ),
1664
- /* @__PURE__ */ jsx(
1665
- SearchDrawer,
1666
- {
1667
- isOpen: searchOpen,
1668
- onClose: () => setSearchOpen(false),
1669
- status,
1670
- search,
1671
- onSearch: (q) => void runSearch(q),
1672
- onQueryChange: (v) => setSearch((prev) => ({ ...prev, query: v })),
1673
- onOptionChange: (opt) => setSearch((prev) => ({ ...prev, options: { ...prev.options, ...opt } })),
1674
- onCancelSearch: () => readerRef.current?.cancelSearch(),
1675
- onResultSelect: handleSearchResultSelect
1676
- }
1677
- ),
1678
- /* @__PURE__ */ jsx(
1679
- DesktopBottomBar,
1680
- {
1681
- status,
1682
- errorText,
1683
- sectionLabel,
1684
- displayedPercent,
1685
- onSeekStart: handleSeekStart,
1686
- onSeekChange: handleSeekChange,
1687
- onSeekEnd: handleSeekEnd,
1688
- onSeekCommit: handleSeekEnd
1689
- }
1690
- )
1691
- ] })
1339
+ )
1692
1340
  ]
1693
1341
  }
1694
1342
  );
1695
1343
  });
1696
1344
 
1697
- // src/react/index.ts
1698
- ensureEpubReaderStyle();
1699
-
1700
1345
  export { EBookReader };
1701
1346
  //# sourceMappingURL=react.js.map
1702
1347
  //# sourceMappingURL=react.js.map