@pressy-pub/components 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2618 @@
1
+ // src/Reader.tsx
2
+ import { useState, useEffect as useEffect3, useRef as useRef2, useCallback } from "preact/hooks";
3
+
4
+ // src/Navigation.tsx
5
+ import { jsx, jsxs } from "preact/jsx-runtime";
6
+ function Navigation({ prev, next }) {
7
+ if (!prev && !next) return null;
8
+ return /* @__PURE__ */ jsxs("nav", { class: "pressy-navigation", "aria-label": "Chapter navigation", children: [
9
+ /* @__PURE__ */ jsxs("div", { class: "pressy-nav-inner", children: [
10
+ prev ? /* @__PURE__ */ jsxs("a", { href: prev.slug, class: "pressy-nav-link pressy-nav-prev", children: [
11
+ /* @__PURE__ */ jsx("span", { class: "pressy-nav-direction", children: "Previous" }),
12
+ /* @__PURE__ */ jsx("span", { class: "pressy-nav-title", children: prev.title })
13
+ ] }) : /* @__PURE__ */ jsx("div", { class: "pressy-nav-placeholder" }),
14
+ next ? /* @__PURE__ */ jsxs("a", { href: next.slug, class: "pressy-nav-link pressy-nav-next", children: [
15
+ /* @__PURE__ */ jsx("span", { class: "pressy-nav-direction", children: "Next" }),
16
+ /* @__PURE__ */ jsx("span", { class: "pressy-nav-title", children: next.title })
17
+ ] }) : /* @__PURE__ */ jsx("div", { class: "pressy-nav-placeholder" })
18
+ ] }),
19
+ /* @__PURE__ */ jsx("style", { children: `
20
+ .pressy-navigation {
21
+ border-top: 1px solid var(--color-border);
22
+ padding: 2rem 1.5rem;
23
+ margin-top: 3rem;
24
+ }
25
+
26
+ .pressy-nav-inner {
27
+ display: flex;
28
+ justify-content: space-between;
29
+ gap: 2rem;
30
+ max-width: 65ch;
31
+ margin: 0 auto;
32
+ }
33
+
34
+ .pressy-nav-link {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 0.25rem;
38
+ text-decoration: none;
39
+ padding: 1rem;
40
+ border-radius: 0.5rem;
41
+ transition: background 0.15s;
42
+ max-width: 45%;
43
+ }
44
+
45
+ .pressy-nav-link:hover {
46
+ background: var(--color-bg-subtle);
47
+ }
48
+
49
+ .pressy-nav-prev {
50
+ align-items: flex-start;
51
+ }
52
+
53
+ .pressy-nav-next {
54
+ align-items: flex-end;
55
+ text-align: right;
56
+ }
57
+
58
+ .pressy-nav-direction {
59
+ font-size: var(--font-size-sm);
60
+ color: var(--color-text-muted);
61
+ text-transform: uppercase;
62
+ letter-spacing: 0.05em;
63
+ }
64
+
65
+ .pressy-nav-title {
66
+ color: var(--color-text);
67
+ font-weight: 500;
68
+ }
69
+
70
+ .pressy-nav-placeholder {
71
+ flex: 1;
72
+ }
73
+
74
+ @media (max-width: 640px) {
75
+ .pressy-nav-inner {
76
+ flex-direction: column;
77
+ gap: 1rem;
78
+ }
79
+
80
+ .pressy-nav-link {
81
+ max-width: 100%;
82
+ }
83
+
84
+ .pressy-nav-next {
85
+ align-items: flex-start;
86
+ text-align: left;
87
+ }
88
+ }
89
+ ` })
90
+ ] });
91
+ }
92
+
93
+ // src/TextShare.tsx
94
+ import { useSignal } from "@preact/signals";
95
+ import { useRef, useEffect } from "preact/hooks";
96
+ import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
97
+ function TextShare() {
98
+ const isVisible = useSignal(false);
99
+ const position = useSignal({ x: 0, y: 0 });
100
+ const selectedText = useSignal("");
101
+ const buttonRef = useRef(null);
102
+ useEffect(() => {
103
+ const handleSelectionChange = () => {
104
+ const selection = window.getSelection();
105
+ if (!selection || selection.isCollapsed) {
106
+ isVisible.value = false;
107
+ return;
108
+ }
109
+ const text = selection.toString().trim();
110
+ if (text.length < 5) {
111
+ isVisible.value = false;
112
+ return;
113
+ }
114
+ selectedText.value = text;
115
+ const range = selection.getRangeAt(0);
116
+ const rect = range.getBoundingClientRect();
117
+ position.value = {
118
+ x: rect.left + rect.width / 2,
119
+ y: rect.top - 10
120
+ };
121
+ isVisible.value = true;
122
+ };
123
+ document.addEventListener("selectionchange", handleSelectionChange);
124
+ document.addEventListener("mouseup", handleSelectionChange);
125
+ return () => {
126
+ document.removeEventListener("selectionchange", handleSelectionChange);
127
+ document.removeEventListener("mouseup", handleSelectionChange);
128
+ };
129
+ }, []);
130
+ const generateShareUrl = () => {
131
+ const encodedText = encodeURIComponent(selectedText.value);
132
+ const url = new URL(window.location.href);
133
+ url.hash = `:~:text=${encodedText}`;
134
+ return url.toString();
135
+ };
136
+ const handleShare = async () => {
137
+ const url = generateShareUrl();
138
+ const text = `"${selectedText.value}"`;
139
+ if (navigator.share) {
140
+ try {
141
+ await navigator.share({
142
+ text,
143
+ url
144
+ });
145
+ isVisible.value = false;
146
+ return;
147
+ } catch (err) {
148
+ }
149
+ }
150
+ try {
151
+ await navigator.clipboard.writeText(`${text}
152
+
153
+ ${url}`);
154
+ if (buttonRef.current) {
155
+ buttonRef.current.classList.add("copied");
156
+ setTimeout(() => {
157
+ buttonRef.current?.classList.remove("copied");
158
+ }, 2e3);
159
+ }
160
+ } catch (err) {
161
+ console.error("Failed to copy to clipboard:", err);
162
+ }
163
+ };
164
+ const handleCopyLink = async () => {
165
+ const url = generateShareUrl();
166
+ try {
167
+ await navigator.clipboard.writeText(url);
168
+ if (buttonRef.current) {
169
+ buttonRef.current.classList.add("copied");
170
+ setTimeout(() => {
171
+ buttonRef.current?.classList.remove("copied");
172
+ }, 2e3);
173
+ }
174
+ } catch (err) {
175
+ console.error("Failed to copy link:", err);
176
+ }
177
+ };
178
+ if (!isVisible.value) return null;
179
+ return /* @__PURE__ */ jsxs2(
180
+ "div",
181
+ {
182
+ ref: buttonRef,
183
+ class: "pressy-text-share",
184
+ style: {
185
+ left: `${position.value.x}px`,
186
+ top: `${position.value.y}px`
187
+ },
188
+ children: [
189
+ /* @__PURE__ */ jsx2(
190
+ "button",
191
+ {
192
+ class: "pressy-share-btn",
193
+ onClick: handleShare,
194
+ "aria-label": "Share selected text",
195
+ title: "Share quote",
196
+ children: /* @__PURE__ */ jsx2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92-1.31-2.92-2.92-2.92z" }) })
197
+ }
198
+ ),
199
+ /* @__PURE__ */ jsx2(
200
+ "button",
201
+ {
202
+ class: "pressy-copy-link-btn",
203
+ onClick: handleCopyLink,
204
+ "aria-label": "Copy link to selected text",
205
+ title: "Copy link",
206
+ children: /* @__PURE__ */ jsx2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx2("path", { d: "M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z" }) })
207
+ }
208
+ ),
209
+ /* @__PURE__ */ jsx2("style", { children: `
210
+ .pressy-text-share {
211
+ position: fixed;
212
+ transform: translate(-50%, -100%);
213
+ display: flex;
214
+ gap: 0.25rem;
215
+ background: var(--color-bg);
216
+ border: 1px solid var(--color-border);
217
+ border-radius: 0.5rem;
218
+ padding: 0.25rem;
219
+ box-shadow: var(--shadow-md);
220
+ z-index: 1000;
221
+ }
222
+
223
+ .pressy-text-share::after {
224
+ content: '';
225
+ position: absolute;
226
+ bottom: -6px;
227
+ left: 50%;
228
+ transform: translateX(-50%);
229
+ border-left: 6px solid transparent;
230
+ border-right: 6px solid transparent;
231
+ border-top: 6px solid var(--color-bg);
232
+ }
233
+
234
+ .pressy-share-btn,
235
+ .pressy-copy-link-btn {
236
+ display: flex;
237
+ align-items: center;
238
+ justify-content: center;
239
+ width: 2rem;
240
+ height: 2rem;
241
+ border: none;
242
+ background: transparent;
243
+ color: var(--color-text-muted);
244
+ cursor: pointer;
245
+ border-radius: 0.25rem;
246
+ transition: background 0.15s, color 0.15s;
247
+ }
248
+
249
+ .pressy-share-btn:hover,
250
+ .pressy-copy-link-btn:hover {
251
+ background: var(--color-bg-muted);
252
+ color: var(--color-text);
253
+ }
254
+
255
+ .pressy-text-share.copied .pressy-copy-link-btn {
256
+ color: var(--color-accent);
257
+ }
258
+ ` })
259
+ ]
260
+ }
261
+ );
262
+ }
263
+
264
+ // src/OfflineIndicator.tsx
265
+ import { useSignal as useSignal2 } from "@preact/signals";
266
+ import { useEffect as useEffect2 } from "preact/hooks";
267
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
268
+ function OfflineIndicator() {
269
+ const isOffline = useSignal2(!navigator.onLine);
270
+ const isVisible = useSignal2(false);
271
+ useEffect2(() => {
272
+ const handleOnline = () => {
273
+ isOffline.value = false;
274
+ isVisible.value = true;
275
+ setTimeout(() => {
276
+ isVisible.value = false;
277
+ }, 3e3);
278
+ };
279
+ const handleOffline = () => {
280
+ isOffline.value = true;
281
+ isVisible.value = true;
282
+ };
283
+ window.addEventListener("online", handleOnline);
284
+ window.addEventListener("offline", handleOffline);
285
+ return () => {
286
+ window.removeEventListener("online", handleOnline);
287
+ window.removeEventListener("offline", handleOffline);
288
+ };
289
+ }, []);
290
+ if (!isVisible.value) return null;
291
+ return /* @__PURE__ */ jsxs3("div", { class: `pressy-offline-indicator ${isOffline.value ? "offline" : "online"}`, children: [
292
+ isOffline.value ? /* @__PURE__ */ jsxs3(Fragment, { children: [
293
+ /* @__PURE__ */ jsx3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx3("path", { d: "M23.64 7c-.45-.34-4.93-4-11.64-4-1.5 0-2.89.19-4.15.48L18.18 13.8 23.64 7zM3.41 1.31L2 2.72l2.05 2.05C1.91 5.76.59 6.82.36 7L12 21.5l3.91-4.87 3.32 3.32 1.41-1.41L3.41 1.31z" }) }),
294
+ /* @__PURE__ */ jsx3("span", { children: "You're offline" })
295
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
296
+ /* @__PURE__ */ jsx3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx3("path", { d: "M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z" }) }),
297
+ /* @__PURE__ */ jsx3("span", { children: "Back online" })
298
+ ] }),
299
+ /* @__PURE__ */ jsx3("style", { children: `
300
+ .pressy-offline-indicator {
301
+ position: fixed;
302
+ bottom: 1.5rem;
303
+ left: 50%;
304
+ transform: translateX(-50%);
305
+ display: flex;
306
+ align-items: center;
307
+ gap: 0.5rem;
308
+ padding: 0.75rem 1rem;
309
+ border-radius: 2rem;
310
+ font-size: var(--font-size-sm);
311
+ font-weight: 500;
312
+ z-index: 1000;
313
+ animation: slideUp 0.3s ease;
314
+ }
315
+
316
+ .pressy-offline-indicator.offline {
317
+ background: #fef2f2;
318
+ color: #dc2626;
319
+ border: 1px solid #fecaca;
320
+ }
321
+
322
+ .pressy-offline-indicator.online {
323
+ background: #f0fdf4;
324
+ color: #16a34a;
325
+ border: 1px solid #bbf7d0;
326
+ }
327
+
328
+ @keyframes slideUp {
329
+ from {
330
+ opacity: 0;
331
+ transform: translateX(-50%) translateY(1rem);
332
+ }
333
+ to {
334
+ opacity: 1;
335
+ transform: translateX(-50%) translateY(0);
336
+ }
337
+ }
338
+
339
+ @media (prefers-color-scheme: dark) {
340
+ .pressy-offline-indicator.offline {
341
+ background: #450a0a;
342
+ color: #fca5a5;
343
+ border-color: #7f1d1d;
344
+ }
345
+
346
+ .pressy-offline-indicator.online {
347
+ background: #052e16;
348
+ color: #86efac;
349
+ border-color: #166534;
350
+ }
351
+ }
352
+ ` })
353
+ ] });
354
+ }
355
+
356
+ // src/Reader.tsx
357
+ import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
358
+ function Reader({
359
+ children,
360
+ bookTitle,
361
+ prevChapter,
362
+ nextChapter,
363
+ showDropCap = true,
364
+ paginationMode = "scroll",
365
+ onSaveProgress,
366
+ onRestoreProgress,
367
+ bookProgressPercent,
368
+ initialContent,
369
+ chapterMapData,
370
+ currentChapterSlug,
371
+ allChapters,
372
+ bookBasePath,
373
+ onChapterChange,
374
+ mdxComponents
375
+ }) {
376
+ if (paginationMode === "paginated") {
377
+ return /* @__PURE__ */ jsx4(
378
+ PaginatedReader,
379
+ {
380
+ bookTitle,
381
+ prevChapter,
382
+ nextChapter,
383
+ showDropCap,
384
+ onSaveProgress,
385
+ onRestoreProgress,
386
+ bookProgressPercent,
387
+ initialContent,
388
+ chapterMapData,
389
+ currentChapterSlug,
390
+ allChapters,
391
+ bookBasePath,
392
+ onChapterChange,
393
+ mdxComponents,
394
+ children
395
+ }
396
+ );
397
+ }
398
+ return /* @__PURE__ */ jsx4(
399
+ ScrollReader,
400
+ {
401
+ prevChapter,
402
+ nextChapter,
403
+ showDropCap,
404
+ onSaveProgress,
405
+ onRestoreProgress,
406
+ children
407
+ }
408
+ );
409
+ }
410
+ function ScrollReader({
411
+ children,
412
+ prevChapter,
413
+ nextChapter,
414
+ showDropCap,
415
+ onSaveProgress,
416
+ onRestoreProgress
417
+ }) {
418
+ const saveTimerRef = useRef2(null);
419
+ useEffect3(() => {
420
+ if (!onRestoreProgress) return;
421
+ onRestoreProgress().then((data) => {
422
+ if (data && data.scrollPosition > 0) {
423
+ window.scrollTo(0, data.scrollPosition);
424
+ }
425
+ });
426
+ }, []);
427
+ useEffect3(() => {
428
+ if (!onSaveProgress) return;
429
+ const handleScroll = () => {
430
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
431
+ saveTimerRef.current = setTimeout(() => {
432
+ onSaveProgress({
433
+ page: 0,
434
+ totalPages: 0,
435
+ scrollPosition: window.scrollY
436
+ });
437
+ }, 500);
438
+ };
439
+ window.addEventListener("scroll", handleScroll, { passive: true });
440
+ return () => {
441
+ window.removeEventListener("scroll", handleScroll);
442
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
443
+ };
444
+ }, [onSaveProgress]);
445
+ useEffect3(() => {
446
+ if (!onSaveProgress) return;
447
+ const handleUnload = () => {
448
+ onSaveProgress({
449
+ page: 0,
450
+ totalPages: 0,
451
+ scrollPosition: window.scrollY
452
+ });
453
+ };
454
+ window.addEventListener("beforeunload", handleUnload);
455
+ return () => window.removeEventListener("beforeunload", handleUnload);
456
+ }, [onSaveProgress]);
457
+ return /* @__PURE__ */ jsxs4("div", { class: "pressy-reader", children: [
458
+ /* @__PURE__ */ jsx4("main", { class: "pressy-reader-main", children: /* @__PURE__ */ jsx4(
459
+ "article",
460
+ {
461
+ class: `pressy-prose ${showDropCap ? "" : "no-drop-cap"}`,
462
+ "data-drop-cap": showDropCap,
463
+ children
464
+ }
465
+ ) }),
466
+ /* @__PURE__ */ jsx4(TextShare, {}),
467
+ /* @__PURE__ */ jsx4(Navigation, { prev: prevChapter, next: nextChapter }),
468
+ /* @__PURE__ */ jsx4(OfflineIndicator, {}),
469
+ /* @__PURE__ */ jsx4("style", { children: SCROLL_STYLES })
470
+ ] });
471
+ }
472
+ function ChapterDivider({ title }) {
473
+ return /* @__PURE__ */ jsx4("div", { class: "pressy-chapter-divider", children: /* @__PURE__ */ jsx4("h2", { class: "pressy-chapter-divider-title", children: title }) });
474
+ }
475
+ function PaginatedReader({
476
+ children,
477
+ bookTitle,
478
+ prevChapter,
479
+ nextChapter,
480
+ showDropCap,
481
+ onSaveProgress,
482
+ onRestoreProgress,
483
+ bookProgressPercent,
484
+ initialContent,
485
+ chapterMapData,
486
+ currentChapterSlug,
487
+ allChapters,
488
+ bookBasePath,
489
+ onChapterChange,
490
+ mdxComponents
491
+ }) {
492
+ const containerRef = useRef2(null);
493
+ const viewportRef = useRef2(null);
494
+ const articleRef = useRef2(null);
495
+ const [currentPage, setCurrentPage] = useState(0);
496
+ const [totalPages, setTotalPages] = useState(1);
497
+ const hasRestoredRef = useRef2(false);
498
+ const saveTimerRef = useRef2(null);
499
+ const [loadedChapters, setLoadedChapters] = useState([]);
500
+ const [chapterRanges, setChapterRanges] = useState([]);
501
+ const [activeChapterSlug, setActiveChapterSlug] = useState(
502
+ currentChapterSlug || ""
503
+ );
504
+ const preloadingRef = useRef2(/* @__PURE__ */ new Set());
505
+ const isMultiChapter = !!(chapterMapData && initialContent && currentChapterSlug && allChapters);
506
+ useEffect3(() => {
507
+ if (isMultiChapter && initialContent && currentChapterSlug) {
508
+ const chapterInfo = allChapters.find(
509
+ (ch) => ch.slug === currentChapterSlug
510
+ );
511
+ setLoadedChapters([
512
+ {
513
+ slug: currentChapterSlug,
514
+ title: chapterInfo?.title || currentChapterSlug,
515
+ Content: initialContent
516
+ }
517
+ ]);
518
+ setActiveChapterSlug(currentChapterSlug);
519
+ }
520
+ }, []);
521
+ const [settingsOpen, setSettingsOpen] = useState(false);
522
+ const [activeTheme, setActiveTheme] = useState(() => {
523
+ if (typeof localStorage !== "undefined") {
524
+ return localStorage.getItem("pressy-theme") || "light";
525
+ }
526
+ return "light";
527
+ });
528
+ const [fontScale, setFontScale] = useState(() => {
529
+ if (typeof localStorage !== "undefined") {
530
+ const saved = localStorage.getItem("pressy-font-size");
531
+ return saved ? parseFloat(saved) : 1;
532
+ }
533
+ return 1;
534
+ });
535
+ const applyFontScale = useCallback((scale) => {
536
+ if (scale === 1) {
537
+ document.documentElement.style.removeProperty("--font-size-base");
538
+ } else {
539
+ document.documentElement.style.setProperty(
540
+ "--font-size-base",
541
+ `calc(clamp(1rem, 0.875rem + 0.5vw, 1.25rem) * ${scale})`
542
+ );
543
+ }
544
+ }, []);
545
+ useEffect3(() => {
546
+ if (fontScale !== 1) {
547
+ applyFontScale(fontScale);
548
+ }
549
+ }, []);
550
+ const handleThemeChange = useCallback((theme) => {
551
+ setActiveTheme(theme);
552
+ localStorage.setItem("pressy-theme", theme);
553
+ const resolved = theme === "system" ? window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : theme;
554
+ document.documentElement.setAttribute("data-theme", resolved);
555
+ }, []);
556
+ const [isDragging, setIsDragging] = useState(false);
557
+ const [dragOffset, setDragOffset] = useState(0);
558
+ const [chapterHint, setChapterHint] = useState(null);
559
+ const touchStartXRef = useRef2(0);
560
+ const touchStartYRef = useRef2(0);
561
+ const lastTouchXRef = useRef2(0);
562
+ const lastTouchTimeRef = useRef2(0);
563
+ const velocityRef = useRef2(0);
564
+ const isSwipingRef = useRef2(false);
565
+ const isDraggingRef = useRef2(false);
566
+ const updateChapterRanges = useCallback(() => {
567
+ if (!isMultiChapter) return;
568
+ const article = articleRef.current;
569
+ const viewport = viewportRef.current;
570
+ if (!article || !viewport) return;
571
+ const viewportWidth = viewport.clientWidth;
572
+ if (viewportWidth === 0) return;
573
+ const sections = article.querySelectorAll(".pressy-chapter-section");
574
+ const ranges = [];
575
+ sections.forEach((section) => {
576
+ const slug = section.getAttribute("data-chapter-slug") || "";
577
+ const sectionLeft = section.offsetLeft;
578
+ const sectionWidth = section.scrollWidth;
579
+ const startPage = Math.floor(sectionLeft / viewportWidth);
580
+ const endPage = Math.max(
581
+ startPage,
582
+ Math.ceil((sectionLeft + sectionWidth) / viewportWidth) - 1
583
+ );
584
+ ranges.push({ slug, startPage, endPage });
585
+ });
586
+ setChapterRanges(ranges);
587
+ }, [isMultiChapter]);
588
+ const recalculatePages = useCallback(() => {
589
+ const article = articleRef.current;
590
+ const viewport = viewportRef.current;
591
+ if (!article || !viewport) return;
592
+ const viewportWidth = viewport.clientWidth;
593
+ if (viewportWidth === 0) return;
594
+ article.style.columnWidth = `${viewportWidth}px`;
595
+ void article.scrollWidth;
596
+ const total = Math.max(1, Math.round(article.scrollWidth / viewportWidth));
597
+ setTotalPages(total);
598
+ setCurrentPage((prev) => Math.min(prev, total - 1));
599
+ updateChapterRanges();
600
+ }, [updateChapterRanges]);
601
+ const handleFontSizeChange = useCallback(
602
+ (delta) => {
603
+ setFontScale((prev) => {
604
+ const next = Math.round(Math.max(0.8, Math.min(1.5, prev + delta)) * 10) / 10;
605
+ localStorage.setItem("pressy-font-size", String(next));
606
+ applyFontScale(next);
607
+ setTimeout(() => recalculatePages(), 100);
608
+ return next;
609
+ });
610
+ },
611
+ [applyFontScale, recalculatePages]
612
+ );
613
+ useEffect3(() => {
614
+ const viewport = viewportRef.current;
615
+ if (!viewport) return;
616
+ const initialTimer = setTimeout(recalculatePages, 50);
617
+ const observer = new ResizeObserver(() => {
618
+ recalculatePages();
619
+ });
620
+ observer.observe(viewport);
621
+ return () => {
622
+ clearTimeout(initialTimer);
623
+ observer.disconnect();
624
+ };
625
+ }, [recalculatePages]);
626
+ useEffect3(() => {
627
+ if (loadedChapters.length > 0) {
628
+ const timer = setTimeout(recalculatePages, 50);
629
+ return () => clearTimeout(timer);
630
+ }
631
+ }, [loadedChapters.length, recalculatePages]);
632
+ useEffect3(() => {
633
+ const article = articleRef.current;
634
+ if (!article) return;
635
+ const images = article.querySelectorAll("img");
636
+ if (images.length === 0) return;
637
+ const onLoad = () => recalculatePages();
638
+ images.forEach((img) => {
639
+ if (!img.complete) {
640
+ img.addEventListener("load", onLoad);
641
+ img.addEventListener("error", onLoad);
642
+ }
643
+ });
644
+ return () => {
645
+ images.forEach((img) => {
646
+ img.removeEventListener("load", onLoad);
647
+ img.removeEventListener("error", onLoad);
648
+ });
649
+ };
650
+ }, [recalculatePages, loadedChapters.length]);
651
+ useEffect3(() => {
652
+ const params = new URLSearchParams(window.location.search);
653
+ if (params.get("page") === "last" && totalPages > 1) {
654
+ setCurrentPage(totalPages - 1);
655
+ hasRestoredRef.current = true;
656
+ const url = new URL(window.location.href);
657
+ url.searchParams.delete("page");
658
+ history.replaceState(null, "", url.pathname);
659
+ }
660
+ }, [totalPages]);
661
+ useEffect3(() => {
662
+ const article = articleRef.current;
663
+ const viewport = viewportRef.current;
664
+ if (!article || !viewport) return;
665
+ const baseOffset = currentPage * viewport.clientWidth;
666
+ const totalOffset = baseOffset - dragOffset;
667
+ if (isDragging) {
668
+ article.style.transition = "none";
669
+ } else {
670
+ article.style.transition = "transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)";
671
+ }
672
+ article.style.transform = `translateX(-${totalOffset}px)`;
673
+ }, [currentPage, dragOffset, isDragging]);
674
+ useEffect3(() => {
675
+ const article = articleRef.current;
676
+ const viewport = viewportRef.current;
677
+ if (!article || !viewport) return;
678
+ const viewportRect = viewport.getBoundingClientRect();
679
+ const focusable = article.querySelectorAll(
680
+ "a[href], button, input, select, textarea, [tabindex]"
681
+ );
682
+ focusable.forEach((el) => {
683
+ const elRect = el.getBoundingClientRect();
684
+ const onCurrentPage = elRect.left >= viewportRect.left - 1 && elRect.right <= viewportRect.right + 1;
685
+ if (onCurrentPage) {
686
+ const original = el.getAttribute("data-original-tabindex");
687
+ if (original !== null) {
688
+ if (original === "") {
689
+ el.removeAttribute("tabindex");
690
+ } else {
691
+ el.setAttribute("tabindex", original);
692
+ }
693
+ el.removeAttribute("data-original-tabindex");
694
+ }
695
+ } else {
696
+ if (!el.hasAttribute("data-original-tabindex")) {
697
+ el.setAttribute(
698
+ "data-original-tabindex",
699
+ el.getAttribute("tabindex") || ""
700
+ );
701
+ }
702
+ el.setAttribute("tabindex", "-1");
703
+ }
704
+ });
705
+ }, [currentPage, totalPages]);
706
+ useEffect3(() => {
707
+ if (!isMultiChapter || !chapterMapData || chapterRanges.length === 0)
708
+ return;
709
+ const activeRange = chapterRanges.find((r) => r.slug === activeChapterSlug);
710
+ if (!activeRange) return;
711
+ const pagesFromEnd = activeRange.endPage - currentPage;
712
+ if (pagesFromEnd > 2) return;
713
+ const { chapterOrder, chapterMap } = chapterMapData;
714
+ const lastLoadedSlug = loadedChapters[loadedChapters.length - 1]?.slug;
715
+ const lastLoadedIdx = chapterOrder.indexOf(lastLoadedSlug);
716
+ if (lastLoadedIdx === -1 || lastLoadedIdx >= chapterOrder.length - 1)
717
+ return;
718
+ const nextSlug = chapterOrder[lastLoadedIdx + 1];
719
+ if (preloadingRef.current.has(nextSlug)) return;
720
+ if (loadedChapters.some((ch) => ch.slug === nextSlug)) return;
721
+ preloadingRef.current.add(nextSlug);
722
+ const loader = chapterMap[nextSlug];
723
+ if (!loader) return;
724
+ loader().then((mod) => {
725
+ const Content = mod.default;
726
+ const chapterInfo = allChapters.find((ch) => ch.slug === nextSlug);
727
+ setLoadedChapters((prev) => {
728
+ if (prev.some((ch) => ch.slug === nextSlug)) return prev;
729
+ return [
730
+ ...prev,
731
+ {
732
+ slug: nextSlug,
733
+ title: chapterInfo?.title || nextSlug,
734
+ Content
735
+ }
736
+ ];
737
+ });
738
+ }).catch(() => {
739
+ preloadingRef.current.delete(nextSlug);
740
+ });
741
+ }, [
742
+ currentPage,
743
+ activeChapterSlug,
744
+ chapterRanges,
745
+ isMultiChapter,
746
+ chapterMapData,
747
+ loadedChapters,
748
+ allChapters
749
+ ]);
750
+ useEffect3(() => {
751
+ if (!isMultiChapter || chapterRanges.length === 0 || !bookBasePath) return;
752
+ const currentRange = chapterRanges.find(
753
+ (r) => currentPage >= r.startPage && currentPage <= r.endPage
754
+ );
755
+ if (!currentRange || currentRange.slug === activeChapterSlug) return;
756
+ const prevSlug = activeChapterSlug;
757
+ setActiveChapterSlug(currentRange.slug);
758
+ const newPath = `${bookBasePath}/${currentRange.slug}`;
759
+ history.replaceState(null, "", newPath);
760
+ const chapterInfo = allChapters?.find(
761
+ (ch) => ch.slug === currentRange.slug
762
+ );
763
+ if (chapterInfo) {
764
+ document.title = document.title.replace(
765
+ /^[^|]+/,
766
+ chapterInfo.title + " "
767
+ );
768
+ }
769
+ if (onChapterChange && prevSlug) {
770
+ const prevRange = chapterRanges.find((r) => r.slug === prevSlug);
771
+ if (prevRange) {
772
+ const prevTotalPages = prevRange.endPage - prevRange.startPage + 1;
773
+ onChapterChange(prevSlug, prevTotalPages - 1, prevTotalPages);
774
+ }
775
+ }
776
+ }, [
777
+ currentPage,
778
+ chapterRanges,
779
+ activeChapterSlug,
780
+ isMultiChapter,
781
+ bookBasePath,
782
+ allChapters,
783
+ onChapterChange
784
+ ]);
785
+ useEffect3(() => {
786
+ if (!onSaveProgress || !hasRestoredRef.current) return;
787
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
788
+ saveTimerRef.current = setTimeout(() => {
789
+ if (isMultiChapter && chapterRanges.length > 0) {
790
+ const activeRange = chapterRanges.find(
791
+ (r) => r.slug === activeChapterSlug
792
+ );
793
+ if (activeRange) {
794
+ const chapterPage = currentPage - activeRange.startPage;
795
+ const chapterTotalPages = activeRange.endPage - activeRange.startPage + 1;
796
+ onSaveProgress({
797
+ page: chapterPage,
798
+ totalPages: chapterTotalPages,
799
+ scrollPosition: 0
800
+ });
801
+ }
802
+ } else {
803
+ onSaveProgress({
804
+ page: currentPage,
805
+ totalPages,
806
+ scrollPosition: 0
807
+ });
808
+ }
809
+ }, 300);
810
+ return () => {
811
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
812
+ };
813
+ }, [
814
+ currentPage,
815
+ totalPages,
816
+ onSaveProgress,
817
+ isMultiChapter,
818
+ chapterRanges,
819
+ activeChapterSlug
820
+ ]);
821
+ useEffect3(() => {
822
+ if (!onSaveProgress) return;
823
+ const handleUnload = () => {
824
+ if (isMultiChapter && chapterRanges.length > 0) {
825
+ const activeRange = chapterRanges.find(
826
+ (r) => r.slug === activeChapterSlug
827
+ );
828
+ if (activeRange) {
829
+ const chapterPage = currentPage - activeRange.startPage;
830
+ const chapterTotalPages = activeRange.endPage - activeRange.startPage + 1;
831
+ onSaveProgress({
832
+ page: chapterPage,
833
+ totalPages: chapterTotalPages,
834
+ scrollPosition: 0
835
+ });
836
+ }
837
+ } else {
838
+ onSaveProgress({
839
+ page: currentPage,
840
+ totalPages,
841
+ scrollPosition: 0
842
+ });
843
+ }
844
+ };
845
+ window.addEventListener("beforeunload", handleUnload);
846
+ return () => window.removeEventListener("beforeunload", handleUnload);
847
+ }, [
848
+ currentPage,
849
+ totalPages,
850
+ onSaveProgress,
851
+ isMultiChapter,
852
+ chapterRanges,
853
+ activeChapterSlug
854
+ ]);
855
+ const effectiveNextChapter = (() => {
856
+ if (!isMultiChapter || !chapterMapData) return nextChapter;
857
+ const { chapterOrder } = chapterMapData;
858
+ const lastLoadedSlug = loadedChapters[loadedChapters.length - 1]?.slug;
859
+ const lastLoadedIdx = chapterOrder.indexOf(lastLoadedSlug);
860
+ if (lastLoadedIdx < chapterOrder.length - 1) return void 0;
861
+ if (bookBasePath) return { slug: bookBasePath, title: "Table of Contents" };
862
+ return nextChapter;
863
+ })();
864
+ const effectivePrevChapter = (() => {
865
+ if (!isMultiChapter || !chapterMapData) return prevChapter;
866
+ const { chapterOrder } = chapterMapData;
867
+ const firstLoadedSlug = loadedChapters[0]?.slug;
868
+ const firstLoadedIdx = chapterOrder.indexOf(firstLoadedSlug);
869
+ if (firstLoadedIdx <= 0) return prevChapter;
870
+ const prevSlug = chapterOrder[firstLoadedIdx - 1];
871
+ const prevInfo = allChapters?.find((ch) => ch.slug === prevSlug);
872
+ return {
873
+ slug: `${bookBasePath}/${prevSlug}?page=last`,
874
+ title: prevInfo?.title || prevSlug
875
+ };
876
+ })();
877
+ const goToPage = useCallback(
878
+ (page) => {
879
+ const clamped = Math.max(0, Math.min(page, totalPages - 1));
880
+ setCurrentPage(clamped);
881
+ },
882
+ [totalPages]
883
+ );
884
+ const goNext = useCallback(() => {
885
+ if (currentPage >= totalPages - 1) {
886
+ if (effectiveNextChapter) {
887
+ window.location.href = effectiveNextChapter.slug;
888
+ }
889
+ return;
890
+ }
891
+ goToPage(currentPage + 1);
892
+ }, [currentPage, totalPages, effectiveNextChapter, goToPage]);
893
+ const goPrev = useCallback(() => {
894
+ if (currentPage <= 0) {
895
+ if (effectivePrevChapter) {
896
+ window.location.href = effectivePrevChapter.slug;
897
+ }
898
+ return;
899
+ }
900
+ goToPage(currentPage - 1);
901
+ }, [currentPage, effectivePrevChapter, goToPage]);
902
+ const [footerVisible, setFooterVisible] = useState(false);
903
+ const footerTimerRef = useRef2(null);
904
+ const showFooterTemporarily = useCallback(() => {
905
+ setFooterVisible(true);
906
+ if (footerTimerRef.current) clearTimeout(footerTimerRef.current);
907
+ footerTimerRef.current = setTimeout(() => setFooterVisible(false), 3e3);
908
+ }, []);
909
+ const lastMiddleTapRef = useRef2(0);
910
+ const middleTapTimerRef = useRef2(null);
911
+ const toggleFullscreen = useCallback(() => {
912
+ if (document.fullscreenElement) {
913
+ document.exitFullscreen();
914
+ } else {
915
+ document.documentElement.requestFullscreen().catch(() => {
916
+ });
917
+ }
918
+ }, []);
919
+ const handleContainerClick = useCallback(
920
+ (e) => {
921
+ const target = e.target;
922
+ if (target.closest(".pressy-settings-panel, .pressy-settings-toggle"))
923
+ return;
924
+ if (target.closest('a, button, input, select, textarea, [role="button"]'))
925
+ return;
926
+ if (settingsOpen) {
927
+ setSettingsOpen(false);
928
+ return;
929
+ }
930
+ const container = containerRef.current;
931
+ if (!container) return;
932
+ const rect = container.getBoundingClientRect();
933
+ const x = e.clientX - rect.left;
934
+ const edgeZone = rect.width * 0.15;
935
+ if (x < edgeZone) {
936
+ goPrev();
937
+ } else if (x > rect.width - edgeZone) {
938
+ goNext();
939
+ } else {
940
+ const now = Date.now();
941
+ const timeSinceLastTap = now - lastMiddleTapRef.current;
942
+ lastMiddleTapRef.current = now;
943
+ if (timeSinceLastTap < 300) {
944
+ if (middleTapTimerRef.current)
945
+ clearTimeout(middleTapTimerRef.current);
946
+ toggleFullscreen();
947
+ } else {
948
+ if (middleTapTimerRef.current)
949
+ clearTimeout(middleTapTimerRef.current);
950
+ middleTapTimerRef.current = setTimeout(() => {
951
+ if (footerVisible) {
952
+ setFooterVisible(false);
953
+ if (footerTimerRef.current) clearTimeout(footerTimerRef.current);
954
+ } else {
955
+ showFooterTemporarily();
956
+ }
957
+ }, 300);
958
+ }
959
+ }
960
+ },
961
+ [
962
+ goNext,
963
+ goPrev,
964
+ footerVisible,
965
+ settingsOpen,
966
+ showFooterTemporarily,
967
+ toggleFullscreen
968
+ ]
969
+ );
970
+ const [hoverZone, setHoverZone] = useState(null);
971
+ const handleMouseMove = useCallback(
972
+ (e) => {
973
+ const container = containerRef.current;
974
+ if (!container) return;
975
+ const rect = container.getBoundingClientRect();
976
+ const x = e.clientX - rect.left;
977
+ const y = e.clientY - rect.top;
978
+ const third = rect.width / 3;
979
+ if (x < third) {
980
+ setHoverZone("left");
981
+ } else if (x > third * 2) {
982
+ setHoverZone("right");
983
+ } else {
984
+ setHoverZone(null);
985
+ }
986
+ if (y > rect.height * 0.75) {
987
+ setFooterVisible(true);
988
+ if (footerTimerRef.current) clearTimeout(footerTimerRef.current);
989
+ } else if (!settingsOpen) {
990
+ if (footerTimerRef.current) clearTimeout(footerTimerRef.current);
991
+ footerTimerRef.current = setTimeout(() => setFooterVisible(false), 600);
992
+ }
993
+ },
994
+ [settingsOpen]
995
+ );
996
+ const handleMouseLeave = useCallback(() => {
997
+ setHoverZone(null);
998
+ if (!settingsOpen) {
999
+ if (footerTimerRef.current) clearTimeout(footerTimerRef.current);
1000
+ footerTimerRef.current = setTimeout(() => setFooterVisible(false), 600);
1001
+ }
1002
+ }, [settingsOpen]);
1003
+ useEffect3(() => {
1004
+ const handleKeyDown = (e) => {
1005
+ const tag = e.target.tagName;
1006
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1007
+ if (e.key === "ArrowRight" || e.key === " ") {
1008
+ e.preventDefault();
1009
+ goNext();
1010
+ } else if (e.key === "ArrowLeft") {
1011
+ e.preventDefault();
1012
+ goPrev();
1013
+ } else if (e.key === "Home") {
1014
+ e.preventDefault();
1015
+ goToPage(0);
1016
+ } else if (e.key === "End") {
1017
+ e.preventDefault();
1018
+ goToPage(totalPages - 1);
1019
+ }
1020
+ };
1021
+ window.addEventListener("keydown", handleKeyDown);
1022
+ return () => window.removeEventListener("keydown", handleKeyDown);
1023
+ }, [goNext, goPrev, goToPage, totalPages]);
1024
+ useEffect3(() => {
1025
+ const container = containerRef.current;
1026
+ if (!container) return;
1027
+ const onTouchStart = (e) => {
1028
+ const touch = e.touches[0];
1029
+ touchStartXRef.current = touch.clientX;
1030
+ touchStartYRef.current = touch.clientY;
1031
+ lastTouchXRef.current = touch.clientX;
1032
+ lastTouchTimeRef.current = performance.now();
1033
+ velocityRef.current = 0;
1034
+ isSwipingRef.current = false;
1035
+ isDraggingRef.current = false;
1036
+ };
1037
+ const onTouchMove = (e) => {
1038
+ const touch = e.touches[0];
1039
+ const dx = touch.clientX - touchStartXRef.current;
1040
+ const dy = touch.clientY - touchStartYRef.current;
1041
+ if (!isSwipingRef.current) {
1042
+ if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 10) {
1043
+ isSwipingRef.current = true;
1044
+ } else if (Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > 10) {
1045
+ return;
1046
+ } else {
1047
+ return;
1048
+ }
1049
+ }
1050
+ e.preventDefault();
1051
+ const now = performance.now();
1052
+ const timeDelta = now - lastTouchTimeRef.current;
1053
+ if (timeDelta > 0) {
1054
+ const instantVelocity = (touch.clientX - lastTouchXRef.current) / timeDelta;
1055
+ velocityRef.current = 0.6 * instantVelocity + 0.4 * velocityRef.current;
1056
+ }
1057
+ lastTouchXRef.current = touch.clientX;
1058
+ lastTouchTimeRef.current = now;
1059
+ let offset = dx;
1060
+ const atStart = currentPage === 0;
1061
+ const atEnd = currentPage >= totalPages - 1;
1062
+ if (atStart && dx > 0 || atEnd && dx < 0) {
1063
+ const sign = dx > 0 ? 1 : -1;
1064
+ const absDx = Math.abs(dx);
1065
+ offset = sign * Math.sqrt(absDx) * 5;
1066
+ if (dx > 40 && atStart && effectivePrevChapter) {
1067
+ setChapterHint("prev");
1068
+ } else if (dx < -40 && atEnd && effectiveNextChapter) {
1069
+ setChapterHint("next");
1070
+ } else {
1071
+ setChapterHint(null);
1072
+ }
1073
+ } else {
1074
+ setChapterHint(null);
1075
+ }
1076
+ isDraggingRef.current = true;
1077
+ setIsDragging(true);
1078
+ setDragOffset(offset);
1079
+ };
1080
+ const onTouchEnd = (e) => {
1081
+ if (!isSwipingRef.current || !isDraggingRef.current) {
1082
+ setIsDragging(false);
1083
+ setDragOffset(0);
1084
+ setChapterHint(null);
1085
+ return;
1086
+ }
1087
+ const dx = e.changedTouches[0].clientX - touchStartXRef.current;
1088
+ const velocity = velocityRef.current;
1089
+ const distanceThreshold = 50;
1090
+ const velocityThreshold = 0.3;
1091
+ const chapterThreshold = 80;
1092
+ const atStart = currentPage === 0;
1093
+ const atEnd = currentPage >= totalPages - 1;
1094
+ if (atEnd && dx < -chapterThreshold && effectiveNextChapter) {
1095
+ setIsDragging(false);
1096
+ setDragOffset(0);
1097
+ setChapterHint(null);
1098
+ window.location.href = effectiveNextChapter.slug;
1099
+ return;
1100
+ }
1101
+ if (atStart && dx > chapterThreshold && effectivePrevChapter) {
1102
+ setIsDragging(false);
1103
+ setDragOffset(0);
1104
+ setChapterHint(null);
1105
+ window.location.href = effectivePrevChapter.slug;
1106
+ return;
1107
+ }
1108
+ if (dx < -distanceThreshold || velocity < -velocityThreshold) {
1109
+ goNext();
1110
+ } else if (dx > distanceThreshold || velocity > velocityThreshold) {
1111
+ goPrev();
1112
+ }
1113
+ isDraggingRef.current = false;
1114
+ setIsDragging(false);
1115
+ setDragOffset(0);
1116
+ setChapterHint(null);
1117
+ };
1118
+ const onTouchCancel = () => {
1119
+ isDraggingRef.current = false;
1120
+ setIsDragging(false);
1121
+ setDragOffset(0);
1122
+ setChapterHint(null);
1123
+ };
1124
+ container.addEventListener("touchstart", onTouchStart, { passive: true });
1125
+ container.addEventListener("touchmove", onTouchMove, { passive: false });
1126
+ container.addEventListener("touchend", onTouchEnd, { passive: true });
1127
+ container.addEventListener("touchcancel", onTouchCancel, { passive: true });
1128
+ return () => {
1129
+ container.removeEventListener("touchstart", onTouchStart);
1130
+ container.removeEventListener("touchmove", onTouchMove);
1131
+ container.removeEventListener("touchend", onTouchEnd);
1132
+ container.removeEventListener("touchcancel", onTouchCancel);
1133
+ };
1134
+ }, [
1135
+ currentPage,
1136
+ totalPages,
1137
+ goNext,
1138
+ goPrev,
1139
+ effectiveNextChapter,
1140
+ effectivePrevChapter
1141
+ ]);
1142
+ const chapterPageInfo = (() => {
1143
+ if (!isMultiChapter || chapterRanges.length === 0) {
1144
+ return { chapterPage: currentPage, chapterTotalPages: totalPages };
1145
+ }
1146
+ const activeRange = chapterRanges.find((r) => r.slug === activeChapterSlug);
1147
+ if (!activeRange) {
1148
+ return { chapterPage: currentPage, chapterTotalPages: totalPages };
1149
+ }
1150
+ return {
1151
+ chapterPage: currentPage - activeRange.startPage,
1152
+ chapterTotalPages: activeRange.endPage - activeRange.startPage + 1
1153
+ };
1154
+ })();
1155
+ const progressPercent = (() => {
1156
+ if (!allChapters || allChapters.length === 0) {
1157
+ return totalPages > 1 ? currentPage / (totalPages - 1) * 100 : 100;
1158
+ }
1159
+ const totalWords = allChapters.reduce(
1160
+ (sum, ch) => sum + (ch.wordCount || 0),
1161
+ 0
1162
+ );
1163
+ if (totalWords === 0) return 0;
1164
+ const firstLoadedSlug = loadedChapters[0]?.slug || activeChapterSlug;
1165
+ let wordsBeforeLoaded = 0;
1166
+ let loadedWords = 0;
1167
+ let foundFirst = false;
1168
+ for (const ch of allChapters) {
1169
+ if (ch.slug === firstLoadedSlug) foundFirst = true;
1170
+ if (!foundFirst) {
1171
+ wordsBeforeLoaded += ch.wordCount || 0;
1172
+ } else if (loadedChapters.some((lc) => lc.slug === ch.slug)) {
1173
+ loadedWords += ch.wordCount || 0;
1174
+ } else {
1175
+ break;
1176
+ }
1177
+ }
1178
+ const pageFraction = totalPages > 1 ? currentPage / (totalPages - 1) : 0;
1179
+ const wordsInto = wordsBeforeLoaded + pageFraction * loadedWords;
1180
+ return Math.min(100, wordsInto / totalWords * 100);
1181
+ })();
1182
+ const renderContent = () => {
1183
+ if (isMultiChapter && loadedChapters.length > 0) {
1184
+ return loadedChapters.map((ch) => /* @__PURE__ */ jsxs4(
1185
+ "section",
1186
+ {
1187
+ class: "pressy-chapter-section",
1188
+ "data-chapter-slug": ch.slug,
1189
+ children: [
1190
+ /* @__PURE__ */ jsx4(ChapterDivider, { title: ch.title }),
1191
+ /* @__PURE__ */ jsx4(ch.Content, { components: mdxComponents || {} })
1192
+ ]
1193
+ },
1194
+ ch.slug
1195
+ ));
1196
+ }
1197
+ return children;
1198
+ };
1199
+ return /* @__PURE__ */ jsxs4(
1200
+ "div",
1201
+ {
1202
+ class: "pressy-reader pressy-reader--paginated",
1203
+ ref: containerRef,
1204
+ onClick: handleContainerClick,
1205
+ onMouseMove: handleMouseMove,
1206
+ onMouseLeave: handleMouseLeave,
1207
+ children: [
1208
+ /* @__PURE__ */ jsx4("div", { class: "pressy-paginated-viewport", ref: viewportRef, children: /* @__PURE__ */ jsx4(
1209
+ "article",
1210
+ {
1211
+ ref: articleRef,
1212
+ class: `pressy-prose pressy-prose--paginated ${showDropCap ? "" : "no-drop-cap"}`,
1213
+ "data-drop-cap": showDropCap,
1214
+ children: renderContent()
1215
+ }
1216
+ ) }),
1217
+ /* @__PURE__ */ jsx4(
1218
+ "div",
1219
+ {
1220
+ class: `pressy-nav-arrow pressy-nav-arrow--prev ${hoverZone === "left" ? "pressy-nav-arrow--visible" : ""}`,
1221
+ children: /* @__PURE__ */ jsx4(
1222
+ "svg",
1223
+ {
1224
+ viewBox: "0 0 24 24",
1225
+ fill: "none",
1226
+ stroke: "currentColor",
1227
+ "stroke-width": "2",
1228
+ "stroke-linecap": "round",
1229
+ "stroke-linejoin": "round",
1230
+ children: /* @__PURE__ */ jsx4("polyline", { points: "15 18 9 12 15 6" })
1231
+ }
1232
+ )
1233
+ }
1234
+ ),
1235
+ /* @__PURE__ */ jsx4(
1236
+ "div",
1237
+ {
1238
+ class: `pressy-nav-arrow pressy-nav-arrow--next ${hoverZone === "right" ? "pressy-nav-arrow--visible" : ""}`,
1239
+ children: /* @__PURE__ */ jsx4(
1240
+ "svg",
1241
+ {
1242
+ viewBox: "0 0 24 24",
1243
+ fill: "none",
1244
+ stroke: "currentColor",
1245
+ "stroke-width": "2",
1246
+ "stroke-linecap": "round",
1247
+ "stroke-linejoin": "round",
1248
+ children: /* @__PURE__ */ jsx4("polyline", { points: "9 6 15 12 9 18" })
1249
+ }
1250
+ )
1251
+ }
1252
+ ),
1253
+ chapterHint === "prev" && effectivePrevChapter && /* @__PURE__ */ jsxs4(
1254
+ "div",
1255
+ {
1256
+ class: "pressy-chapter-hint pressy-chapter-hint--prev",
1257
+ "aria-live": "polite",
1258
+ children: [
1259
+ /* @__PURE__ */ jsx4("span", { class: "pressy-chapter-hint-arrow", children: "\u2190" }),
1260
+ /* @__PURE__ */ jsx4("span", { class: "pressy-chapter-hint-text", children: effectivePrevChapter.title })
1261
+ ]
1262
+ }
1263
+ ),
1264
+ chapterHint === "next" && effectiveNextChapter && /* @__PURE__ */ jsxs4(
1265
+ "div",
1266
+ {
1267
+ class: "pressy-chapter-hint pressy-chapter-hint--next",
1268
+ "aria-live": "polite",
1269
+ children: [
1270
+ /* @__PURE__ */ jsx4("span", { class: "pressy-chapter-hint-text", children: effectiveNextChapter.title }),
1271
+ /* @__PURE__ */ jsx4("span", { class: "pressy-chapter-hint-arrow", children: "\u2192" })
1272
+ ]
1273
+ }
1274
+ ),
1275
+ /* @__PURE__ */ jsxs4(
1276
+ "div",
1277
+ {
1278
+ class: `pressy-page-footer ${footerVisible || settingsOpen ? "pressy-page-footer--visible" : ""}`,
1279
+ children: [
1280
+ /* @__PURE__ */ jsx4("div", { class: "pressy-progress-bar", children: /* @__PURE__ */ jsx4(
1281
+ "div",
1282
+ {
1283
+ class: "pressy-progress-fill",
1284
+ style: { width: `${progressPercent}%` }
1285
+ }
1286
+ ) }),
1287
+ /* @__PURE__ */ jsxs4("div", { class: "pressy-footer-row", children: [
1288
+ bookTitle && /* @__PURE__ */ jsx4("span", { class: "pressy-footer-title", children: bookTitle }),
1289
+ /* @__PURE__ */ jsx4(
1290
+ "button",
1291
+ {
1292
+ class: "pressy-settings-toggle",
1293
+ onClick: (e) => {
1294
+ e.stopPropagation();
1295
+ setSettingsOpen(!settingsOpen);
1296
+ },
1297
+ "aria-label": "Settings",
1298
+ children: /* @__PURE__ */ jsxs4(
1299
+ "svg",
1300
+ {
1301
+ viewBox: "0 0 24 24",
1302
+ fill: "none",
1303
+ stroke: "currentColor",
1304
+ "stroke-width": "1.5",
1305
+ "stroke-linecap": "round",
1306
+ "stroke-linejoin": "round",
1307
+ width: "18",
1308
+ height: "18",
1309
+ children: [
1310
+ /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "3" }),
1311
+ /* @__PURE__ */ jsx4("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
1312
+ ]
1313
+ }
1314
+ )
1315
+ }
1316
+ )
1317
+ ] }),
1318
+ /* @__PURE__ */ jsxs4(
1319
+ "div",
1320
+ {
1321
+ class: `pressy-settings-panel ${settingsOpen ? "pressy-settings-panel--open" : ""}`,
1322
+ children: [
1323
+ /* @__PURE__ */ jsxs4("div", { class: "pressy-settings-section", children: [
1324
+ /* @__PURE__ */ jsx4("div", { class: "pressy-settings-label", children: "Theme" }),
1325
+ /* @__PURE__ */ jsx4("div", { class: "pressy-theme-options", children: [
1326
+ {
1327
+ id: "light",
1328
+ label: "Light",
1329
+ icon: /* @__PURE__ */ jsxs4(
1330
+ "svg",
1331
+ {
1332
+ viewBox: "0 0 24 24",
1333
+ fill: "none",
1334
+ stroke: "currentColor",
1335
+ "stroke-width": "1.5",
1336
+ width: "16",
1337
+ height: "16",
1338
+ children: [
1339
+ /* @__PURE__ */ jsx4("circle", { cx: "12", cy: "12", r: "5" }),
1340
+ /* @__PURE__ */ jsx4("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
1341
+ /* @__PURE__ */ jsx4("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
1342
+ /* @__PURE__ */ jsx4("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
1343
+ /* @__PURE__ */ jsx4("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
1344
+ /* @__PURE__ */ jsx4("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
1345
+ /* @__PURE__ */ jsx4("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
1346
+ /* @__PURE__ */ jsx4("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
1347
+ /* @__PURE__ */ jsx4("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })
1348
+ ]
1349
+ }
1350
+ )
1351
+ },
1352
+ {
1353
+ id: "dark",
1354
+ label: "Dark",
1355
+ icon: /* @__PURE__ */ jsx4(
1356
+ "svg",
1357
+ {
1358
+ viewBox: "0 0 24 24",
1359
+ fill: "none",
1360
+ stroke: "currentColor",
1361
+ "stroke-width": "1.5",
1362
+ width: "16",
1363
+ height: "16",
1364
+ children: /* @__PURE__ */ jsx4("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })
1365
+ }
1366
+ )
1367
+ },
1368
+ {
1369
+ id: "system",
1370
+ label: "System",
1371
+ icon: /* @__PURE__ */ jsxs4(
1372
+ "svg",
1373
+ {
1374
+ viewBox: "0 0 24 24",
1375
+ fill: "none",
1376
+ stroke: "currentColor",
1377
+ "stroke-width": "1.5",
1378
+ width: "16",
1379
+ height: "16",
1380
+ children: [
1381
+ /* @__PURE__ */ jsx4(
1382
+ "rect",
1383
+ {
1384
+ x: "2",
1385
+ y: "3",
1386
+ width: "20",
1387
+ height: "14",
1388
+ rx: "2",
1389
+ ry: "2"
1390
+ }
1391
+ ),
1392
+ /* @__PURE__ */ jsx4("line", { x1: "8", y1: "21", x2: "16", y2: "21" }),
1393
+ /* @__PURE__ */ jsx4("line", { x1: "12", y1: "17", x2: "12", y2: "21" })
1394
+ ]
1395
+ }
1396
+ )
1397
+ },
1398
+ {
1399
+ id: "sepia",
1400
+ label: "Sepia",
1401
+ icon: /* @__PURE__ */ jsxs4(
1402
+ "svg",
1403
+ {
1404
+ viewBox: "0 0 24 24",
1405
+ fill: "none",
1406
+ stroke: "currentColor",
1407
+ "stroke-width": "1.5",
1408
+ width: "16",
1409
+ height: "16",
1410
+ children: [
1411
+ /* @__PURE__ */ jsx4("path", { d: "M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" }),
1412
+ /* @__PURE__ */ jsx4("path", { d: "M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" })
1413
+ ]
1414
+ }
1415
+ )
1416
+ }
1417
+ ].map((t) => /* @__PURE__ */ jsxs4(
1418
+ "button",
1419
+ {
1420
+ class: `pressy-theme-btn ${activeTheme === t.id ? "pressy-theme-btn--active" : ""}`,
1421
+ onClick: (e) => {
1422
+ e.stopPropagation();
1423
+ handleThemeChange(t.id);
1424
+ },
1425
+ children: [
1426
+ t.icon,
1427
+ /* @__PURE__ */ jsx4("span", { children: t.label })
1428
+ ]
1429
+ },
1430
+ t.id
1431
+ )) })
1432
+ ] }),
1433
+ /* @__PURE__ */ jsxs4("div", { class: "pressy-settings-section", children: [
1434
+ /* @__PURE__ */ jsx4("div", { class: "pressy-settings-label", children: "Font Size" }),
1435
+ /* @__PURE__ */ jsxs4("div", { class: "pressy-font-size-controls", children: [
1436
+ /* @__PURE__ */ jsx4(
1437
+ "button",
1438
+ {
1439
+ class: "pressy-font-size-btn",
1440
+ onClick: (e) => {
1441
+ e.stopPropagation();
1442
+ handleFontSizeChange(-0.1);
1443
+ },
1444
+ disabled: fontScale <= 0.8,
1445
+ "aria-label": "Decrease font size",
1446
+ children: "A-"
1447
+ }
1448
+ ),
1449
+ /* @__PURE__ */ jsxs4("span", { class: "pressy-font-size-value", children: [
1450
+ Math.round(fontScale * 100),
1451
+ "%"
1452
+ ] }),
1453
+ /* @__PURE__ */ jsx4(
1454
+ "button",
1455
+ {
1456
+ class: "pressy-font-size-btn",
1457
+ onClick: (e) => {
1458
+ e.stopPropagation();
1459
+ handleFontSizeChange(0.1);
1460
+ },
1461
+ disabled: fontScale >= 1.5,
1462
+ "aria-label": "Increase font size",
1463
+ children: "A+"
1464
+ }
1465
+ )
1466
+ ] })
1467
+ ] })
1468
+ ]
1469
+ }
1470
+ )
1471
+ ]
1472
+ }
1473
+ ),
1474
+ /* @__PURE__ */ jsx4(TextShare, {}),
1475
+ /* @__PURE__ */ jsx4(OfflineIndicator, {}),
1476
+ /* @__PURE__ */ jsx4("style", { children: PAGINATED_STYLES })
1477
+ ]
1478
+ }
1479
+ );
1480
+ }
1481
+ var SCROLL_STYLES = `
1482
+ .pressy-reader {
1483
+ min-height: 100vh;
1484
+ display: flex;
1485
+ flex-direction: column;
1486
+ }
1487
+
1488
+ .pressy-reader-main {
1489
+ flex: 1;
1490
+ padding: 2rem 0;
1491
+ }
1492
+ `;
1493
+ var PAGINATED_STYLES = `
1494
+ html:has(.pressy-reader--paginated) body {
1495
+ margin: 0;
1496
+ }
1497
+
1498
+ .pressy-reader--paginated {
1499
+ height: 100vh;
1500
+ height: 100dvh;
1501
+ display: flex;
1502
+ flex-direction: column;
1503
+ overflow: hidden;
1504
+ position: relative;
1505
+ }
1506
+
1507
+ /* Viewport clips overflow and shows one page at a time.
1508
+ Vertical padding gives consistent top/bottom margins on every page. */
1509
+ .pressy-paginated-viewport {
1510
+ flex: 1;
1511
+ overflow: hidden;
1512
+ position: relative;
1513
+ min-height: 0;
1514
+ padding-block: 2rem;
1515
+ box-sizing: border-box;
1516
+ }
1517
+
1518
+ /* Article uses CSS multi-column layout for pagination.
1519
+ column-width is set dynamically via JS to match viewport width.
1520
+ Each column = one "page". Content overflows horizontally into new columns.
1521
+ translateX controlled by JS \u2014 transitions set dynamically during drag vs snap. */
1522
+ .pressy-prose--paginated {
1523
+ max-width: none;
1524
+ height: 100%;
1525
+ column-fill: auto;
1526
+ column-gap: 0;
1527
+ overflow: visible;
1528
+ box-sizing: border-box;
1529
+ padding: 0;
1530
+ will-change: transform;
1531
+ /* Transition is set dynamically: none during drag, ease-out on snap */
1532
+ }
1533
+
1534
+ /* Center content elements within each column/page at a readable width */
1535
+ .pressy-prose--paginated > * {
1536
+ max-width: min(65ch, calc(100% - 3rem));
1537
+ margin-left: auto;
1538
+ margin-right: auto;
1539
+ }
1540
+
1541
+ /* Preserve vertical margins from prose styles */
1542
+ .pressy-prose--paginated > h1,
1543
+ .pressy-prose--paginated > h2,
1544
+ .pressy-prose--paginated > h3,
1545
+ .pressy-prose--paginated > h4 {
1546
+ max-width: min(65ch, calc(100% - 3rem));
1547
+ margin-left: auto;
1548
+ margin-right: auto;
1549
+ }
1550
+
1551
+ /* \u2500\u2500 Navigation arrows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1552
+ /* Hover-triggered arrows on left/right edges for desktop navigation */
1553
+ .pressy-nav-arrow {
1554
+ position: absolute;
1555
+ top: 50%;
1556
+ transform: translateY(-50%);
1557
+ width: 2.5rem;
1558
+ height: 2.5rem;
1559
+ display: flex;
1560
+ align-items: center;
1561
+ justify-content: center;
1562
+ color: var(--color-text-muted, #6c757d);
1563
+ opacity: 0;
1564
+ transition: opacity 0.2s ease;
1565
+ pointer-events: none;
1566
+ z-index: 5;
1567
+ }
1568
+
1569
+ .pressy-nav-arrow svg {
1570
+ width: 1.5rem;
1571
+ height: 1.5rem;
1572
+ }
1573
+
1574
+ .pressy-nav-arrow--prev {
1575
+ left: 0.75rem;
1576
+ }
1577
+
1578
+ .pressy-nav-arrow--next {
1579
+ right: 0.75rem;
1580
+ }
1581
+
1582
+ .pressy-nav-arrow--visible {
1583
+ opacity: 0.4;
1584
+ }
1585
+
1586
+ /* Hide on touch devices \u2014 swipe handles navigation there */
1587
+ @media (hover: none) {
1588
+ .pressy-nav-arrow {
1589
+ display: none;
1590
+ }
1591
+ }
1592
+
1593
+ /* \u2500\u2500 Chapter boundary hints \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1594
+ /* Shown when overscrolling past first/last page of a chapter */
1595
+ .pressy-chapter-hint {
1596
+ position: absolute;
1597
+ top: 50%;
1598
+ transform: translateY(-50%);
1599
+ display: flex;
1600
+ align-items: center;
1601
+ gap: 0.5rem;
1602
+ padding: 0.75rem 1.25rem;
1603
+ background: var(--color-bg-subtle, #f5f5f5);
1604
+ border: 1px solid var(--color-border, #dee2e6);
1605
+ border-radius: 0.75rem;
1606
+ font-family: var(--font-heading, system-ui, -apple-system, sans-serif);
1607
+ font-size: var(--font-size-sm, 0.875rem);
1608
+ color: var(--color-text-muted, #6c757d);
1609
+ z-index: 20;
1610
+ pointer-events: none;
1611
+ animation: pressy-hint-fade-in 0.15s ease-out;
1612
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
1613
+ max-width: 60%;
1614
+ white-space: nowrap;
1615
+ overflow: hidden;
1616
+ text-overflow: ellipsis;
1617
+ }
1618
+
1619
+ .pressy-chapter-hint--prev {
1620
+ left: 1rem;
1621
+ }
1622
+
1623
+ .pressy-chapter-hint--next {
1624
+ right: 1rem;
1625
+ }
1626
+
1627
+ .pressy-chapter-hint-arrow {
1628
+ flex-shrink: 0;
1629
+ font-size: 1rem;
1630
+ opacity: 0.6;
1631
+ }
1632
+
1633
+ .pressy-chapter-hint-text {
1634
+ overflow: hidden;
1635
+ text-overflow: ellipsis;
1636
+ }
1637
+
1638
+ @keyframes pressy-hint-fade-in {
1639
+ from { opacity: 0; transform: translateY(-50%) scale(0.95); }
1640
+ to { opacity: 1; transform: translateY(-50%) scale(1); }
1641
+ }
1642
+
1643
+ /* \u2500\u2500 Page footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1644
+ .pressy-page-footer {
1645
+ position: absolute;
1646
+ bottom: 0;
1647
+ left: 0;
1648
+ right: 0;
1649
+ padding: 0.5rem 1.5rem 1rem;
1650
+ text-align: center;
1651
+ user-select: none;
1652
+ transform: translateY(100%);
1653
+ opacity: 0;
1654
+ transition: transform 0.25s ease, opacity 0.25s ease;
1655
+ z-index: 15;
1656
+ background: var(--color-bg, #ffffff);
1657
+ }
1658
+
1659
+ .pressy-page-footer--visible {
1660
+ transform: translateY(0);
1661
+ opacity: 1;
1662
+ }
1663
+
1664
+ /* Progress bar */
1665
+ .pressy-progress-bar {
1666
+ height: 3px;
1667
+ background: var(--color-border, #dee2e6);
1668
+ border-radius: 1.5px;
1669
+ overflow: hidden;
1670
+ }
1671
+
1672
+ .pressy-progress-fill {
1673
+ height: 100%;
1674
+ background: var(--color-accent, #212529);
1675
+ border-radius: 1.5px;
1676
+ transition: width 0.3s ease;
1677
+ }
1678
+
1679
+ /* \u2500\u2500 Footer row (title + gear) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1680
+ .pressy-footer-row {
1681
+ display: flex;
1682
+ align-items: center;
1683
+ justify-content: space-between;
1684
+ margin-top: 0.5rem;
1685
+ gap: 0.5rem;
1686
+ }
1687
+
1688
+ .pressy-footer-title {
1689
+ font-family: var(--font-heading, system-ui, -apple-system, sans-serif);
1690
+ font-size: 0.75rem;
1691
+ color: var(--color-text-muted, #6c757d);
1692
+ overflow: hidden;
1693
+ text-overflow: ellipsis;
1694
+ white-space: nowrap;
1695
+ min-width: 0;
1696
+ }
1697
+
1698
+ .pressy-settings-toggle {
1699
+ flex-shrink: 0;
1700
+ display: flex;
1701
+ align-items: center;
1702
+ justify-content: center;
1703
+ width: 2rem;
1704
+ height: 2rem;
1705
+ border: none;
1706
+ background: transparent;
1707
+ color: var(--color-text-muted, #6c757d);
1708
+ cursor: pointer;
1709
+ border-radius: 0.375rem;
1710
+ transition: color 0.15s, background 0.15s;
1711
+ padding: 0;
1712
+ }
1713
+
1714
+ .pressy-settings-toggle:hover {
1715
+ color: var(--color-text, #212529);
1716
+ background: var(--color-bg-subtle, #f8f9fa);
1717
+ }
1718
+
1719
+ /* \u2500\u2500 Settings panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1720
+ .pressy-settings-panel {
1721
+ overflow: hidden;
1722
+ max-height: 0;
1723
+ opacity: 0;
1724
+ transition: max-height 0.25s ease, opacity 0.2s ease;
1725
+ border-top: 0 solid var(--color-border, #dee2e6);
1726
+ }
1727
+
1728
+ .pressy-settings-panel--open {
1729
+ max-height: 300px;
1730
+ opacity: 1;
1731
+ border-top-width: 1px;
1732
+ margin-top: 0.5rem;
1733
+ padding-top: 0.75rem;
1734
+ }
1735
+
1736
+ .pressy-settings-section {
1737
+ margin-bottom: 0.75rem;
1738
+ }
1739
+
1740
+ .pressy-settings-section:last-child {
1741
+ margin-bottom: 0;
1742
+ }
1743
+
1744
+ .pressy-settings-label {
1745
+ font-family: var(--font-heading, system-ui, -apple-system, sans-serif);
1746
+ font-size: 0.6875rem;
1747
+ font-weight: 600;
1748
+ text-transform: uppercase;
1749
+ letter-spacing: 0.05em;
1750
+ color: var(--color-text-muted, #6c757d);
1751
+ margin-bottom: 0.5rem;
1752
+ text-align: left;
1753
+ }
1754
+
1755
+ .pressy-theme-options {
1756
+ display: flex;
1757
+ gap: 0.375rem;
1758
+ }
1759
+
1760
+ .pressy-theme-btn {
1761
+ flex: 1;
1762
+ display: flex;
1763
+ align-items: center;
1764
+ justify-content: center;
1765
+ gap: 0.25rem;
1766
+ padding: 0.375rem 0.5rem;
1767
+ border: 1.5px solid var(--color-border, #dee2e6);
1768
+ border-radius: 0.5rem;
1769
+ background: transparent;
1770
+ color: var(--color-text, #212529);
1771
+ font-family: var(--font-heading, system-ui, -apple-system, sans-serif);
1772
+ font-size: 0.6875rem;
1773
+ cursor: pointer;
1774
+ transition: border-color 0.15s, background 0.15s;
1775
+ }
1776
+
1777
+ .pressy-theme-btn:hover {
1778
+ background: var(--color-bg-subtle, #f8f9fa);
1779
+ }
1780
+
1781
+ .pressy-theme-btn--active {
1782
+ border-color: var(--color-accent, #212529);
1783
+ background: var(--color-bg-subtle, #f8f9fa);
1784
+ font-weight: 600;
1785
+ }
1786
+
1787
+ .pressy-font-size-controls {
1788
+ display: flex;
1789
+ align-items: center;
1790
+ justify-content: center;
1791
+ gap: 1rem;
1792
+ }
1793
+
1794
+ .pressy-font-size-btn {
1795
+ display: flex;
1796
+ align-items: center;
1797
+ justify-content: center;
1798
+ width: 2rem;
1799
+ height: 2rem;
1800
+ border: 1.5px solid var(--color-border, #dee2e6);
1801
+ border-radius: 50%;
1802
+ background: transparent;
1803
+ color: var(--color-text, #212529);
1804
+ font-family: var(--font-heading, system-ui, -apple-system, sans-serif);
1805
+ font-size: 0.75rem;
1806
+ font-weight: 600;
1807
+ cursor: pointer;
1808
+ transition: border-color 0.15s, background 0.15s;
1809
+ }
1810
+
1811
+ .pressy-font-size-btn:hover:not(:disabled) {
1812
+ background: var(--color-bg-subtle, #f8f9fa);
1813
+ border-color: var(--color-accent, #212529);
1814
+ }
1815
+
1816
+ .pressy-font-size-btn:disabled {
1817
+ opacity: 0.3;
1818
+ cursor: default;
1819
+ }
1820
+
1821
+ .pressy-font-size-value {
1822
+ font-family: var(--font-heading, system-ui, -apple-system, sans-serif);
1823
+ font-size: 0.8125rem;
1824
+ font-weight: 500;
1825
+ color: var(--color-text, #212529);
1826
+ min-width: 3ch;
1827
+ text-align: center;
1828
+ }
1829
+
1830
+ /* \u2500\u2500 Chapter sections \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1831
+ .pressy-chapter-section + .pressy-chapter-section {
1832
+ break-before: column;
1833
+ }
1834
+
1835
+ .pressy-chapter-divider {
1836
+ text-align: center;
1837
+ padding: 3rem 1.5rem;
1838
+ max-width: min(65ch, calc(100% - 3rem));
1839
+ margin: 0 auto;
1840
+ }
1841
+
1842
+ .pressy-chapter-divider-title {
1843
+ font-size: 1.5rem;
1844
+ font-family: var(--font-heading, system-ui, -apple-system, sans-serif);
1845
+ margin: 0;
1846
+ }
1847
+
1848
+ /* \u2500\u2500 Reduced motion preference \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1849
+ Disable page turn animations for users who prefer reduced motion. */
1850
+ @media (prefers-reduced-motion: reduce) {
1851
+ .pressy-prose--paginated {
1852
+ transition: none !important;
1853
+ }
1854
+ .pressy-progress-fill {
1855
+ transition: none !important;
1856
+ }
1857
+ }
1858
+ `;
1859
+
1860
+ // src/TableOfContents.tsx
1861
+ import { jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
1862
+ function TableOfContents({ items, onNavigate, activeSlug }) {
1863
+ const handleClick = (slug) => {
1864
+ const element = document.getElementById(slug);
1865
+ if (element) {
1866
+ element.scrollIntoView({ behavior: "smooth" });
1867
+ }
1868
+ onNavigate?.();
1869
+ };
1870
+ return /* @__PURE__ */ jsxs5("nav", { class: "pressy-toc", "aria-label": "Table of contents", children: [
1871
+ /* @__PURE__ */ jsx5("h2", { class: "pressy-toc-title", children: "Contents" }),
1872
+ /* @__PURE__ */ jsx5("ul", { class: "pressy-toc-list", children: items.map((item) => /* @__PURE__ */ jsx5(
1873
+ "li",
1874
+ {
1875
+ class: `pressy-toc-item pressy-toc-level-${item.level}`,
1876
+ "data-active": activeSlug === item.slug,
1877
+ children: /* @__PURE__ */ jsx5(
1878
+ "a",
1879
+ {
1880
+ href: `#${item.slug}`,
1881
+ onClick: (e) => {
1882
+ e.preventDefault();
1883
+ handleClick(item.slug);
1884
+ },
1885
+ class: "pressy-toc-link",
1886
+ children: item.text
1887
+ }
1888
+ )
1889
+ },
1890
+ item.slug
1891
+ )) }),
1892
+ /* @__PURE__ */ jsx5("style", { children: `
1893
+ .pressy-toc {
1894
+ font-size: var(--font-size-sm);
1895
+ }
1896
+
1897
+ .pressy-toc-title {
1898
+ font-size: var(--font-size-base);
1899
+ font-weight: 600;
1900
+ margin-bottom: 1rem;
1901
+ color: var(--color-heading);
1902
+ }
1903
+
1904
+ .pressy-toc-list {
1905
+ list-style: none;
1906
+ padding: 0;
1907
+ margin: 0;
1908
+ }
1909
+
1910
+ .pressy-toc-item {
1911
+ margin-bottom: 0.5rem;
1912
+ }
1913
+
1914
+ .pressy-toc-level-1 {
1915
+ padding-left: 0;
1916
+ }
1917
+
1918
+ .pressy-toc-level-2 {
1919
+ padding-left: 1rem;
1920
+ }
1921
+
1922
+ .pressy-toc-level-3 {
1923
+ padding-left: 2rem;
1924
+ }
1925
+
1926
+ .pressy-toc-level-4 {
1927
+ padding-left: 3rem;
1928
+ }
1929
+
1930
+ .pressy-toc-link {
1931
+ color: var(--color-text-muted);
1932
+ text-decoration: none;
1933
+ transition: color 0.15s;
1934
+ display: block;
1935
+ padding: 0.25rem 0;
1936
+ }
1937
+
1938
+ .pressy-toc-link:hover {
1939
+ color: var(--color-text);
1940
+ }
1941
+
1942
+ .pressy-toc-item[data-active="true"] .pressy-toc-link {
1943
+ color: var(--color-accent);
1944
+ font-weight: 500;
1945
+ }
1946
+ ` })
1947
+ ] });
1948
+ }
1949
+
1950
+ // src/Paywall.tsx
1951
+ import { useSignal as useSignal3 } from "@preact/signals";
1952
+ import { jsx as jsx6, jsxs as jsxs6 } from "preact/jsx-runtime";
1953
+ function Paywall({
1954
+ bookSlug,
1955
+ bookTitle,
1956
+ previewChapters,
1957
+ currentChapter,
1958
+ shopifyProductId,
1959
+ mode = "email",
1960
+ onUnlock
1961
+ }) {
1962
+ const email = useSignal3("");
1963
+ const isLoading = useSignal3(false);
1964
+ const error = useSignal3("");
1965
+ const isUnlocked = useSignal3(false);
1966
+ if (typeof window !== "undefined") {
1967
+ const unlocked = localStorage.getItem(`pressy-unlocked-${bookSlug}`);
1968
+ if (unlocked) {
1969
+ isUnlocked.value = true;
1970
+ }
1971
+ }
1972
+ if (isUnlocked.value) {
1973
+ return null;
1974
+ }
1975
+ if (currentChapter <= previewChapters) {
1976
+ return null;
1977
+ }
1978
+ const handleEmailSubmit = async (e) => {
1979
+ e.preventDefault();
1980
+ if (!email.value || !email.value.includes("@")) {
1981
+ error.value = "Please enter a valid email address";
1982
+ return;
1983
+ }
1984
+ isLoading.value = true;
1985
+ error.value = "";
1986
+ try {
1987
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1988
+ localStorage.setItem(`pressy-unlocked-${bookSlug}`, "email");
1989
+ localStorage.setItem(`pressy-email-${bookSlug}`, email.value);
1990
+ isUnlocked.value = true;
1991
+ onUnlock?.();
1992
+ } catch (err) {
1993
+ error.value = "Something went wrong. Please try again.";
1994
+ } finally {
1995
+ isLoading.value = false;
1996
+ }
1997
+ };
1998
+ const handleShopifyPurchase = async () => {
1999
+ if (!shopifyProductId) {
2000
+ error.value = "Purchase not available";
2001
+ return;
2002
+ }
2003
+ isLoading.value = true;
2004
+ try {
2005
+ const { createCheckout } = await import("@pressy-pub/shopify");
2006
+ const checkoutUrl = await createCheckout(shopifyProductId);
2007
+ window.location.href = checkoutUrl;
2008
+ } catch (err) {
2009
+ error.value = "Failed to create checkout. Please try again.";
2010
+ isLoading.value = false;
2011
+ }
2012
+ };
2013
+ return /* @__PURE__ */ jsxs6("div", { class: "pressy-paywall", children: [
2014
+ /* @__PURE__ */ jsxs6("div", { class: "pressy-paywall-content", children: [
2015
+ /* @__PURE__ */ jsx6("div", { class: "pressy-paywall-icon", children: /* @__PURE__ */ jsx6("svg", { width: "48", height: "48", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx6("path", { d: "M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" }) }) }),
2016
+ /* @__PURE__ */ jsx6("h2", { class: "pressy-paywall-title", children: "Continue Reading" }),
2017
+ /* @__PURE__ */ jsxs6("p", { class: "pressy-paywall-description", children: [
2018
+ "You've enjoyed the first ",
2019
+ previewChapters,
2020
+ ' chapters of "',
2021
+ bookTitle,
2022
+ '".',
2023
+ mode === "shopify" ? " Purchase the full book to continue reading." : " Enter your email to unlock the full book."
2024
+ ] }),
2025
+ mode === "email" ? /* @__PURE__ */ jsxs6("form", { class: "pressy-paywall-form", onSubmit: handleEmailSubmit, children: [
2026
+ /* @__PURE__ */ jsx6(
2027
+ "input",
2028
+ {
2029
+ type: "email",
2030
+ placeholder: "Enter your email",
2031
+ value: email.value,
2032
+ onInput: (e) => email.value = e.target.value,
2033
+ class: "pressy-paywall-input",
2034
+ disabled: isLoading.value
2035
+ }
2036
+ ),
2037
+ /* @__PURE__ */ jsx6("button", { type: "submit", class: "pressy-paywall-button", disabled: isLoading.value, children: isLoading.value ? "Unlocking..." : "Unlock Full Book" })
2038
+ ] }) : /* @__PURE__ */ jsx6(
2039
+ "button",
2040
+ {
2041
+ class: "pressy-paywall-button pressy-paywall-purchase",
2042
+ onClick: handleShopifyPurchase,
2043
+ disabled: isLoading.value,
2044
+ children: isLoading.value ? "Processing..." : "Purchase Now"
2045
+ }
2046
+ ),
2047
+ error.value && /* @__PURE__ */ jsx6("p", { class: "pressy-paywall-error", children: error.value }),
2048
+ /* @__PURE__ */ jsx6("p", { class: "pressy-paywall-note", children: mode === "email" ? "We'll never share your email with anyone." : "Secure checkout powered by Shopify." })
2049
+ ] }),
2050
+ /* @__PURE__ */ jsx6("style", { children: `
2051
+ .pressy-paywall {
2052
+ position: fixed;
2053
+ inset: 0;
2054
+ background: rgba(0, 0, 0, 0.8);
2055
+ display: flex;
2056
+ align-items: center;
2057
+ justify-content: center;
2058
+ z-index: 1000;
2059
+ padding: 1.5rem;
2060
+ }
2061
+
2062
+ .pressy-paywall-content {
2063
+ background: var(--color-bg);
2064
+ border-radius: 1rem;
2065
+ padding: 2.5rem;
2066
+ max-width: 400px;
2067
+ width: 100%;
2068
+ text-align: center;
2069
+ }
2070
+
2071
+ .pressy-paywall-icon {
2072
+ color: var(--color-accent);
2073
+ margin-bottom: 1.5rem;
2074
+ }
2075
+
2076
+ .pressy-paywall-title {
2077
+ font-size: var(--font-size-xl);
2078
+ font-weight: 600;
2079
+ margin-bottom: 0.75rem;
2080
+ color: var(--color-heading);
2081
+ }
2082
+
2083
+ .pressy-paywall-description {
2084
+ color: var(--color-text-muted);
2085
+ margin-bottom: 1.5rem;
2086
+ line-height: 1.5;
2087
+ }
2088
+
2089
+ .pressy-paywall-form {
2090
+ display: flex;
2091
+ flex-direction: column;
2092
+ gap: 0.75rem;
2093
+ }
2094
+
2095
+ .pressy-paywall-input {
2096
+ width: 100%;
2097
+ padding: 0.875rem 1rem;
2098
+ border: 1px solid var(--color-border);
2099
+ border-radius: 0.5rem;
2100
+ font-size: var(--font-size-base);
2101
+ background: var(--color-bg);
2102
+ color: var(--color-text);
2103
+ }
2104
+
2105
+ .pressy-paywall-input:focus {
2106
+ outline: none;
2107
+ border-color: var(--color-accent);
2108
+ box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
2109
+ }
2110
+
2111
+ .pressy-paywall-button {
2112
+ width: 100%;
2113
+ padding: 0.875rem 1rem;
2114
+ border: none;
2115
+ border-radius: 0.5rem;
2116
+ font-size: var(--font-size-base);
2117
+ font-weight: 500;
2118
+ background: var(--color-accent);
2119
+ color: white;
2120
+ cursor: pointer;
2121
+ transition: opacity 0.15s;
2122
+ }
2123
+
2124
+ .pressy-paywall-button:hover:not(:disabled) {
2125
+ opacity: 0.9;
2126
+ }
2127
+
2128
+ .pressy-paywall-button:disabled {
2129
+ opacity: 0.6;
2130
+ cursor: not-allowed;
2131
+ }
2132
+
2133
+ .pressy-paywall-error {
2134
+ color: #dc2626;
2135
+ font-size: var(--font-size-sm);
2136
+ margin-top: 0.75rem;
2137
+ }
2138
+
2139
+ .pressy-paywall-note {
2140
+ font-size: var(--font-size-sm);
2141
+ color: var(--color-text-muted);
2142
+ margin-top: 1.5rem;
2143
+ }
2144
+ ` })
2145
+ ] });
2146
+ }
2147
+
2148
+ // src/ThemeSwitcher.tsx
2149
+ import { useSignal as useSignal4 } from "@preact/signals";
2150
+ import { jsx as jsx7, jsxs as jsxs7 } from "preact/jsx-runtime";
2151
+ var themes = ["light", "dark", "sepia"];
2152
+ function ThemeSwitcher() {
2153
+ const isOpen = useSignal4(false);
2154
+ const currentTheme = useSignal4(
2155
+ (typeof localStorage !== "undefined" ? localStorage.getItem("pressy-theme") : "light") || "light"
2156
+ );
2157
+ const setTheme = (theme) => {
2158
+ currentTheme.value = theme;
2159
+ document.documentElement.setAttribute("data-theme", theme);
2160
+ localStorage.setItem("pressy-theme", theme);
2161
+ isOpen.value = false;
2162
+ };
2163
+ const themeIcons = {
2164
+ light: "\u2600\uFE0F",
2165
+ dark: "\u{1F319}",
2166
+ sepia: "\u{1F4DC}"
2167
+ };
2168
+ const themeLabels = {
2169
+ light: "Light",
2170
+ dark: "Dark",
2171
+ sepia: "Sepia"
2172
+ };
2173
+ return /* @__PURE__ */ jsxs7("div", { class: "pressy-theme-switcher", children: [
2174
+ /* @__PURE__ */ jsx7(
2175
+ "button",
2176
+ {
2177
+ class: "pressy-theme-toggle",
2178
+ onClick: () => isOpen.value = !isOpen.value,
2179
+ "aria-label": "Change theme",
2180
+ "aria-expanded": isOpen.value,
2181
+ children: /* @__PURE__ */ jsx7("span", { class: "pressy-theme-icon", children: themeIcons[currentTheme.value] })
2182
+ }
2183
+ ),
2184
+ isOpen.value && /* @__PURE__ */ jsx7("div", { class: "pressy-theme-menu", role: "menu", children: themes.map((theme) => /* @__PURE__ */ jsxs7(
2185
+ "button",
2186
+ {
2187
+ class: "pressy-theme-option",
2188
+ onClick: () => setTheme(theme),
2189
+ role: "menuitem",
2190
+ "data-active": currentTheme.value === theme,
2191
+ children: [
2192
+ /* @__PURE__ */ jsx7("span", { class: "pressy-theme-icon", children: themeIcons[theme] }),
2193
+ /* @__PURE__ */ jsx7("span", { class: "pressy-theme-label", children: themeLabels[theme] })
2194
+ ]
2195
+ },
2196
+ theme
2197
+ )) }),
2198
+ /* @__PURE__ */ jsx7("style", { children: `
2199
+ .pressy-theme-switcher {
2200
+ position: relative;
2201
+ }
2202
+
2203
+ .pressy-theme-toggle {
2204
+ display: flex;
2205
+ align-items: center;
2206
+ justify-content: center;
2207
+ width: 2.5rem;
2208
+ height: 2.5rem;
2209
+ border: none;
2210
+ background: transparent;
2211
+ cursor: pointer;
2212
+ border-radius: 0.5rem;
2213
+ transition: background 0.15s;
2214
+ font-size: 1.25rem;
2215
+ }
2216
+
2217
+ .pressy-theme-toggle:hover {
2218
+ background: var(--color-bg-muted);
2219
+ }
2220
+
2221
+ .pressy-theme-menu {
2222
+ position: absolute;
2223
+ top: 100%;
2224
+ right: 0;
2225
+ margin-top: 0.5rem;
2226
+ background: var(--color-bg);
2227
+ border: 1px solid var(--color-border);
2228
+ border-radius: 0.5rem;
2229
+ box-shadow: var(--shadow-md);
2230
+ overflow: hidden;
2231
+ z-index: 100;
2232
+ }
2233
+
2234
+ .pressy-theme-option {
2235
+ display: flex;
2236
+ align-items: center;
2237
+ gap: 0.5rem;
2238
+ width: 100%;
2239
+ padding: 0.75rem 1rem;
2240
+ border: none;
2241
+ background: transparent;
2242
+ cursor: pointer;
2243
+ text-align: left;
2244
+ color: var(--color-text);
2245
+ transition: background 0.15s;
2246
+ }
2247
+
2248
+ .pressy-theme-option:hover {
2249
+ background: var(--color-bg-subtle);
2250
+ }
2251
+
2252
+ .pressy-theme-option[data-active="true"] {
2253
+ background: var(--color-bg-muted);
2254
+ font-weight: 500;
2255
+ }
2256
+
2257
+ .pressy-theme-label {
2258
+ font-size: var(--font-size-sm);
2259
+ }
2260
+ ` })
2261
+ ] });
2262
+ }
2263
+
2264
+ // src/DownloadBook.tsx
2265
+ import { useSignal as useSignal5 } from "@preact/signals";
2266
+ import { useEffect as useEffect4 } from "preact/hooks";
2267
+ import { jsx as jsx8, jsxs as jsxs8 } from "preact/jsx-runtime";
2268
+ var CACHED_BOOKS_KEY = "pressy-cached-books";
2269
+ function readCachedBooks() {
2270
+ try {
2271
+ const stored = localStorage.getItem(CACHED_BOOKS_KEY);
2272
+ if (stored) return new Set(JSON.parse(stored));
2273
+ } catch {
2274
+ }
2275
+ return /* @__PURE__ */ new Set();
2276
+ }
2277
+ function DownloadBook({
2278
+ bookSlug,
2279
+ chapterUrls,
2280
+ cachedBooks,
2281
+ cacheProgress,
2282
+ onDownload,
2283
+ onRemove
2284
+ }) {
2285
+ const isCached = useSignal5(readCachedBooks().has(bookSlug) || cachedBooks.value.has(bookSlug));
2286
+ const isDownloading = useSignal5(cacheProgress.value?.bookSlug === bookSlug);
2287
+ const progress = useSignal5(null);
2288
+ useEffect4(() => {
2289
+ const check = () => {
2290
+ isCached.value = readCachedBooks().has(bookSlug) || cachedBooks.value.has(bookSlug);
2291
+ isDownloading.value = cacheProgress.value?.bookSlug === bookSlug;
2292
+ const p = cacheProgress.value;
2293
+ progress.value = p && p.bookSlug === bookSlug ? Math.round(p.current / p.total * 100) : null;
2294
+ };
2295
+ const interval = setInterval(check, 500);
2296
+ check();
2297
+ return () => clearInterval(interval);
2298
+ }, [bookSlug]);
2299
+ if (isCached.value) {
2300
+ return /* @__PURE__ */ jsxs8("div", { class: "pressy-download-book", children: [
2301
+ /* @__PURE__ */ jsxs8("div", { class: "pressy-download-status", children: [
2302
+ /* @__PURE__ */ jsx8("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx8("path", { d: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" }) }),
2303
+ /* @__PURE__ */ jsx8("span", { children: "Available offline" })
2304
+ ] }),
2305
+ /* @__PURE__ */ jsx8(
2306
+ "button",
2307
+ {
2308
+ class: "pressy-download-remove",
2309
+ onClick: () => onRemove(bookSlug),
2310
+ "aria-label": "Remove offline copy",
2311
+ children: "Remove"
2312
+ }
2313
+ ),
2314
+ /* @__PURE__ */ jsx8("style", { children: STYLES })
2315
+ ] });
2316
+ }
2317
+ if (isDownloading.value) {
2318
+ return /* @__PURE__ */ jsxs8("div", { class: "pressy-download-book", children: [
2319
+ /* @__PURE__ */ jsxs8("div", { class: "pressy-download-progress", children: [
2320
+ /* @__PURE__ */ jsx8("div", { class: "pressy-download-bar", children: /* @__PURE__ */ jsx8(
2321
+ "div",
2322
+ {
2323
+ class: "pressy-download-bar-fill",
2324
+ style: { width: `${progress.value || 0}%` }
2325
+ }
2326
+ ) }),
2327
+ /* @__PURE__ */ jsxs8("span", { class: "pressy-download-percent", children: [
2328
+ progress.value || 0,
2329
+ "%"
2330
+ ] })
2331
+ ] }),
2332
+ /* @__PURE__ */ jsx8("style", { children: STYLES })
2333
+ ] });
2334
+ }
2335
+ return /* @__PURE__ */ jsxs8("div", { class: "pressy-download-book", children: [
2336
+ /* @__PURE__ */ jsxs8(
2337
+ "button",
2338
+ {
2339
+ class: "pressy-download-btn",
2340
+ onClick: () => onDownload(bookSlug, chapterUrls),
2341
+ "aria-label": "Download for offline reading",
2342
+ children: [
2343
+ /* @__PURE__ */ jsx8("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx8("path", { d: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" }) }),
2344
+ /* @__PURE__ */ jsx8("span", { children: "Download for offline" })
2345
+ ]
2346
+ }
2347
+ ),
2348
+ /* @__PURE__ */ jsx8("style", { children: STYLES })
2349
+ ] });
2350
+ }
2351
+ var STYLES = `
2352
+ .pressy-download-book {
2353
+ display: flex;
2354
+ align-items: center;
2355
+ gap: 0.75rem;
2356
+ padding: 0.75rem 0;
2357
+ }
2358
+
2359
+ .pressy-download-btn {
2360
+ display: inline-flex;
2361
+ align-items: center;
2362
+ gap: 0.5rem;
2363
+ padding: 0.5rem 1rem;
2364
+ font-family: inherit;
2365
+ font-size: var(--font-size-sm, 0.875rem);
2366
+ color: var(--color-text, #1a1a1a);
2367
+ background: var(--color-bg-muted, #f5f5f5);
2368
+ border: 1px solid var(--color-border, #e5e5e5);
2369
+ border-radius: 0.5rem;
2370
+ cursor: pointer;
2371
+ transition: background 0.15s, border-color 0.15s;
2372
+ }
2373
+
2374
+ .pressy-download-btn:hover {
2375
+ background: var(--color-bg-subtle, #eee);
2376
+ border-color: var(--color-text-muted, #999);
2377
+ }
2378
+
2379
+ .pressy-download-status {
2380
+ display: inline-flex;
2381
+ align-items: center;
2382
+ gap: 0.5rem;
2383
+ font-size: var(--font-size-sm, 0.875rem);
2384
+ color: #16a34a;
2385
+ }
2386
+
2387
+ .pressy-download-remove {
2388
+ font-family: inherit;
2389
+ font-size: var(--font-size-sm, 0.875rem);
2390
+ color: var(--color-text-muted, #666);
2391
+ background: none;
2392
+ border: none;
2393
+ cursor: pointer;
2394
+ text-decoration: underline;
2395
+ padding: 0;
2396
+ }
2397
+
2398
+ .pressy-download-remove:hover {
2399
+ color: var(--color-text, #1a1a1a);
2400
+ }
2401
+
2402
+ .pressy-download-progress {
2403
+ display: flex;
2404
+ align-items: center;
2405
+ gap: 0.75rem;
2406
+ flex: 1;
2407
+ max-width: 300px;
2408
+ }
2409
+
2410
+ .pressy-download-bar {
2411
+ flex: 1;
2412
+ height: 6px;
2413
+ background: var(--color-bg-muted, #f5f5f5);
2414
+ border-radius: 3px;
2415
+ overflow: hidden;
2416
+ }
2417
+
2418
+ .pressy-download-bar-fill {
2419
+ height: 100%;
2420
+ background: var(--color-text, #1a1a1a);
2421
+ border-radius: 3px;
2422
+ transition: width 0.3s ease;
2423
+ }
2424
+
2425
+ .pressy-download-percent {
2426
+ font-size: var(--font-size-sm, 0.875rem);
2427
+ color: var(--color-text-muted, #666);
2428
+ min-width: 3ch;
2429
+ text-align: right;
2430
+ }
2431
+
2432
+ @media (prefers-color-scheme: dark) {
2433
+ .pressy-download-status {
2434
+ color: #86efac;
2435
+ }
2436
+ }
2437
+ `;
2438
+
2439
+ // src/BookProgress.tsx
2440
+ import { useState as useState2, useEffect as useEffect5 } from "preact/hooks";
2441
+ import { jsx as jsx9, jsxs as jsxs9 } from "preact/jsx-runtime";
2442
+ function BookProgress({
2443
+ chapters,
2444
+ basePath,
2445
+ bookSlug,
2446
+ loadAllProgress
2447
+ }) {
2448
+ const [progressMap, setProgressMap] = useState2(/* @__PURE__ */ new Map());
2449
+ useEffect5(() => {
2450
+ loadAllProgress().then((allProgress) => {
2451
+ const chapterSlugs = new Set(chapters.map((ch) => ch.slug));
2452
+ const map = /* @__PURE__ */ new Map();
2453
+ for (const p of allProgress) {
2454
+ if (chapterSlugs.has(p.chapterSlug)) {
2455
+ map.set(p.chapterSlug, { page: p.page, totalPages: p.totalPages });
2456
+ }
2457
+ }
2458
+ setProgressMap(map);
2459
+ });
2460
+ }, [chapters, loadAllProgress]);
2461
+ const totalWords = chapters.reduce((sum, ch) => sum + (ch.wordCount || 0), 0);
2462
+ let wordsRead = 0;
2463
+ let chaptersCompleted = 0;
2464
+ let chaptersStarted = 0;
2465
+ for (const ch of chapters) {
2466
+ const progress = progressMap.get(ch.slug);
2467
+ if (!progress) continue;
2468
+ const chapterWords = ch.wordCount || 0;
2469
+ if (progress.totalPages > 0 && progress.page >= progress.totalPages - 1) {
2470
+ wordsRead += chapterWords;
2471
+ chaptersCompleted++;
2472
+ chaptersStarted++;
2473
+ } else if (progress.page > 0 && progress.totalPages > 0) {
2474
+ wordsRead += progress.page / progress.totalPages * chapterWords;
2475
+ chaptersStarted++;
2476
+ }
2477
+ }
2478
+ const overallPercent = totalWords > 0 ? wordsRead / totalWords * 100 : 0;
2479
+ const hasAnyProgress = chaptersStarted > 0;
2480
+ return /* @__PURE__ */ jsxs9("div", { class: "pressy-book-progress-section", children: [
2481
+ hasAnyProgress && /* @__PURE__ */ jsxs9("div", { class: "pressy-overall-progress", children: [
2482
+ /* @__PURE__ */ jsx9("div", { class: "pressy-overall-progress-bar", children: /* @__PURE__ */ jsx9(
2483
+ "div",
2484
+ {
2485
+ class: "pressy-overall-progress-fill",
2486
+ style: { width: `${overallPercent}%` }
2487
+ }
2488
+ ) }),
2489
+ /* @__PURE__ */ jsxs9("div", { class: "pressy-overall-progress-text", children: [
2490
+ Math.round(overallPercent),
2491
+ "% complete",
2492
+ /* @__PURE__ */ jsxs9("span", { class: "pressy-overall-progress-detail", children: [
2493
+ chaptersCompleted,
2494
+ " of ",
2495
+ chapters.length,
2496
+ " chapters"
2497
+ ] })
2498
+ ] })
2499
+ ] }),
2500
+ /* @__PURE__ */ jsx9("nav", { class: "pressy-chapter-list", children: chapters.map((ch) => {
2501
+ const progress = progressMap.get(ch.slug);
2502
+ const isCompleted = progress && progress.totalPages > 0 && progress.page >= progress.totalPages - 1;
2503
+ const isStarted = progress && progress.page > 0 && !isCompleted;
2504
+ return /* @__PURE__ */ jsxs9(
2505
+ "a",
2506
+ {
2507
+ href: `${basePath}/books/${bookSlug}/${ch.slug}`,
2508
+ class: `pressy-chapter-link ${isCompleted ? "pressy-chapter--completed" : ""} ${isStarted ? "pressy-chapter--started" : ""}`,
2509
+ children: [
2510
+ /* @__PURE__ */ jsxs9("span", { class: "pressy-chapter-order", children: [
2511
+ ch.order,
2512
+ "."
2513
+ ] }),
2514
+ /* @__PURE__ */ jsx9("span", { class: "pressy-chapter-title", children: ch.title }),
2515
+ isCompleted && /* @__PURE__ */ jsx9("span", { class: "pressy-chapter-badge pressy-chapter-badge--done", children: "Done" }),
2516
+ isStarted && progress && /* @__PURE__ */ jsxs9("span", { class: "pressy-chapter-badge pressy-chapter-badge--reading", children: [
2517
+ "p.",
2518
+ progress.page + 1,
2519
+ "/",
2520
+ progress.totalPages
2521
+ ] })
2522
+ ]
2523
+ }
2524
+ );
2525
+ }) }),
2526
+ /* @__PURE__ */ jsx9("style", { children: BOOK_PROGRESS_STYLES })
2527
+ ] });
2528
+ }
2529
+ var BOOK_PROGRESS_STYLES = `
2530
+ .pressy-overall-progress {
2531
+ margin-bottom: 1.5rem;
2532
+ padding: 1rem;
2533
+ background: var(--color-bg-subtle, #f5f5f5);
2534
+ border-radius: 0.5rem;
2535
+ }
2536
+
2537
+ .pressy-overall-progress-bar {
2538
+ height: 4px;
2539
+ background: var(--color-border, #dee2e6);
2540
+ border-radius: 2px;
2541
+ overflow: hidden;
2542
+ margin-bottom: 0.5rem;
2543
+ }
2544
+
2545
+ .pressy-overall-progress-fill {
2546
+ height: 100%;
2547
+ background: var(--color-accent, #212529);
2548
+ border-radius: 2px;
2549
+ transition: width 0.3s ease;
2550
+ }
2551
+
2552
+ .pressy-overall-progress-text {
2553
+ font-family: var(--font-heading, system-ui, -apple-system, sans-serif);
2554
+ font-size: var(--font-size-sm, 0.875rem);
2555
+ color: var(--color-text-muted, #6c757d);
2556
+ display: flex;
2557
+ justify-content: space-between;
2558
+ align-items: center;
2559
+ }
2560
+
2561
+ .pressy-overall-progress-detail {
2562
+ opacity: 0.7;
2563
+ }
2564
+
2565
+ .pressy-chapter-link {
2566
+ display: flex;
2567
+ gap: 0.75rem;
2568
+ align-items: center;
2569
+ padding: 0.75rem 1rem;
2570
+ text-decoration: none;
2571
+ color: var(--color-text, #1a1a1a);
2572
+ border-radius: 0.5rem;
2573
+ transition: background 0.15s;
2574
+ }
2575
+
2576
+ .pressy-chapter-link:hover {
2577
+ background: var(--color-bg-subtle, #f5f5f5);
2578
+ }
2579
+
2580
+ .pressy-chapter--completed {
2581
+ opacity: 0.7;
2582
+ }
2583
+
2584
+ .pressy-chapter-title {
2585
+ flex: 1;
2586
+ }
2587
+
2588
+ .pressy-chapter-badge {
2589
+ font-family: var(--font-heading, system-ui, -apple-system, sans-serif);
2590
+ font-size: 0.75rem;
2591
+ padding: 0.15rem 0.5rem;
2592
+ border-radius: 0.25rem;
2593
+ white-space: nowrap;
2594
+ }
2595
+
2596
+ .pressy-chapter-badge--done {
2597
+ background: var(--color-accent, #212529);
2598
+ color: var(--color-bg, #fff);
2599
+ }
2600
+
2601
+ .pressy-chapter-badge--reading {
2602
+ background: var(--color-bg-subtle, #f5f5f5);
2603
+ color: var(--color-text-muted, #6c757d);
2604
+ border: 1px solid var(--color-border, #dee2e6);
2605
+ }
2606
+ `;
2607
+ export {
2608
+ BookProgress,
2609
+ DownloadBook,
2610
+ Navigation,
2611
+ OfflineIndicator,
2612
+ Paywall,
2613
+ Reader,
2614
+ TableOfContents,
2615
+ TextShare,
2616
+ ThemeSwitcher
2617
+ };
2618
+ //# sourceMappingURL=index.js.map