@aphrody/m3-reader 3.3.3
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/README.md +12 -0
- package/dist/CollectionReader.d.ts +26 -0
- package/dist/CollectionReader.d.ts.map +1 -0
- package/dist/DatabookReader.d.ts +39 -0
- package/dist/DatabookReader.d.ts.map +1 -0
- package/dist/MangaReader.d.ts +36 -0
- package/dist/MangaReader.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1478 -0
- package/dist/index.js.map +14 -0
- package/dist/reader-lifecycle.test.d.ts +2 -0
- package/dist/reader-lifecycle.test.d.ts.map +1 -0
- package/dist/use-focus-trap.d.ts +9 -0
- package/dist/use-focus-trap.d.ts.map +1 -0
- package/dist/use-reader-core.d.ts +33 -0
- package/dist/use-reader-core.d.ts.map +1 -0
- package/dragon-ball.css +60 -0
- package/package.json +59 -0
- package/src/CollectionReader.tsx +82 -0
- package/src/DatabookReader.tsx +804 -0
- package/src/MangaReader.tsx +560 -0
- package/src/index.ts +7 -0
- package/src/reader-lifecycle.test.tsx +428 -0
- package/src/use-focus-trap.ts +129 -0
- package/src/use-reader-core.ts +138 -0
- package/styles.css +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1478 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
// packages/m3-reader/src/MangaReader.tsx
|
|
3
|
+
import {
|
|
4
|
+
useCallback as useCallback2,
|
|
5
|
+
useEffect as useEffect2,
|
|
6
|
+
useId,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef as useRef2,
|
|
9
|
+
useState as useState2
|
|
10
|
+
} from "react";
|
|
11
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
12
|
+
import {
|
|
13
|
+
BookOpen,
|
|
14
|
+
ChevronLeft,
|
|
15
|
+
ChevronRight,
|
|
16
|
+
Columns2,
|
|
17
|
+
Maximize2,
|
|
18
|
+
Minimize2,
|
|
19
|
+
MoveHorizontal,
|
|
20
|
+
ScrollText,
|
|
21
|
+
Square
|
|
22
|
+
} from "lucide-react";
|
|
23
|
+
import { Swiper, SwiperSlide } from "swiper/react";
|
|
24
|
+
import { Keyboard, Mousewheel, Zoom } from "swiper/modules";
|
|
25
|
+
import"swiper/css";
|
|
26
|
+
import"swiper/css/zoom";
|
|
27
|
+
|
|
28
|
+
// packages/m3-reader/src/use-reader-core.ts
|
|
29
|
+
import {
|
|
30
|
+
useCallback,
|
|
31
|
+
useEffect,
|
|
32
|
+
useRef,
|
|
33
|
+
useState
|
|
34
|
+
} from "react";
|
|
35
|
+
"use client";
|
|
36
|
+
var SHOW_STRIP_MAX = 40;
|
|
37
|
+
function useFullscreen(containerRef) {
|
|
38
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
39
|
+
const [fullscreenError, setFullscreenError] = useState(null);
|
|
40
|
+
const mounted = useRef(false);
|
|
41
|
+
const operation = useRef(0);
|
|
42
|
+
const toggleFullscreen = useCallback(async () => {
|
|
43
|
+
const el = containerRef.current;
|
|
44
|
+
if (!el)
|
|
45
|
+
return false;
|
|
46
|
+
const doc = el.ownerDocument;
|
|
47
|
+
const current = ++operation.current;
|
|
48
|
+
if (mounted.current)
|
|
49
|
+
setFullscreenError(null);
|
|
50
|
+
try {
|
|
51
|
+
if (doc.fullscreenElement === el) {
|
|
52
|
+
if (typeof doc.exitFullscreen !== "function")
|
|
53
|
+
throw new Error("Fullscreen exit is unavailable");
|
|
54
|
+
await doc.exitFullscreen();
|
|
55
|
+
} else {
|
|
56
|
+
if (typeof el.requestFullscreen !== "function")
|
|
57
|
+
throw new Error("Fullscreen is unavailable");
|
|
58
|
+
await el.requestFullscreen();
|
|
59
|
+
}
|
|
60
|
+
if (mounted.current && current === operation.current) {
|
|
61
|
+
setIsFullscreen(Boolean(containerRef.current && doc.fullscreenElement === containerRef.current));
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
} catch (reason) {
|
|
65
|
+
if (mounted.current && current === operation.current) {
|
|
66
|
+
setFullscreenError(reason instanceof Error ? reason : new Error(String(reason)));
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}, [containerRef]);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const doc = containerRef.current?.ownerDocument;
|
|
73
|
+
if (!doc)
|
|
74
|
+
return;
|
|
75
|
+
mounted.current = true;
|
|
76
|
+
const onChange = () => setIsFullscreen(Boolean(containerRef.current && doc.fullscreenElement === containerRef.current));
|
|
77
|
+
onChange();
|
|
78
|
+
doc.addEventListener("fullscreenchange", onChange);
|
|
79
|
+
return () => {
|
|
80
|
+
mounted.current = false;
|
|
81
|
+
operation.current++;
|
|
82
|
+
doc.removeEventListener("fullscreenchange", onChange);
|
|
83
|
+
};
|
|
84
|
+
}, [containerRef]);
|
|
85
|
+
return { isFullscreen, fullscreenError, toggleFullscreen };
|
|
86
|
+
}
|
|
87
|
+
function useSwiperSync(setCurrent) {
|
|
88
|
+
const swiperRef = useRef(null);
|
|
89
|
+
const handleSwiper = useCallback((sw) => {
|
|
90
|
+
swiperRef.current = sw;
|
|
91
|
+
setCurrent(sw.realIndex ?? sw.activeIndex ?? 0);
|
|
92
|
+
}, [setCurrent]);
|
|
93
|
+
const handleSlideChange = useCallback((sw) => setCurrent(sw.realIndex ?? sw.activeIndex ?? 0), [setCurrent]);
|
|
94
|
+
return { swiperRef, handleSwiper, handleSlideChange };
|
|
95
|
+
}
|
|
96
|
+
function useVerticalScrollSync(mode, scrollParentRef, virtualizer, setCurrent) {
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (mode !== "vertical")
|
|
99
|
+
return;
|
|
100
|
+
const el = scrollParentRef.current;
|
|
101
|
+
if (!el)
|
|
102
|
+
return;
|
|
103
|
+
const onScroll = () => {
|
|
104
|
+
const mid = el.scrollTop + el.clientHeight / 2;
|
|
105
|
+
const item = virtualizer.getVirtualItems().find((vi) => vi.start <= mid && vi.end >= mid);
|
|
106
|
+
if (item)
|
|
107
|
+
setCurrent(item.index);
|
|
108
|
+
};
|
|
109
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
110
|
+
onScroll();
|
|
111
|
+
return () => el.removeEventListener("scroll", onScroll);
|
|
112
|
+
}, [mode, scrollParentRef, virtualizer, setCurrent]);
|
|
113
|
+
}
|
|
114
|
+
var virtualRowStyle = (start) => ({
|
|
115
|
+
position: "absolute",
|
|
116
|
+
top: 0,
|
|
117
|
+
left: 0,
|
|
118
|
+
width: "100%",
|
|
119
|
+
transform: `translateY(${start}px)`
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// packages/m3-reader/src/MangaReader.tsx
|
|
123
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
124
|
+
"use client";
|
|
125
|
+
var LG_BREAKPOINT = 1024;
|
|
126
|
+
function useIsLargeScreen() {
|
|
127
|
+
const [isLarge, setIsLarge] = useState2(false);
|
|
128
|
+
useEffect2(() => {
|
|
129
|
+
const mql = window.matchMedia(`(min-width: ${LG_BREAKPOINT}px)`);
|
|
130
|
+
const sync = () => setIsLarge(mql.matches);
|
|
131
|
+
sync();
|
|
132
|
+
mql.addEventListener("change", sync);
|
|
133
|
+
return () => mql.removeEventListener("change", sync);
|
|
134
|
+
}, []);
|
|
135
|
+
return isLarge;
|
|
136
|
+
}
|
|
137
|
+
function MangaReader({
|
|
138
|
+
pages,
|
|
139
|
+
title,
|
|
140
|
+
direction = "rtl",
|
|
141
|
+
prevHref = null,
|
|
142
|
+
nextHref = null,
|
|
143
|
+
prevPages = null,
|
|
144
|
+
nextPages = null,
|
|
145
|
+
initialPage = 1,
|
|
146
|
+
onPageChange,
|
|
147
|
+
apiPageHref,
|
|
148
|
+
resolveAssetUrl = (path) => path,
|
|
149
|
+
resolveMirrorUrl,
|
|
150
|
+
renderChapterLink
|
|
151
|
+
}) {
|
|
152
|
+
const containerRef = useRef2(null);
|
|
153
|
+
const [mode, setMode] = useState2("paged");
|
|
154
|
+
const [dir, setDir] = useState2(direction);
|
|
155
|
+
const [spread, setSpread] = useState2(false);
|
|
156
|
+
const [current, setCurrent] = useState2(() => Math.max(0, Math.min(pages.length - 1, initialPage - 1)));
|
|
157
|
+
const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef);
|
|
158
|
+
const { swiperRef, handleSwiper, handleSlideChange } = useSwiperSync(setCurrent);
|
|
159
|
+
const isLarge = useIsLargeScreen();
|
|
160
|
+
const useSpread = spread && isLarge && mode === "paged";
|
|
161
|
+
const activeThumbRef = useRef2(null);
|
|
162
|
+
useEffect2(() => {
|
|
163
|
+
if (mode === "paged" && activeThumbRef.current) {
|
|
164
|
+
activeThumbRef.current.scrollIntoView({
|
|
165
|
+
behavior: "smooth",
|
|
166
|
+
block: "nearest",
|
|
167
|
+
inline: "center"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}, [current, mode]);
|
|
171
|
+
const urls = useMemo(() => pages.map(resolveAssetUrl), [pages, resolveAssetUrl]);
|
|
172
|
+
const total = urls.length;
|
|
173
|
+
const baseId = useId();
|
|
174
|
+
useEffect2(() => {
|
|
175
|
+
if (urls.length === 0)
|
|
176
|
+
return;
|
|
177
|
+
const indices = [current, current - 1, current + 1, current + 2, current + 3];
|
|
178
|
+
for (const idx of indices) {
|
|
179
|
+
if (idx >= 0 && idx < urls.length) {
|
|
180
|
+
const img = new Image;
|
|
181
|
+
img.src = urls[idx];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (nextPages && nextPages.length > 0) {
|
|
185
|
+
nextPages.slice(0, 3).forEach((page) => {
|
|
186
|
+
const img = new Image;
|
|
187
|
+
img.src = resolveAssetUrl(page);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (prevPages && prevPages.length > 0) {
|
|
191
|
+
prevPages.slice(-3).forEach((page) => {
|
|
192
|
+
const img = new Image;
|
|
193
|
+
img.src = resolveAssetUrl(page);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}, [current, urls, prevPages, nextPages, resolveAssetUrl]);
|
|
197
|
+
const handleImageError = useCallback2((e) => {
|
|
198
|
+
const target = e.currentTarget;
|
|
199
|
+
if (target.dataset.fallbackApplied)
|
|
200
|
+
return;
|
|
201
|
+
target.dataset.fallbackApplied = "true";
|
|
202
|
+
try {
|
|
203
|
+
const mirror = resolveMirrorUrl?.(target.src);
|
|
204
|
+
if (mirror)
|
|
205
|
+
target.src = mirror;
|
|
206
|
+
} catch {}
|
|
207
|
+
}, [resolveMirrorUrl]);
|
|
208
|
+
const scrollParentRef = useRef2(null);
|
|
209
|
+
const virtualizer = useVirtualizer({
|
|
210
|
+
count: total,
|
|
211
|
+
getScrollElement: () => scrollParentRef.current,
|
|
212
|
+
estimateSize: () => typeof window === "undefined" ? 900 : window.innerHeight,
|
|
213
|
+
overscan: 3
|
|
214
|
+
});
|
|
215
|
+
useVerticalScrollSync(mode, scrollParentRef, virtualizer, setCurrent);
|
|
216
|
+
useEffect2(() => {
|
|
217
|
+
onPageChange?.(current + 1);
|
|
218
|
+
}, [current, onPageChange]);
|
|
219
|
+
const apiPlanche = apiPageHref?.(current + 1) ?? null;
|
|
220
|
+
const goPrevPage = useCallback2(() => {
|
|
221
|
+
if (mode === "paged")
|
|
222
|
+
swiperRef.current?.slidePrev();
|
|
223
|
+
else
|
|
224
|
+
virtualizer.scrollToIndex(Math.max(0, current - 1), { align: "start" });
|
|
225
|
+
}, [mode, current, virtualizer]);
|
|
226
|
+
const goNextPage = useCallback2(() => {
|
|
227
|
+
if (mode === "paged")
|
|
228
|
+
swiperRef.current?.slideNext();
|
|
229
|
+
else
|
|
230
|
+
virtualizer.scrollToIndex(Math.min(total - 1, current + 1), {
|
|
231
|
+
align: "start"
|
|
232
|
+
});
|
|
233
|
+
}, [mode, current, total, virtualizer]);
|
|
234
|
+
const counter = total ? `${Math.min(current + 1, total)} / ${total}` : "0 / 0";
|
|
235
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
236
|
+
ref: containerRef,
|
|
237
|
+
className: "m3-reader m3-reader-container m3-reader-scroll flex h-[85vh] min-h-[460px] w-full min-w-0 max-w-full flex-col overflow-hidden rounded-xl bg-[var(--reader-surface)] [&:fullscreen]:h-screen [&:fullscreen]:min-h-screen [&:fullscreen]:rounded-none",
|
|
238
|
+
"data-manga-reader": true,
|
|
239
|
+
"data-m3-component": "MangaReader",
|
|
240
|
+
"data-testid": "manga-reader",
|
|
241
|
+
role: "region",
|
|
242
|
+
"aria-label": title,
|
|
243
|
+
children: [
|
|
244
|
+
/* @__PURE__ */ jsxs("div", {
|
|
245
|
+
className: "m3-reader-hud z-10 flex flex-wrap items-center justify-between gap-2 px-3.5 py-2.5",
|
|
246
|
+
children: [
|
|
247
|
+
/* @__PURE__ */ jsxs("div", {
|
|
248
|
+
className: "flex items-center gap-2.5 min-w-0",
|
|
249
|
+
children: [
|
|
250
|
+
/* @__PURE__ */ jsx("span", {
|
|
251
|
+
className: "min-w-0 max-w-[32ch] md:max-w-[45ch] truncate font-sans text-[18px] font-extrabold uppercase tracking-wide text-[var(--reader-on-surface)] drop-shadow-[1px_1px_0px_#000]",
|
|
252
|
+
children: title
|
|
253
|
+
}),
|
|
254
|
+
/* @__PURE__ */ jsx("span", {
|
|
255
|
+
className: "m3-reader-badge bg-[var(--reader-accent)] text-[var(--reader-on-accent)] px-2 py-0.5 text-[18px] font-black",
|
|
256
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
257
|
+
children: counter
|
|
258
|
+
})
|
|
259
|
+
}),
|
|
260
|
+
apiPlanche && /* @__PURE__ */ jsx("a", {
|
|
261
|
+
href: apiPlanche,
|
|
262
|
+
className: "hidden shrink-0 rounded border border-white/15 px-1.5 py-0.5 font-mono text-[18px] font-bold uppercase tracking-wider text-[var(--reader-on-surface)]/50 transition-colors hover:border-[var(--reader-accent)] hover:text-[var(--reader-accent)] sm:inline-block",
|
|
263
|
+
title: "Cette planche en JSON (image + transcription)",
|
|
264
|
+
children: "API"
|
|
265
|
+
})
|
|
266
|
+
]
|
|
267
|
+
}),
|
|
268
|
+
/* @__PURE__ */ jsxs("div", {
|
|
269
|
+
className: "flex flex-wrap items-center gap-1.5 ml-auto",
|
|
270
|
+
children: [
|
|
271
|
+
/* @__PURE__ */ jsxs("button", {
|
|
272
|
+
type: "button",
|
|
273
|
+
className: mode === "paged" ? "m3-reader-btn-active" : "m3-reader-btn",
|
|
274
|
+
onClick: () => setMode("paged"),
|
|
275
|
+
"aria-pressed": mode === "paged",
|
|
276
|
+
"aria-label": "Mode de lecture paginé",
|
|
277
|
+
title: "Mode paginé",
|
|
278
|
+
children: [
|
|
279
|
+
/* @__PURE__ */ jsx(BookOpen, {
|
|
280
|
+
size: 13,
|
|
281
|
+
"aria-hidden": true
|
|
282
|
+
}),
|
|
283
|
+
" Paginé"
|
|
284
|
+
]
|
|
285
|
+
}),
|
|
286
|
+
/* @__PURE__ */ jsxs("button", {
|
|
287
|
+
type: "button",
|
|
288
|
+
className: mode === "vertical" ? "m3-reader-btn-active" : "m3-reader-btn",
|
|
289
|
+
onClick: () => setMode("vertical"),
|
|
290
|
+
"aria-pressed": mode === "vertical",
|
|
291
|
+
"aria-label": "Mode de lecture vertical (webtoon)",
|
|
292
|
+
title: "Mode vertical (webtoon)",
|
|
293
|
+
children: [
|
|
294
|
+
/* @__PURE__ */ jsx(ScrollText, {
|
|
295
|
+
size: 13,
|
|
296
|
+
"aria-hidden": true
|
|
297
|
+
}),
|
|
298
|
+
" Défilement"
|
|
299
|
+
]
|
|
300
|
+
}),
|
|
301
|
+
/* @__PURE__ */ jsxs("button", {
|
|
302
|
+
type: "button",
|
|
303
|
+
className: dir === "rtl" ? "m3-reader-btn-active" : "m3-reader-btn",
|
|
304
|
+
onClick: () => setDir((d) => d === "rtl" ? "ltr" : "rtl"),
|
|
305
|
+
"aria-label": dir === "rtl" ? "Sens de lecture manga (droite vers gauche)" : "Sens de lecture BD (gauche vers droite)",
|
|
306
|
+
title: dir === "rtl" ? "Sens manga (droite→gauche)" : "Sens BD (gauche→droite)",
|
|
307
|
+
children: [
|
|
308
|
+
/* @__PURE__ */ jsx(MoveHorizontal, {
|
|
309
|
+
size: 13,
|
|
310
|
+
"aria-hidden": true
|
|
311
|
+
}),
|
|
312
|
+
dir === "rtl" ? "RTL" : "LTR"
|
|
313
|
+
]
|
|
314
|
+
}),
|
|
315
|
+
/* @__PURE__ */ jsxs("button", {
|
|
316
|
+
type: "button",
|
|
317
|
+
className: useSpread ? "m3-reader-btn-active" : "m3-reader-btn",
|
|
318
|
+
onClick: () => setSpread((s) => !s),
|
|
319
|
+
disabled: mode !== "paged" || !isLarge,
|
|
320
|
+
"aria-pressed": useSpread,
|
|
321
|
+
"aria-label": "Affichage double-page (grand écran)",
|
|
322
|
+
title: "Double-page (grand écran)",
|
|
323
|
+
children: [
|
|
324
|
+
useSpread ? /* @__PURE__ */ jsx(Columns2, {
|
|
325
|
+
size: 13,
|
|
326
|
+
"aria-hidden": true
|
|
327
|
+
}) : /* @__PURE__ */ jsx(Square, {
|
|
328
|
+
size: 13,
|
|
329
|
+
"aria-hidden": true
|
|
330
|
+
}),
|
|
331
|
+
"Double"
|
|
332
|
+
]
|
|
333
|
+
}),
|
|
334
|
+
/* @__PURE__ */ jsx("button", {
|
|
335
|
+
type: "button",
|
|
336
|
+
className: "m3-reader-btn",
|
|
337
|
+
onClick: toggleFullscreen,
|
|
338
|
+
"aria-label": isFullscreen ? "Quitter le plein écran" : "Activer le plein écran",
|
|
339
|
+
title: isFullscreen ? "Quitter le plein écran" : "Plein écran",
|
|
340
|
+
children: isFullscreen ? /* @__PURE__ */ jsx(Minimize2, {
|
|
341
|
+
size: 13,
|
|
342
|
+
"aria-hidden": true
|
|
343
|
+
}) : /* @__PURE__ */ jsx(Maximize2, {
|
|
344
|
+
size: 13,
|
|
345
|
+
"aria-hidden": true
|
|
346
|
+
})
|
|
347
|
+
}),
|
|
348
|
+
prevHref ? renderChapterLink ? renderChapterLink({
|
|
349
|
+
href: prevHref,
|
|
350
|
+
children: /* @__PURE__ */ jsxs(Fragment, {
|
|
351
|
+
children: [
|
|
352
|
+
/* @__PURE__ */ jsx(ChevronLeft, {
|
|
353
|
+
size: 13,
|
|
354
|
+
"aria-hidden": true
|
|
355
|
+
}),
|
|
356
|
+
" Préc."
|
|
357
|
+
]
|
|
358
|
+
}),
|
|
359
|
+
className: "m3-reader-btn",
|
|
360
|
+
ariaLabel: "Chapitre précédent"
|
|
361
|
+
}) : /* @__PURE__ */ jsxs("a", {
|
|
362
|
+
href: prevHref,
|
|
363
|
+
className: "m3-reader-btn",
|
|
364
|
+
"aria-label": "Chapitre précédent",
|
|
365
|
+
title: "Chapitre précédent",
|
|
366
|
+
children: [
|
|
367
|
+
/* @__PURE__ */ jsx(ChevronLeft, {
|
|
368
|
+
size: 13,
|
|
369
|
+
"aria-hidden": true
|
|
370
|
+
}),
|
|
371
|
+
" Préc."
|
|
372
|
+
]
|
|
373
|
+
}) : /* @__PURE__ */ jsxs("span", {
|
|
374
|
+
className: "m3-reader-btn pointer-events-none opacity-40",
|
|
375
|
+
children: [
|
|
376
|
+
/* @__PURE__ */ jsx(ChevronLeft, {
|
|
377
|
+
size: 13,
|
|
378
|
+
"aria-hidden": true
|
|
379
|
+
}),
|
|
380
|
+
" Préc."
|
|
381
|
+
]
|
|
382
|
+
}),
|
|
383
|
+
nextHref ? renderChapterLink ? renderChapterLink({
|
|
384
|
+
href: nextHref,
|
|
385
|
+
children: /* @__PURE__ */ jsxs(Fragment, {
|
|
386
|
+
children: [
|
|
387
|
+
"Suiv. ",
|
|
388
|
+
/* @__PURE__ */ jsx(ChevronRight, {
|
|
389
|
+
size: 13,
|
|
390
|
+
"aria-hidden": true
|
|
391
|
+
})
|
|
392
|
+
]
|
|
393
|
+
}),
|
|
394
|
+
className: "m3-reader-btn",
|
|
395
|
+
ariaLabel: "Chapitre suivant"
|
|
396
|
+
}) : /* @__PURE__ */ jsxs("a", {
|
|
397
|
+
href: nextHref,
|
|
398
|
+
className: "m3-reader-btn",
|
|
399
|
+
"aria-label": "Chapitre suivant",
|
|
400
|
+
title: "Chapitre suivant",
|
|
401
|
+
children: [
|
|
402
|
+
"Suiv. ",
|
|
403
|
+
/* @__PURE__ */ jsx(ChevronRight, {
|
|
404
|
+
size: 13,
|
|
405
|
+
"aria-hidden": true
|
|
406
|
+
})
|
|
407
|
+
]
|
|
408
|
+
}) : /* @__PURE__ */ jsxs("span", {
|
|
409
|
+
className: "m3-reader-btn pointer-events-none opacity-40",
|
|
410
|
+
children: [
|
|
411
|
+
"Suiv. ",
|
|
412
|
+
/* @__PURE__ */ jsx(ChevronRight, {
|
|
413
|
+
size: 13,
|
|
414
|
+
"aria-hidden": true
|
|
415
|
+
})
|
|
416
|
+
]
|
|
417
|
+
})
|
|
418
|
+
]
|
|
419
|
+
})
|
|
420
|
+
]
|
|
421
|
+
}),
|
|
422
|
+
/* @__PURE__ */ jsx("div", {
|
|
423
|
+
className: "relative min-h-0 min-w-0 flex-1 overflow-hidden bg-[var(--reader-surface)]",
|
|
424
|
+
children: total === 0 ? /* @__PURE__ */ jsx("div", {
|
|
425
|
+
className: "flex h-full items-center justify-center text-[18px] text-[var(--reader-on-surface)]",
|
|
426
|
+
children: "Aucune planche disponible."
|
|
427
|
+
}) : mode === "paged" ? /* @__PURE__ */ jsxs(Fragment, {
|
|
428
|
+
children: [
|
|
429
|
+
/* @__PURE__ */ jsx(Swiper, {
|
|
430
|
+
initialSlide: current,
|
|
431
|
+
dir,
|
|
432
|
+
modules: [Keyboard, Zoom, Mousewheel],
|
|
433
|
+
className: "h-full w-full max-w-full",
|
|
434
|
+
slidesPerView: useSpread ? 2 : 1,
|
|
435
|
+
spaceBetween: useSpread ? 8 : 0,
|
|
436
|
+
keyboard: { enabled: true },
|
|
437
|
+
mousewheel: { forceToAxis: true },
|
|
438
|
+
zoom: { maxRatio: 3, toggle: true },
|
|
439
|
+
onSwiper: handleSwiper,
|
|
440
|
+
onSlideChange: handleSlideChange,
|
|
441
|
+
children: urls.map((url, i) => /* @__PURE__ */ jsx(SwiperSlide, {
|
|
442
|
+
zoom: true,
|
|
443
|
+
className: "flex items-center justify-center",
|
|
444
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
445
|
+
className: "swiper-zoom-container flex h-full w-full max-w-full items-center justify-center",
|
|
446
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
447
|
+
src: url,
|
|
448
|
+
alt: `${title} — page ${i + 1}`,
|
|
449
|
+
loading: Math.abs(i - current) <= 1 ? "eager" : "lazy",
|
|
450
|
+
draggable: false,
|
|
451
|
+
className: "max-h-full max-w-full object-contain pointer-events-none select-none",
|
|
452
|
+
onError: handleImageError
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
}, url + i))
|
|
456
|
+
}, `${dir}-${useSpread ? "spread" : "single"}`),
|
|
457
|
+
/* @__PURE__ */ jsx("button", {
|
|
458
|
+
type: "button",
|
|
459
|
+
"aria-label": "Page précédente",
|
|
460
|
+
onClick: goPrevPage,
|
|
461
|
+
className: "absolute top-1/2 left-3 z-10 -translate-y-1/2 rounded-lg border-2 border-[var(--reader-outline)] bg-[var(--reader-surface)]/85 p-2.5 text-[var(--reader-accent)] shadow-[3px_3px_0px_#000] transition-all hover:bg-[var(--reader-accent)] hover:text-[var(--reader-on-accent)] hover:scale-105 active:translate-y-[calc(-50%+1px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--reader-accent)]",
|
|
462
|
+
children: dir === "rtl" ? /* @__PURE__ */ jsx(ChevronRight, {
|
|
463
|
+
size: 22,
|
|
464
|
+
"aria-hidden": true
|
|
465
|
+
}) : /* @__PURE__ */ jsx(ChevronLeft, {
|
|
466
|
+
size: 22,
|
|
467
|
+
"aria-hidden": true
|
|
468
|
+
})
|
|
469
|
+
}),
|
|
470
|
+
/* @__PURE__ */ jsx("button", {
|
|
471
|
+
type: "button",
|
|
472
|
+
"aria-label": "Page suivante",
|
|
473
|
+
onClick: goNextPage,
|
|
474
|
+
className: "absolute top-1/2 right-3 z-10 -translate-y-1/2 rounded-lg border-2 border-[var(--reader-outline)] bg-[var(--reader-surface)]/85 p-2.5 text-[var(--reader-accent)] shadow-[3px_3px_0px_#000] transition-all hover:bg-[var(--reader-accent)] hover:text-[var(--reader-on-accent)] hover:scale-105 active:translate-y-[calc(-50%+1px)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--reader-accent)]",
|
|
475
|
+
children: dir === "rtl" ? /* @__PURE__ */ jsx(ChevronLeft, {
|
|
476
|
+
size: 22,
|
|
477
|
+
"aria-hidden": true
|
|
478
|
+
}) : /* @__PURE__ */ jsx(ChevronRight, {
|
|
479
|
+
size: 22,
|
|
480
|
+
"aria-hidden": true
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
]
|
|
484
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
485
|
+
ref: scrollParentRef,
|
|
486
|
+
tabIndex: 0,
|
|
487
|
+
className: "h-full w-full overflow-y-auto overflow-x-hidden m3-reader-scroll focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--reader-accent)]",
|
|
488
|
+
"aria-label": `${title} — lecture verticale`,
|
|
489
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
490
|
+
style: {
|
|
491
|
+
height: virtualizer.getTotalSize(),
|
|
492
|
+
width: "100%",
|
|
493
|
+
position: "relative"
|
|
494
|
+
},
|
|
495
|
+
children: virtualizer.getVirtualItems().map((vi) => /* @__PURE__ */ jsx("div", {
|
|
496
|
+
"data-index": vi.index,
|
|
497
|
+
ref: virtualizer.measureElement,
|
|
498
|
+
style: virtualRowStyle(vi.start),
|
|
499
|
+
className: "flex justify-center py-2",
|
|
500
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
501
|
+
src: urls[vi.index],
|
|
502
|
+
alt: `${title} — page ${vi.index + 1}`,
|
|
503
|
+
loading: Math.abs(vi.index - current) <= 1 ? "eager" : "lazy",
|
|
504
|
+
draggable: false,
|
|
505
|
+
className: "h-auto w-full max-w-3xl object-contain border-y md:border-2 border-[var(--reader-outline)]/40 shadow-[0_4px_12px_rgba(0,0,0,0.8)]",
|
|
506
|
+
onError: handleImageError
|
|
507
|
+
})
|
|
508
|
+
}, `${baseId}-${vi.key}`))
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
}),
|
|
512
|
+
mode === "paged" && total > 0 && total <= SHOW_STRIP_MAX && /* @__PURE__ */ jsx("div", {
|
|
513
|
+
className: "z-10 w-full min-w-0 max-w-full bg-[var(--reader-surface)] border-t-2 border-[var(--reader-outline)] px-4 py-3 flex items-center overflow-x-auto m3-reader-scroll",
|
|
514
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
515
|
+
className: "flex gap-2.5 min-w-max mx-auto px-2",
|
|
516
|
+
children: urls.map((url, idx) => /* @__PURE__ */ jsxs("button", {
|
|
517
|
+
ref: current === idx ? activeThumbRef : undefined,
|
|
518
|
+
type: "button",
|
|
519
|
+
onClick: () => {
|
|
520
|
+
setCurrent(idx);
|
|
521
|
+
swiperRef.current?.slideTo(idx);
|
|
522
|
+
},
|
|
523
|
+
className: `relative shrink-0 w-12 h-16 rounded-md overflow-hidden border-2 transition-all ${current === idx ? "border-[var(--reader-accent)] scale-105 shadow-[0_0_12px_var(--reader-accent)]" : "border-[var(--reader-outline)] shadow-[2px_2px_0px_#000] hover:border-white/50"}`,
|
|
524
|
+
children: [
|
|
525
|
+
/* @__PURE__ */ jsx("img", {
|
|
526
|
+
src: url,
|
|
527
|
+
alt: `Page ${idx + 1}`,
|
|
528
|
+
className: "w-full h-full object-cover select-none pointer-events-none",
|
|
529
|
+
loading: "lazy"
|
|
530
|
+
}),
|
|
531
|
+
/* @__PURE__ */ jsx("span", {
|
|
532
|
+
className: "absolute bottom-0 inset-x-0 bg-[var(--reader-surface)]/85 text-[18px] font-extrabold text-[var(--reader-on-surface)] text-center font-mono py-0.5 border-t border-[var(--reader-outline)]/60",
|
|
533
|
+
children: idx + 1
|
|
534
|
+
})
|
|
535
|
+
]
|
|
536
|
+
}, url + "-thumb-" + idx))
|
|
537
|
+
})
|
|
538
|
+
}),
|
|
539
|
+
mode === "paged" && total > SHOW_STRIP_MAX && /* @__PURE__ */ jsxs("form", {
|
|
540
|
+
className: "z-10 flex items-center justify-center gap-3 border-t-2 border-[var(--reader-outline)] bg-[var(--reader-surface)] px-4 py-3",
|
|
541
|
+
onSubmit: (e) => {
|
|
542
|
+
e.preventDefault();
|
|
543
|
+
const field = e.currentTarget.elements.namedItem("goto");
|
|
544
|
+
const val = Number(field?.value);
|
|
545
|
+
if (!Number.isFinite(val))
|
|
546
|
+
return;
|
|
547
|
+
const i = Math.min(Math.max(val - 1, 0), total - 1);
|
|
548
|
+
setCurrent(i);
|
|
549
|
+
swiperRef.current?.slideTo(i);
|
|
550
|
+
},
|
|
551
|
+
children: [
|
|
552
|
+
/* @__PURE__ */ jsx("label", {
|
|
553
|
+
htmlFor: `${baseId}-goto`,
|
|
554
|
+
className: "text-[18px] font-black uppercase tracking-wider text-[var(--reader-on-surface)]",
|
|
555
|
+
children: "Aller à la planche"
|
|
556
|
+
}),
|
|
557
|
+
/* @__PURE__ */ jsx("input", {
|
|
558
|
+
id: `${baseId}-goto`,
|
|
559
|
+
name: "goto",
|
|
560
|
+
type: "number",
|
|
561
|
+
min: 1,
|
|
562
|
+
max: total,
|
|
563
|
+
defaultValue: current + 1,
|
|
564
|
+
className: "w-20 rounded-md border-2 border-[var(--reader-outline)] bg-[var(--reader-surface)] px-2.5 py-1 text-center text-[18px] font-extrabold tabular-nums text-[var(--reader-accent)] shadow-[2px_2px_0px_#000] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--reader-accent)]"
|
|
565
|
+
}, current),
|
|
566
|
+
/* @__PURE__ */ jsxs("span", {
|
|
567
|
+
className: "text-[18px] font-bold tabular-nums text-[var(--reader-on-surface)]/70",
|
|
568
|
+
children: [
|
|
569
|
+
"/ ",
|
|
570
|
+
total
|
|
571
|
+
]
|
|
572
|
+
}),
|
|
573
|
+
/* @__PURE__ */ jsx("button", {
|
|
574
|
+
type: "submit",
|
|
575
|
+
className: "m3-reader-btn-active !py-1 !px-3.5",
|
|
576
|
+
children: "OK"
|
|
577
|
+
})
|
|
578
|
+
]
|
|
579
|
+
})
|
|
580
|
+
]
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
// packages/m3-reader/src/DatabookReader.tsx
|
|
584
|
+
import { useCallback as useCallback3, useEffect as useEffect4, useId as useId2, useMemo as useMemo2, useRef as useRef4, useState as useState3 } from "react";
|
|
585
|
+
import { useVirtualizer as useVirtualizer2 } from "@tanstack/react-virtual";
|
|
586
|
+
|
|
587
|
+
// packages/m3-reader/src/use-focus-trap.ts
|
|
588
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
589
|
+
"use client";
|
|
590
|
+
var FOCUSABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
591
|
+
var activeTraps = new WeakMap;
|
|
592
|
+
function isTabbable(element) {
|
|
593
|
+
if (element.tabIndex < 0 || element.matches(":disabled, input[type='hidden']"))
|
|
594
|
+
return false;
|
|
595
|
+
if (element.closest("[hidden], [inert]"))
|
|
596
|
+
return false;
|
|
597
|
+
const view = element.ownerDocument.defaultView;
|
|
598
|
+
const visibility = view?.getComputedStyle(element).visibility;
|
|
599
|
+
if (visibility === "hidden" || visibility === "collapse")
|
|
600
|
+
return false;
|
|
601
|
+
for (let ancestor = element;ancestor; ancestor = ancestor.parentElement) {
|
|
602
|
+
if (view?.getComputedStyle(ancestor).display === "none")
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
function useFocusTrap(panelRef, active, onEscape) {
|
|
608
|
+
const onEscapeRef = useRef3(onEscape);
|
|
609
|
+
onEscapeRef.current = onEscape;
|
|
610
|
+
useEffect3(() => {
|
|
611
|
+
if (!active)
|
|
612
|
+
return;
|
|
613
|
+
const panel = panelRef.current;
|
|
614
|
+
if (!panel)
|
|
615
|
+
return;
|
|
616
|
+
const doc = panel.ownerDocument;
|
|
617
|
+
let state = activeTraps.get(doc);
|
|
618
|
+
if (!state) {
|
|
619
|
+
state = { stack: [], previousOverflow: doc.body.style.overflow };
|
|
620
|
+
activeTraps.set(doc, state);
|
|
621
|
+
}
|
|
622
|
+
const lease = {
|
|
623
|
+
panel,
|
|
624
|
+
previousFocused: doc.activeElement,
|
|
625
|
+
previousTrap: state.stack.at(-1)
|
|
626
|
+
};
|
|
627
|
+
state.stack.push(lease);
|
|
628
|
+
panelRef.current?.focus();
|
|
629
|
+
const onKey = (e) => {
|
|
630
|
+
if (state.stack.at(-1) !== lease)
|
|
631
|
+
return;
|
|
632
|
+
if (e.key === "Escape") {
|
|
633
|
+
onEscapeRef.current?.();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (e.key !== "Tab")
|
|
637
|
+
return;
|
|
638
|
+
const currentPanel = panelRef.current;
|
|
639
|
+
if (!currentPanel)
|
|
640
|
+
return;
|
|
641
|
+
const items = [...currentPanel.querySelectorAll(FOCUSABLE)].filter(isTabbable);
|
|
642
|
+
if (items.length === 0) {
|
|
643
|
+
e.preventDefault();
|
|
644
|
+
currentPanel.focus();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const first = items[0];
|
|
648
|
+
const last = items[items.length - 1];
|
|
649
|
+
const current = doc.activeElement;
|
|
650
|
+
if (!current || !items.includes(current)) {
|
|
651
|
+
e.preventDefault();
|
|
652
|
+
(e.shiftKey ? last : first).focus();
|
|
653
|
+
} else if (e.shiftKey && current === first) {
|
|
654
|
+
e.preventDefault();
|
|
655
|
+
last.focus();
|
|
656
|
+
} else if (!e.shiftKey && current === last) {
|
|
657
|
+
e.preventDefault();
|
|
658
|
+
first.focus();
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
doc.addEventListener("keydown", onKey);
|
|
662
|
+
doc.body.style.overflow = "hidden";
|
|
663
|
+
return () => {
|
|
664
|
+
doc.removeEventListener("keydown", onKey);
|
|
665
|
+
const wasTop = state.stack.at(-1) === lease;
|
|
666
|
+
state.stack.splice(state.stack.indexOf(lease), 1);
|
|
667
|
+
for (const remaining of state.stack) {
|
|
668
|
+
if (remaining.previousTrap === lease) {
|
|
669
|
+
remaining.previousTrap = lease.previousTrap;
|
|
670
|
+
remaining.previousFocused = lease.previousFocused;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const top = state.stack.at(-1);
|
|
674
|
+
if (!top) {
|
|
675
|
+
doc.body.style.overflow = state.previousOverflow;
|
|
676
|
+
activeTraps.delete(doc);
|
|
677
|
+
}
|
|
678
|
+
if (wasTop) {
|
|
679
|
+
const previous = lease.previousFocused;
|
|
680
|
+
if (previous?.isConnected && (!top || top.panel.contains(previous)))
|
|
681
|
+
previous.focus();
|
|
682
|
+
else
|
|
683
|
+
top?.panel.focus();
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}, [panelRef, active]);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// packages/m3-reader/src/DatabookReader.tsx
|
|
690
|
+
import {
|
|
691
|
+
BookOpen as BookOpen2,
|
|
692
|
+
ChevronLeft as ChevronLeft2,
|
|
693
|
+
ChevronRight as ChevronRight2,
|
|
694
|
+
Maximize2 as Maximize22,
|
|
695
|
+
Minimize2 as Minimize22,
|
|
696
|
+
ScrollText as ScrollText2,
|
|
697
|
+
X,
|
|
698
|
+
ZoomIn
|
|
699
|
+
} from "lucide-react";
|
|
700
|
+
import { Swiper as Swiper2, SwiperSlide as SwiperSlide2 } from "swiper/react";
|
|
701
|
+
import { Mousewheel as Mousewheel2, Virtual, Zoom as Zoom2 } from "swiper/modules";
|
|
702
|
+
import"swiper/css";
|
|
703
|
+
import"swiper/css/zoom";
|
|
704
|
+
import"swiper/css/virtual";
|
|
705
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
706
|
+
"use client";
|
|
707
|
+
function pageNumber(p, fallback) {
|
|
708
|
+
const raw = p.number;
|
|
709
|
+
if (typeof raw === "number" && Number.isFinite(raw))
|
|
710
|
+
return Math.trunc(raw);
|
|
711
|
+
if (typeof raw === "string" && String(raw).trim() && Number.isFinite(Number(raw))) {
|
|
712
|
+
return Math.trunc(Number(raw));
|
|
713
|
+
}
|
|
714
|
+
return fallback;
|
|
715
|
+
}
|
|
716
|
+
function resolvePages(pages, resolveAssetUrl) {
|
|
717
|
+
const out = [];
|
|
718
|
+
for (let i = 0;i < pages.length; i++) {
|
|
719
|
+
const p = pages[i];
|
|
720
|
+
const img = typeof p.image === "string" && p.image.trim() ? p.image.trim() : null;
|
|
721
|
+
const text = typeof p.text === "string" && p.text.trim() ? p.text.trim() : null;
|
|
722
|
+
const aDuTexte = p.aDuTexte === true || text !== null;
|
|
723
|
+
if (!img && !aDuTexte)
|
|
724
|
+
continue;
|
|
725
|
+
out.push({
|
|
726
|
+
number: pageNumber(p, i + 1),
|
|
727
|
+
imageUrl: img ? resolveAssetUrl(img) : null,
|
|
728
|
+
text,
|
|
729
|
+
aDuTexte,
|
|
730
|
+
verifiee: p.verifiee === true
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
var FENETRE_TEXTE = 6;
|
|
736
|
+
function useTextesPlanches(loadTranscriptions, items, current) {
|
|
737
|
+
const [charges, setCharges] = useState3(() => new Map);
|
|
738
|
+
const demandes = useRef4(new Set);
|
|
739
|
+
const manquants = useMemo2(() => {
|
|
740
|
+
if (!loadTranscriptions)
|
|
741
|
+
return [];
|
|
742
|
+
const de = Math.max(0, current - FENETRE_TEXTE);
|
|
743
|
+
const a = Math.min(items.length, current + FENETRE_TEXTE + 1);
|
|
744
|
+
const out = [];
|
|
745
|
+
for (let i = de;i < a; i++) {
|
|
746
|
+
const it = items[i];
|
|
747
|
+
if (!it || !it.aDuTexte || it.text !== null)
|
|
748
|
+
continue;
|
|
749
|
+
if (charges.has(it.number) || demandes.current.has(it.number))
|
|
750
|
+
continue;
|
|
751
|
+
out.push(it.number);
|
|
752
|
+
}
|
|
753
|
+
return out;
|
|
754
|
+
}, [loadTranscriptions, items, current, charges]);
|
|
755
|
+
useEffect4(() => {
|
|
756
|
+
if (manquants.length === 0)
|
|
757
|
+
return;
|
|
758
|
+
for (const n of manquants)
|
|
759
|
+
demandes.current.add(n);
|
|
760
|
+
const stop = new AbortController;
|
|
761
|
+
loadTranscriptions(manquants, stop.signal).then((textes) => ({ textes })).then((d) => {
|
|
762
|
+
const recus = Object.entries(d.textes ?? {});
|
|
763
|
+
if (recus.length === 0)
|
|
764
|
+
return;
|
|
765
|
+
setCharges((prec) => {
|
|
766
|
+
const suiv = new Map(prec);
|
|
767
|
+
for (const [n, t] of recus)
|
|
768
|
+
suiv.set(Number(n), t);
|
|
769
|
+
return suiv;
|
|
770
|
+
});
|
|
771
|
+
return;
|
|
772
|
+
}).catch(() => {});
|
|
773
|
+
return () => stop.abort();
|
|
774
|
+
}, [manquants, loadTranscriptions]);
|
|
775
|
+
return charges;
|
|
776
|
+
}
|
|
777
|
+
function DatabookReader({
|
|
778
|
+
pages,
|
|
779
|
+
title,
|
|
780
|
+
resolveAssetUrl = (path) => path,
|
|
781
|
+
imageSrc = (url) => url,
|
|
782
|
+
imageSrcSet = () => null,
|
|
783
|
+
loadTranscriptions,
|
|
784
|
+
renderTranscription,
|
|
785
|
+
preloadCount = 4
|
|
786
|
+
}) {
|
|
787
|
+
const containerRef = useRef4(null);
|
|
788
|
+
const lightboxRef = useRef4(null);
|
|
789
|
+
const baseId = useId2();
|
|
790
|
+
const items = useMemo2(() => resolvePages(pages, resolveAssetUrl), [pages, resolveAssetUrl]);
|
|
791
|
+
const total = items.length;
|
|
792
|
+
const [mode, setMode] = useState3("vertical");
|
|
793
|
+
const [current, setCurrent] = useState3(0);
|
|
794
|
+
const { isFullscreen, toggleFullscreen } = useFullscreen(containerRef);
|
|
795
|
+
const { swiperRef, handleSwiper, handleSlideChange } = useSwiperSync(setCurrent);
|
|
796
|
+
const [lightbox, setLightbox] = useState3(false);
|
|
797
|
+
const textesCharges = useTextesPlanches(loadTranscriptions, items, current);
|
|
798
|
+
const texteDe = useCallback3((p) => p ? p.text ?? textesCharges.get(p.number) ?? null : null, [textesCharges]);
|
|
799
|
+
useFocusTrap(lightboxRef, lightbox, () => setLightbox(false));
|
|
800
|
+
useEffect4(() => {
|
|
801
|
+
if (current >= total && total > 0)
|
|
802
|
+
setCurrent(total - 1);
|
|
803
|
+
if (total === 0)
|
|
804
|
+
setCurrent(0);
|
|
805
|
+
}, [total, current]);
|
|
806
|
+
useEffect4(() => {
|
|
807
|
+
if (total === 0)
|
|
808
|
+
return;
|
|
809
|
+
for (const idx of [current - 1, current, current + 1, current + 2]) {
|
|
810
|
+
const url = items[idx]?.imageUrl;
|
|
811
|
+
if (!url)
|
|
812
|
+
continue;
|
|
813
|
+
const img = new Image;
|
|
814
|
+
img.src = url;
|
|
815
|
+
}
|
|
816
|
+
}, [current, items, total]);
|
|
817
|
+
const [fullscreenDispo, setFullscreenDispo] = useState3(false);
|
|
818
|
+
useEffect4(() => {
|
|
819
|
+
setFullscreenDispo(typeof document !== "undefined" && document.fullscreenEnabled && typeof containerRef.current?.requestFullscreen === "function");
|
|
820
|
+
}, []);
|
|
821
|
+
const scrollParentRef = useRef4(null);
|
|
822
|
+
const virtualizer = useVirtualizer2({
|
|
823
|
+
count: total,
|
|
824
|
+
getScrollElement: () => scrollParentRef.current,
|
|
825
|
+
estimateSize: () => typeof window === "undefined" ? 1100 : Math.round(window.innerHeight * 0.95),
|
|
826
|
+
overscan: Math.max(2, preloadCount)
|
|
827
|
+
});
|
|
828
|
+
const goTo = useCallback3((idx) => {
|
|
829
|
+
const i = Math.min(Math.max(idx, 0), Math.max(0, total - 1));
|
|
830
|
+
setCurrent(i);
|
|
831
|
+
if (mode === "paged")
|
|
832
|
+
swiperRef.current?.slideTo(i);
|
|
833
|
+
else
|
|
834
|
+
virtualizer.scrollToIndex(i, { align: "start" });
|
|
835
|
+
}, [mode, total, virtualizer]);
|
|
836
|
+
useVerticalScrollSync(mode, scrollParentRef, virtualizer, setCurrent);
|
|
837
|
+
useEffect4(() => {
|
|
838
|
+
const actif = lightbox || mode === "paged";
|
|
839
|
+
if (!actif)
|
|
840
|
+
return;
|
|
841
|
+
const onKey = (e) => {
|
|
842
|
+
const cible = e.target;
|
|
843
|
+
const tag = cible?.tagName;
|
|
844
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || cible?.isContentEditable)
|
|
845
|
+
return;
|
|
846
|
+
switch (e.key) {
|
|
847
|
+
case "Escape":
|
|
848
|
+
if (lightbox)
|
|
849
|
+
setLightbox(false);
|
|
850
|
+
return;
|
|
851
|
+
case "ArrowLeft":
|
|
852
|
+
e.preventDefault();
|
|
853
|
+
goTo(current - 1);
|
|
854
|
+
return;
|
|
855
|
+
case "ArrowRight":
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
goTo(current + 1);
|
|
858
|
+
return;
|
|
859
|
+
case "Home":
|
|
860
|
+
e.preventDefault();
|
|
861
|
+
goTo(0);
|
|
862
|
+
return;
|
|
863
|
+
case "End":
|
|
864
|
+
e.preventDefault();
|
|
865
|
+
goTo(total - 1);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
window.addEventListener("keydown", onKey);
|
|
870
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
871
|
+
}, [lightbox, mode, total, current, goTo]);
|
|
872
|
+
const goPrevPage = useCallback3(() => {
|
|
873
|
+
goTo(current - 1);
|
|
874
|
+
}, [current, goTo]);
|
|
875
|
+
const goNextPage = useCallback3(() => {
|
|
876
|
+
goTo(current + 1);
|
|
877
|
+
}, [current, goTo]);
|
|
878
|
+
const openLightbox = useCallback3((idx) => {
|
|
879
|
+
if (typeof idx === "number")
|
|
880
|
+
setCurrent(idx);
|
|
881
|
+
const item = items[typeof idx === "number" ? idx : current];
|
|
882
|
+
if (item?.imageUrl)
|
|
883
|
+
setLightbox(true);
|
|
884
|
+
}, [items, current]);
|
|
885
|
+
const toucheRef = useRef4(null);
|
|
886
|
+
const onTouchStart = useCallback3((e) => {
|
|
887
|
+
const t = e.touches[0];
|
|
888
|
+
toucheRef.current = t ? { x: t.clientX, y: t.clientY } : null;
|
|
889
|
+
}, []);
|
|
890
|
+
const onTouchEnd = useCallback3((e) => {
|
|
891
|
+
const depart = toucheRef.current;
|
|
892
|
+
toucheRef.current = null;
|
|
893
|
+
const t = e.changedTouches[0];
|
|
894
|
+
if (!depart || !t)
|
|
895
|
+
return;
|
|
896
|
+
const dx = t.clientX - depart.x;
|
|
897
|
+
const dy = t.clientY - depart.y;
|
|
898
|
+
if (Math.abs(dx) < 48 || Math.abs(dx) < Math.abs(dy))
|
|
899
|
+
return;
|
|
900
|
+
goTo(dx < 0 ? current + 1 : current - 1);
|
|
901
|
+
}, [current, goTo]);
|
|
902
|
+
const currentItem = items[current];
|
|
903
|
+
const currentNum = currentItem?.number ?? current + 1;
|
|
904
|
+
const counter = total ? `n°${currentNum} · ${Math.min(current + 1, total)}/${total}` : "0 / 0";
|
|
905
|
+
const currentText = texteDe(currentItem);
|
|
906
|
+
const currentImage = currentItem?.imageUrl ?? null;
|
|
907
|
+
if (total === 0) {
|
|
908
|
+
return /* @__PURE__ */ jsx2("div", {
|
|
909
|
+
className: "m3-reader-frame flex min-h-[200px] items-center justify-center rounded-xl p-8 text-[18px] text-[var(--reader-on-surface)]/50",
|
|
910
|
+
children: "Aucune page à afficher pour le moment."
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
const legendBlock = /* @__PURE__ */ jsxs2("div", {
|
|
914
|
+
className: "flex h-full min-h-0 flex-col p-4 bg-[var(--reader-surface)] border-t-2 lg:border-t-0 lg:border-l-2 border-[var(--reader-outline)]",
|
|
915
|
+
children: [
|
|
916
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
917
|
+
className: "mb-2 shrink-0 flex items-center justify-between",
|
|
918
|
+
children: [
|
|
919
|
+
/* @__PURE__ */ jsx2("span", {
|
|
920
|
+
className: "m3-reader-badge bg-[var(--reader-accent)] text-[var(--reader-on-accent)] px-2 py-0.5 text-[18px] font-black",
|
|
921
|
+
children: /* @__PURE__ */ jsxs2("span", {
|
|
922
|
+
children: [
|
|
923
|
+
"PLANCHE ",
|
|
924
|
+
currentNum
|
|
925
|
+
]
|
|
926
|
+
})
|
|
927
|
+
}),
|
|
928
|
+
/* @__PURE__ */ jsxs2("span", {
|
|
929
|
+
className: "text-[18px] font-bold tabular-nums text-[var(--reader-on-surface)]/60",
|
|
930
|
+
children: [
|
|
931
|
+
current + 1,
|
|
932
|
+
" / ",
|
|
933
|
+
total
|
|
934
|
+
]
|
|
935
|
+
})
|
|
936
|
+
]
|
|
937
|
+
}),
|
|
938
|
+
/* @__PURE__ */ jsx2("div", {
|
|
939
|
+
className: "min-h-0 flex-1 overflow-y-auto overscroll-contain pr-1 m3-reader-scroll",
|
|
940
|
+
children: currentText ? renderTranscription?.({
|
|
941
|
+
text: currentText,
|
|
942
|
+
page: currentNum,
|
|
943
|
+
verified: currentItem?.verifiee ?? false
|
|
944
|
+
}) ?? currentText : currentItem?.aDuTexte ? /* @__PURE__ */ jsx2("p", {
|
|
945
|
+
className: "text-[18px] italic text-[var(--reader-on-surface)]/50",
|
|
946
|
+
children: "Chargement de la transcription…"
|
|
947
|
+
}) : /* @__PURE__ */ jsx2("p", {
|
|
948
|
+
className: "text-[18px] italic text-[var(--reader-on-surface)]/50",
|
|
949
|
+
children: "Aucune description pour cette page."
|
|
950
|
+
})
|
|
951
|
+
})
|
|
952
|
+
]
|
|
953
|
+
});
|
|
954
|
+
return /* @__PURE__ */ jsxs2("div", {
|
|
955
|
+
ref: containerRef,
|
|
956
|
+
className: "m3-reader m3-reader-container m3-reader-scroll flex h-[84svh] min-h-[460px] w-full min-w-0 flex-col overflow-hidden rounded-xl bg-[var(--reader-surface)] [contain:inline-size] [&:fullscreen]:h-screen [&:fullscreen]:min-h-screen [&:fullscreen]:rounded-none",
|
|
957
|
+
"data-databook-reader": true,
|
|
958
|
+
"data-m3-component": "DatabookReader",
|
|
959
|
+
"data-testid": "databook-reader",
|
|
960
|
+
role: "region",
|
|
961
|
+
"aria-label": title,
|
|
962
|
+
children: [
|
|
963
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
964
|
+
className: "m3-reader-hud z-10 flex flex-nowrap items-center gap-2 px-3 py-2.5 sm:flex-wrap",
|
|
965
|
+
children: [
|
|
966
|
+
/* @__PURE__ */ jsx2("span", {
|
|
967
|
+
className: "mr-1 hidden min-w-0 max-w-[40ch] truncate font-sans text-[18px] font-extrabold uppercase tracking-wide text-[var(--reader-on-surface)] drop-shadow-[1px_1px_0px_#000] sm:block",
|
|
968
|
+
children: title
|
|
969
|
+
}),
|
|
970
|
+
/* @__PURE__ */ jsx2("span", {
|
|
971
|
+
className: "m3-reader-badge bg-[var(--reader-accent)] text-[var(--reader-on-accent)] px-2 py-0.5 text-[18px] font-black sm:ml-auto",
|
|
972
|
+
children: /* @__PURE__ */ jsx2("span", {
|
|
973
|
+
children: counter
|
|
974
|
+
})
|
|
975
|
+
}),
|
|
976
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
977
|
+
className: "ml-auto flex shrink-0 items-center gap-1.5 sm:ml-0 sm:gap-2",
|
|
978
|
+
children: [
|
|
979
|
+
/* @__PURE__ */ jsxs2("button", {
|
|
980
|
+
type: "button",
|
|
981
|
+
className: mode === "vertical" ? "m3-reader-btn-active" : "m3-reader-btn",
|
|
982
|
+
onClick: () => setMode("vertical"),
|
|
983
|
+
"aria-pressed": mode === "vertical",
|
|
984
|
+
"aria-label": "Mode vertical (planche + texte)",
|
|
985
|
+
title: "Mode vertical (planche + texte)",
|
|
986
|
+
children: [
|
|
987
|
+
/* @__PURE__ */ jsx2(ScrollText2, {
|
|
988
|
+
size: 14,
|
|
989
|
+
"aria-hidden": true
|
|
990
|
+
}),
|
|
991
|
+
/* @__PURE__ */ jsx2("span", {
|
|
992
|
+
className: "hidden sm:inline",
|
|
993
|
+
children: "Défilement"
|
|
994
|
+
})
|
|
995
|
+
]
|
|
996
|
+
}),
|
|
997
|
+
/* @__PURE__ */ jsxs2("button", {
|
|
998
|
+
type: "button",
|
|
999
|
+
className: mode === "paged" ? "m3-reader-btn-active" : "m3-reader-btn",
|
|
1000
|
+
onClick: () => setMode("paged"),
|
|
1001
|
+
"aria-pressed": mode === "paged",
|
|
1002
|
+
"aria-label": "Mode paginé",
|
|
1003
|
+
title: "Mode paginé",
|
|
1004
|
+
children: [
|
|
1005
|
+
/* @__PURE__ */ jsx2(BookOpen2, {
|
|
1006
|
+
size: 14,
|
|
1007
|
+
"aria-hidden": true
|
|
1008
|
+
}),
|
|
1009
|
+
/* @__PURE__ */ jsx2("span", {
|
|
1010
|
+
className: "hidden sm:inline",
|
|
1011
|
+
children: "Paginé"
|
|
1012
|
+
})
|
|
1013
|
+
]
|
|
1014
|
+
}),
|
|
1015
|
+
/* @__PURE__ */ jsx2("button", {
|
|
1016
|
+
type: "button",
|
|
1017
|
+
className: "m3-reader-btn",
|
|
1018
|
+
onClick: () => openLightbox(),
|
|
1019
|
+
disabled: !currentImage,
|
|
1020
|
+
"aria-label": "Agrandir la planche",
|
|
1021
|
+
title: "Agrandir la planche",
|
|
1022
|
+
children: /* @__PURE__ */ jsx2(ZoomIn, {
|
|
1023
|
+
size: 14,
|
|
1024
|
+
"aria-hidden": true
|
|
1025
|
+
})
|
|
1026
|
+
}),
|
|
1027
|
+
fullscreenDispo && /* @__PURE__ */ jsx2("button", {
|
|
1028
|
+
type: "button",
|
|
1029
|
+
className: "m3-reader-btn",
|
|
1030
|
+
onClick: toggleFullscreen,
|
|
1031
|
+
"aria-label": isFullscreen ? "Quitter le plein écran" : "Activer le plein écran",
|
|
1032
|
+
title: isFullscreen ? "Quitter le plein écran" : "Plein écran",
|
|
1033
|
+
children: isFullscreen ? /* @__PURE__ */ jsx2(Minimize22, {
|
|
1034
|
+
size: 14,
|
|
1035
|
+
"aria-hidden": true
|
|
1036
|
+
}) : /* @__PURE__ */ jsx2(Maximize22, {
|
|
1037
|
+
size: 14,
|
|
1038
|
+
"aria-hidden": true
|
|
1039
|
+
})
|
|
1040
|
+
})
|
|
1041
|
+
]
|
|
1042
|
+
})
|
|
1043
|
+
]
|
|
1044
|
+
}),
|
|
1045
|
+
/* @__PURE__ */ jsx2("div", {
|
|
1046
|
+
className: "relative min-h-0 min-w-0 flex-1 overflow-hidden bg-[var(--reader-surface)]",
|
|
1047
|
+
children: mode === "paged" ? /* @__PURE__ */ jsxs2("div", {
|
|
1048
|
+
className: "flex h-full min-h-0 flex-col lg:flex-row",
|
|
1049
|
+
children: [
|
|
1050
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
1051
|
+
className: "relative min-h-[52%] min-w-0 flex-[1.6] lg:h-full lg:min-h-0 lg:flex-[1.15]",
|
|
1052
|
+
children: [
|
|
1053
|
+
/* @__PURE__ */ jsx2(Swiper2, {
|
|
1054
|
+
initialSlide: current,
|
|
1055
|
+
dir: "ltr",
|
|
1056
|
+
modules: [Zoom2, Virtual, Mousewheel2],
|
|
1057
|
+
className: "h-full w-full",
|
|
1058
|
+
slidesPerView: 1,
|
|
1059
|
+
mousewheel: { forceToAxis: true },
|
|
1060
|
+
zoom: { maxRatio: 3, toggle: true },
|
|
1061
|
+
virtual: true,
|
|
1062
|
+
onSwiper: handleSwiper,
|
|
1063
|
+
onSlideChange: handleSlideChange,
|
|
1064
|
+
children: items.map((item, i) => /* @__PURE__ */ jsx2(SwiperSlide2, {
|
|
1065
|
+
virtualIndex: i,
|
|
1066
|
+
zoom: Boolean(item.imageUrl),
|
|
1067
|
+
className: "flex items-center justify-center",
|
|
1068
|
+
children: /* @__PURE__ */ jsx2("div", {
|
|
1069
|
+
className: "swiper-zoom-container flex h-full w-full items-center justify-center bg-[var(--reader-surface)]",
|
|
1070
|
+
children: item.imageUrl ? /* @__PURE__ */ jsxs2("button", {
|
|
1071
|
+
type: "button",
|
|
1072
|
+
className: "group relative flex h-full w-full cursor-zoom-in items-center justify-center border-0 bg-transparent p-0",
|
|
1073
|
+
onClick: () => openLightbox(i),
|
|
1074
|
+
"aria-label": `Agrandir la page ${item.number}`,
|
|
1075
|
+
children: [
|
|
1076
|
+
/* @__PURE__ */ jsx2("img", {
|
|
1077
|
+
src: imageSrc(item.imageUrl, 1080),
|
|
1078
|
+
srcSet: imageSrcSet(item.imageUrl, [828, 1080, 1920]) || undefined,
|
|
1079
|
+
sizes: "(min-width: 1024px) 900px, 100vw",
|
|
1080
|
+
alt: `${title} — page ${item.number}`,
|
|
1081
|
+
loading: Math.abs(i - current) <= 1 ? "eager" : "lazy",
|
|
1082
|
+
draggable: false,
|
|
1083
|
+
className: "max-h-full max-w-full object-contain transition-opacity group-hover:opacity-95"
|
|
1084
|
+
}),
|
|
1085
|
+
/* @__PURE__ */ jsx2("span", {
|
|
1086
|
+
className: "pointer-events-none absolute right-3 bottom-3 rounded-md border border-white/15 bg-[var(--reader-surface)]/60 px-2 py-1 text-[18px] font-bold uppercase tracking-wider text-[var(--reader-on-surface)]/70 opacity-0 transition-opacity group-hover:opacity-100",
|
|
1087
|
+
children: "Agrandir"
|
|
1088
|
+
})
|
|
1089
|
+
]
|
|
1090
|
+
}) : /* @__PURE__ */ jsxs2("div", {
|
|
1091
|
+
className: "px-6 text-center text-[18px] text-[var(--reader-on-surface)]/50",
|
|
1092
|
+
children: [
|
|
1093
|
+
"Page ",
|
|
1094
|
+
item.number,
|
|
1095
|
+
" — pas d'image"
|
|
1096
|
+
]
|
|
1097
|
+
})
|
|
1098
|
+
})
|
|
1099
|
+
}, `db-page-${item.number}-${i}`))
|
|
1100
|
+
}, "databook-ltr"),
|
|
1101
|
+
/* @__PURE__ */ jsx2("button", {
|
|
1102
|
+
type: "button",
|
|
1103
|
+
"aria-label": "Page précédente",
|
|
1104
|
+
onClick: goPrevPage,
|
|
1105
|
+
disabled: current <= 0,
|
|
1106
|
+
className: "absolute top-1/2 left-2 z-10 -translate-y-1/2 rounded-full border border-[var(--reader-outline)] bg-[var(--reader-surface)]/60 p-3 text-[var(--reader-on-surface)] transition-colors hover:border-[var(--reader-accent)] hover:text-[var(--reader-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--reader-accent)] disabled:opacity-30",
|
|
1107
|
+
children: /* @__PURE__ */ jsx2(ChevronLeft2, {
|
|
1108
|
+
size: 20,
|
|
1109
|
+
"aria-hidden": true
|
|
1110
|
+
})
|
|
1111
|
+
}),
|
|
1112
|
+
/* @__PURE__ */ jsx2("button", {
|
|
1113
|
+
type: "button",
|
|
1114
|
+
"aria-label": "Page suivante",
|
|
1115
|
+
onClick: goNextPage,
|
|
1116
|
+
disabled: current >= total - 1,
|
|
1117
|
+
className: "absolute top-1/2 right-2 z-10 -translate-y-1/2 rounded-full border border-[var(--reader-outline)] bg-[var(--reader-surface)]/60 p-3 text-[var(--reader-on-surface)] transition-colors hover:border-[var(--reader-accent)] hover:text-[var(--reader-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--reader-accent)] disabled:opacity-30",
|
|
1118
|
+
children: /* @__PURE__ */ jsx2(ChevronRight2, {
|
|
1119
|
+
size: 20,
|
|
1120
|
+
"aria-hidden": true
|
|
1121
|
+
})
|
|
1122
|
+
})
|
|
1123
|
+
]
|
|
1124
|
+
}),
|
|
1125
|
+
/* @__PURE__ */ jsx2("div", {
|
|
1126
|
+
className: "min-h-0 max-h-[38%] shrink-0 border-t border-[var(--reader-outline)] bg-[var(--reader-surface)]/90 px-3 py-2.5 sm:max-h-[48%] sm:px-5 sm:py-4 lg:max-h-none lg:w-[min(420px,38%)] lg:overflow-hidden lg:border-l lg:border-t-0",
|
|
1127
|
+
children: legendBlock
|
|
1128
|
+
})
|
|
1129
|
+
]
|
|
1130
|
+
}) : /* @__PURE__ */ jsx2("div", {
|
|
1131
|
+
ref: scrollParentRef,
|
|
1132
|
+
tabIndex: 0,
|
|
1133
|
+
className: "h-full w-full snap-y snap-proximity overflow-y-auto overflow-x-hidden overscroll-y-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--reader-accent)]",
|
|
1134
|
+
"aria-label": `${title} — lecture verticale`,
|
|
1135
|
+
children: /* @__PURE__ */ jsx2("div", {
|
|
1136
|
+
style: {
|
|
1137
|
+
height: virtualizer.getTotalSize(),
|
|
1138
|
+
width: "100%",
|
|
1139
|
+
position: "relative"
|
|
1140
|
+
},
|
|
1141
|
+
children: virtualizer.getVirtualItems().map((vi) => {
|
|
1142
|
+
const item = items[vi.index];
|
|
1143
|
+
if (!item)
|
|
1144
|
+
return null;
|
|
1145
|
+
return /* @__PURE__ */ jsx2("div", {
|
|
1146
|
+
"data-index": vi.index,
|
|
1147
|
+
ref: virtualizer.measureElement,
|
|
1148
|
+
style: virtualRowStyle(vi.start),
|
|
1149
|
+
className: "snap-start snap-always border-b border-white/5 px-2 py-4 sm:px-4 sm:py-6",
|
|
1150
|
+
children: /* @__PURE__ */ jsxs2("div", {
|
|
1151
|
+
className: "group/planche mx-auto flex w-full max-w-3xl flex-col gap-3",
|
|
1152
|
+
children: [
|
|
1153
|
+
/* @__PURE__ */ jsxs2("span", {
|
|
1154
|
+
className: "text-[18px] font-bold uppercase tracking-[0.2em] text-[var(--reader-accent)]/70",
|
|
1155
|
+
children: [
|
|
1156
|
+
"Page ",
|
|
1157
|
+
item.number
|
|
1158
|
+
]
|
|
1159
|
+
}),
|
|
1160
|
+
item.imageUrl ? /* @__PURE__ */ jsxs2("button", {
|
|
1161
|
+
type: "button",
|
|
1162
|
+
className: "group relative cursor-zoom-in border-0 bg-transparent p-0 text-left focus-visible:outline-none",
|
|
1163
|
+
onClick: () => openLightbox(vi.index),
|
|
1164
|
+
"aria-label": `Ouvrir la page ${item.number} en plein écran`,
|
|
1165
|
+
children: [
|
|
1166
|
+
/* @__PURE__ */ jsx2("img", {
|
|
1167
|
+
src: imageSrc(item.imageUrl, 1080),
|
|
1168
|
+
srcSet: imageSrcSet(item.imageUrl, [828, 1080, 1920]) || undefined,
|
|
1169
|
+
sizes: "(min-width: 768px) 768px, 100vw",
|
|
1170
|
+
alt: `${title} — page ${item.number}`,
|
|
1171
|
+
loading: Math.abs(vi.index - current) <= 3 ? "eager" : "lazy",
|
|
1172
|
+
fetchPriority: vi.index === current ? "high" : "auto",
|
|
1173
|
+
decoding: "async",
|
|
1174
|
+
draggable: false,
|
|
1175
|
+
className: "h-auto w-full rounded-sm bg-[var(--reader-surface)] object-contain shadow-[0_0_24px_rgba(0,0,0,0.45)] transition-[transform,box-shadow] duration-300 ease-out will-change-transform group-hover:scale-[1.015] group-hover:shadow-[0_0_60px_rgba(0,0,0,0.75)] group-focus-visible:scale-[1.015] motion-reduce:transform-none motion-reduce:transition-none"
|
|
1176
|
+
}),
|
|
1177
|
+
/* @__PURE__ */ jsxs2("span", {
|
|
1178
|
+
className: "pointer-events-none absolute right-3 bottom-3 inline-flex items-center gap-1.5 rounded-md border border-white/15 bg-[var(--reader-surface)]/70 px-2.5 py-1.5 text-[18px] font-bold uppercase tracking-wider text-[var(--reader-on-surface)]/80 opacity-0 backdrop-blur-sm transition-opacity duration-200 group-hover:opacity-100 group-focus-visible:opacity-100",
|
|
1179
|
+
children: [
|
|
1180
|
+
/* @__PURE__ */ jsx2(ZoomIn, {
|
|
1181
|
+
size: 12,
|
|
1182
|
+
"aria-hidden": true
|
|
1183
|
+
}),
|
|
1184
|
+
"Plein écran"
|
|
1185
|
+
]
|
|
1186
|
+
})
|
|
1187
|
+
]
|
|
1188
|
+
}) : /* @__PURE__ */ jsx2("div", {
|
|
1189
|
+
className: "flex min-h-[120px] items-center justify-center rounded border border-dashed border-white/10 bg-[var(--reader-surface)]/40 text-[18px] text-[var(--reader-on-surface)]/50",
|
|
1190
|
+
children: "Pas d'image pour cette page"
|
|
1191
|
+
}),
|
|
1192
|
+
texteDe(item) ? /* @__PURE__ */ jsx2("div", {
|
|
1193
|
+
className: "rounded-md border border-[var(--reader-outline)]/40 bg-[var(--reader-surface)]/50 px-4 py-3",
|
|
1194
|
+
children: renderTranscription?.({
|
|
1195
|
+
text: texteDe(item),
|
|
1196
|
+
page: item.number,
|
|
1197
|
+
verified: item.verifiee
|
|
1198
|
+
}) ?? texteDe(item)
|
|
1199
|
+
}) : null
|
|
1200
|
+
]
|
|
1201
|
+
})
|
|
1202
|
+
}, `${baseId}-${vi.key}`);
|
|
1203
|
+
})
|
|
1204
|
+
})
|
|
1205
|
+
})
|
|
1206
|
+
}),
|
|
1207
|
+
mode === "paged" && total > 0 && total <= SHOW_STRIP_MAX && /* @__PURE__ */ jsx2("div", {
|
|
1208
|
+
className: "z-10 flex items-center gap-4 overflow-x-auto border-t border-[var(--reader-outline)] bg-[var(--reader-surface)]/90 px-4 py-3 scrollbar-thin",
|
|
1209
|
+
children: /* @__PURE__ */ jsx2("div", {
|
|
1210
|
+
className: "mx-auto flex gap-2",
|
|
1211
|
+
children: items.map((item, idx) => /* @__PURE__ */ jsxs2("button", {
|
|
1212
|
+
type: "button",
|
|
1213
|
+
onClick: () => goTo(idx),
|
|
1214
|
+
className: `relative h-16 w-12 flex-shrink-0 overflow-hidden rounded border-2 transition-all ${current === idx ? "scale-105 border-[var(--reader-accent)] shadow-[0_0_10px_rgba(255,178,0,0.4)]" : "border-white/10 hover:border-white/30"}`,
|
|
1215
|
+
title: `Page ${item.number}`,
|
|
1216
|
+
"aria-label": `Aller à la page ${item.number}`,
|
|
1217
|
+
"aria-current": current === idx ? "true" : undefined,
|
|
1218
|
+
children: [
|
|
1219
|
+
item.imageUrl ? /* @__PURE__ */ jsx2("img", {
|
|
1220
|
+
src: imageSrc(item.imageUrl, 96),
|
|
1221
|
+
alt: "",
|
|
1222
|
+
className: "h-full w-full object-cover",
|
|
1223
|
+
loading: "lazy"
|
|
1224
|
+
}) : /* @__PURE__ */ jsx2("span", {
|
|
1225
|
+
className: "flex h-full w-full items-center justify-center bg-zinc-900 text-[18px] text-[var(--reader-on-surface)]/50",
|
|
1226
|
+
children: "txt"
|
|
1227
|
+
}),
|
|
1228
|
+
/* @__PURE__ */ jsx2("span", {
|
|
1229
|
+
className: "absolute inset-x-0 bottom-0 bg-[var(--reader-surface)]/70 text-center font-mono text-[18px] font-bold text-[var(--reader-on-surface)]",
|
|
1230
|
+
children: item.number
|
|
1231
|
+
})
|
|
1232
|
+
]
|
|
1233
|
+
}, `thumb-${item.number}-${idx}`))
|
|
1234
|
+
})
|
|
1235
|
+
}),
|
|
1236
|
+
mode === "paged" && total > SHOW_STRIP_MAX && /* @__PURE__ */ jsxs2("form", {
|
|
1237
|
+
className: "z-10 flex items-center justify-center gap-3 border-t-2 border-[var(--reader-outline)] bg-[var(--reader-surface)] px-4 py-3",
|
|
1238
|
+
onSubmit: (e) => {
|
|
1239
|
+
e.preventDefault();
|
|
1240
|
+
const field = e.currentTarget.elements.namedItem("goto");
|
|
1241
|
+
const wanted = Number(field?.value);
|
|
1242
|
+
if (!Number.isFinite(wanted))
|
|
1243
|
+
return;
|
|
1244
|
+
const byNumber = items.findIndex((it) => it.number === Math.trunc(wanted));
|
|
1245
|
+
if (byNumber >= 0)
|
|
1246
|
+
goTo(byNumber);
|
|
1247
|
+
else
|
|
1248
|
+
goTo(Math.trunc(wanted) - 1);
|
|
1249
|
+
},
|
|
1250
|
+
children: [
|
|
1251
|
+
/* @__PURE__ */ jsx2("label", {
|
|
1252
|
+
htmlFor: `${baseId}-goto`,
|
|
1253
|
+
className: "text-[18px] font-black uppercase tracking-wider text-[var(--reader-on-surface)]",
|
|
1254
|
+
children: "Aller à n°"
|
|
1255
|
+
}),
|
|
1256
|
+
/* @__PURE__ */ jsx2("input", {
|
|
1257
|
+
id: `${baseId}-goto`,
|
|
1258
|
+
name: "goto",
|
|
1259
|
+
type: "number",
|
|
1260
|
+
defaultValue: currentNum,
|
|
1261
|
+
className: "w-20 rounded-md border-2 border-[var(--reader-outline)] bg-[var(--reader-surface)] px-2.5 py-1 text-center text-[18px] font-extrabold tabular-nums text-[var(--reader-accent)] shadow-[2px_2px_0px_#000] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--reader-accent)]"
|
|
1262
|
+
}, currentNum),
|
|
1263
|
+
/* @__PURE__ */ jsx2("button", {
|
|
1264
|
+
type: "submit",
|
|
1265
|
+
className: "m3-reader-btn-active !py-1 !px-3.5",
|
|
1266
|
+
children: "OK"
|
|
1267
|
+
})
|
|
1268
|
+
]
|
|
1269
|
+
}),
|
|
1270
|
+
lightbox && currentImage && /* @__PURE__ */ jsxs2("div", {
|
|
1271
|
+
ref: lightboxRef,
|
|
1272
|
+
tabIndex: -1,
|
|
1273
|
+
role: "dialog",
|
|
1274
|
+
"aria-modal": "true",
|
|
1275
|
+
"aria-label": `${title} — page ${currentNum} agrandie`,
|
|
1276
|
+
className: "fixed inset-0 z-[100] flex flex-col bg-[var(--reader-surface)]/95",
|
|
1277
|
+
onClick: () => setLightbox(false),
|
|
1278
|
+
children: [
|
|
1279
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
1280
|
+
className: "flex items-center gap-3 border-b-2 border-[var(--reader-outline)] bg-[var(--reader-surface)] px-4 py-3",
|
|
1281
|
+
children: [
|
|
1282
|
+
/* @__PURE__ */ jsx2("span", {
|
|
1283
|
+
className: "min-w-0 flex-1 truncate font-sans text-[18px] font-extrabold uppercase tracking-wide text-[var(--reader-on-surface)]",
|
|
1284
|
+
children: title
|
|
1285
|
+
}),
|
|
1286
|
+
/* @__PURE__ */ jsx2("span", {
|
|
1287
|
+
className: "m3-reader-badge bg-[var(--reader-accent)] text-[var(--reader-on-accent)] px-2 py-0.5 text-[18px] font-black",
|
|
1288
|
+
children: /* @__PURE__ */ jsxs2("span", {
|
|
1289
|
+
children: [
|
|
1290
|
+
"n°",
|
|
1291
|
+
currentNum
|
|
1292
|
+
]
|
|
1293
|
+
})
|
|
1294
|
+
}),
|
|
1295
|
+
/* @__PURE__ */ jsx2("button", {
|
|
1296
|
+
type: "button",
|
|
1297
|
+
className: "m3-reader-btn ml-auto",
|
|
1298
|
+
onClick: (e) => {
|
|
1299
|
+
e.stopPropagation();
|
|
1300
|
+
setLightbox(false);
|
|
1301
|
+
},
|
|
1302
|
+
"aria-label": "Fermer",
|
|
1303
|
+
children: /* @__PURE__ */ jsx2(X, {
|
|
1304
|
+
size: 16,
|
|
1305
|
+
"aria-hidden": true
|
|
1306
|
+
})
|
|
1307
|
+
})
|
|
1308
|
+
]
|
|
1309
|
+
}),
|
|
1310
|
+
/* @__PURE__ */ jsxs2("div", {
|
|
1311
|
+
className: "relative flex min-h-0 flex-1 items-center justify-center p-2 sm:p-4",
|
|
1312
|
+
onClick: (e) => e.stopPropagation(),
|
|
1313
|
+
onTouchStart,
|
|
1314
|
+
onTouchEnd,
|
|
1315
|
+
children: [
|
|
1316
|
+
/* @__PURE__ */ jsx2("img", {
|
|
1317
|
+
src: currentImage,
|
|
1318
|
+
alt: `${title} — page ${currentNum}`,
|
|
1319
|
+
className: "max-h-full max-w-full object-contain",
|
|
1320
|
+
draggable: false
|
|
1321
|
+
}),
|
|
1322
|
+
/* @__PURE__ */ jsx2("button", {
|
|
1323
|
+
type: "button",
|
|
1324
|
+
"aria-label": "Page précédente",
|
|
1325
|
+
onClick: () => goTo(current - 1),
|
|
1326
|
+
disabled: current <= 0,
|
|
1327
|
+
className: "absolute top-1/2 left-4 -translate-y-1/2 rounded-lg border-2 border-[var(--reader-outline)] bg-[var(--reader-surface)]/90 p-3 text-[var(--reader-accent)] shadow-[3px_3px_0px_#000] hover:bg-[var(--reader-accent)] hover:text-[var(--reader-on-accent)] transition-all disabled:opacity-30",
|
|
1328
|
+
children: /* @__PURE__ */ jsx2(ChevronLeft2, {
|
|
1329
|
+
size: 22,
|
|
1330
|
+
"aria-hidden": true
|
|
1331
|
+
})
|
|
1332
|
+
}),
|
|
1333
|
+
/* @__PURE__ */ jsx2("button", {
|
|
1334
|
+
type: "button",
|
|
1335
|
+
"aria-label": "Page suivante",
|
|
1336
|
+
onClick: () => goTo(current + 1),
|
|
1337
|
+
disabled: current >= total - 1,
|
|
1338
|
+
className: "absolute top-1/2 right-4 -translate-y-1/2 rounded-lg border-2 border-[var(--reader-outline)] bg-[var(--reader-surface)]/90 p-3 text-[var(--reader-accent)] shadow-[3px_3px_0px_#000] hover:bg-[var(--reader-accent)] hover:text-[var(--reader-on-accent)] transition-all disabled:opacity-30",
|
|
1339
|
+
children: /* @__PURE__ */ jsx2(ChevronRight2, {
|
|
1340
|
+
size: 22,
|
|
1341
|
+
"aria-hidden": true
|
|
1342
|
+
})
|
|
1343
|
+
})
|
|
1344
|
+
]
|
|
1345
|
+
})
|
|
1346
|
+
]
|
|
1347
|
+
})
|
|
1348
|
+
]
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
// packages/m3-reader/src/CollectionReader.tsx
|
|
1352
|
+
import { useEffect as useEffect5, useId as useId3, useMemo as useMemo3, useState as useState4 } from "react";
|
|
1353
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1354
|
+
"use client";
|
|
1355
|
+
function CollectionReader({ title, editions, initialEdition, initialChapter, emptyLabel, contentsLabel, previousLabel, nextLabel, onSelectionChange, renderChapter }) {
|
|
1356
|
+
const contentsId = useId3();
|
|
1357
|
+
const available = useMemo3(() => editions.map((edition) => ({ ...edition, chapters: edition.chapters.filter((chapter) => chapter.pageCount > 0) })).filter((edition) => edition.chapters.length > 0), [editions]);
|
|
1358
|
+
const [editionId, setEditionId] = useState4(() => available.find((edition) => edition.id === initialEdition)?.id ?? available[0]?.id ?? "");
|
|
1359
|
+
const edition = available.find((item) => item.id === editionId) ?? available[0];
|
|
1360
|
+
const [chapterIndex, setChapterIndex] = useState4(() => Math.max(0, edition?.chapters.findIndex((chapter) => String(chapter.id) === String(initialChapter)) ?? 0));
|
|
1361
|
+
const [contentsOpen, setContentsOpen] = useState4(false);
|
|
1362
|
+
const safeIndex = Math.min(chapterIndex, Math.max(0, (edition?.chapters.length ?? 0) - 1));
|
|
1363
|
+
const chapter = edition?.chapters[safeIndex];
|
|
1364
|
+
useEffect5(() => {
|
|
1365
|
+
if (edition && chapter)
|
|
1366
|
+
onSelectionChange?.(edition, chapter);
|
|
1367
|
+
}, [edition, chapter, onSelectionChange]);
|
|
1368
|
+
if (!edition || !chapter) {
|
|
1369
|
+
return /* @__PURE__ */ jsx3("p", {
|
|
1370
|
+
className: "m3-reader-collection-empty",
|
|
1371
|
+
"data-m3-component": "CollectionReader",
|
|
1372
|
+
"data-testid": "collection-reader-empty",
|
|
1373
|
+
children: emptyLabel
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
return /* @__PURE__ */ jsxs3("section", {
|
|
1377
|
+
className: "m3-reader m3-reader-collection",
|
|
1378
|
+
"aria-label": title,
|
|
1379
|
+
"data-m3-component": "CollectionReader",
|
|
1380
|
+
"data-testid": "collection-reader",
|
|
1381
|
+
children: [
|
|
1382
|
+
/* @__PURE__ */ jsxs3("div", {
|
|
1383
|
+
className: "m3-reader-collection-toolbar",
|
|
1384
|
+
children: [
|
|
1385
|
+
/* @__PURE__ */ jsx3("div", {
|
|
1386
|
+
className: "m3-reader-collection-editions",
|
|
1387
|
+
role: "group",
|
|
1388
|
+
"aria-label": title,
|
|
1389
|
+
children: available.map((item) => /* @__PURE__ */ jsx3("button", {
|
|
1390
|
+
type: "button",
|
|
1391
|
+
className: item.id === edition.id ? "m3-reader-btn-active" : "m3-reader-btn",
|
|
1392
|
+
"aria-pressed": item.id === edition.id,
|
|
1393
|
+
onClick: () => {
|
|
1394
|
+
setEditionId(item.id);
|
|
1395
|
+
setChapterIndex(0);
|
|
1396
|
+
setContentsOpen(false);
|
|
1397
|
+
},
|
|
1398
|
+
children: item.label
|
|
1399
|
+
}, item.id))
|
|
1400
|
+
}),
|
|
1401
|
+
/* @__PURE__ */ jsxs3("button", {
|
|
1402
|
+
type: "button",
|
|
1403
|
+
className: "m3-reader-btn",
|
|
1404
|
+
"aria-expanded": contentsOpen,
|
|
1405
|
+
"aria-controls": contentsOpen ? contentsId : undefined,
|
|
1406
|
+
onClick: () => setContentsOpen((value) => !value),
|
|
1407
|
+
children: [
|
|
1408
|
+
contentsLabel,
|
|
1409
|
+
" (",
|
|
1410
|
+
edition.chapters.length,
|
|
1411
|
+
")"
|
|
1412
|
+
]
|
|
1413
|
+
})
|
|
1414
|
+
]
|
|
1415
|
+
}),
|
|
1416
|
+
contentsOpen && /* @__PURE__ */ jsx3("div", {
|
|
1417
|
+
id: contentsId,
|
|
1418
|
+
className: "m3-reader-collection-contents",
|
|
1419
|
+
role: "group",
|
|
1420
|
+
"aria-label": contentsLabel,
|
|
1421
|
+
children: edition.chapters.map((item, index) => /* @__PURE__ */ jsxs3("button", {
|
|
1422
|
+
type: "button",
|
|
1423
|
+
className: index === safeIndex ? "m3-reader-btn-active" : "m3-reader-btn",
|
|
1424
|
+
"aria-current": index === safeIndex ? "page" : undefined,
|
|
1425
|
+
disabled: item.pageCount === 0,
|
|
1426
|
+
onClick: () => {
|
|
1427
|
+
setChapterIndex(index);
|
|
1428
|
+
setContentsOpen(false);
|
|
1429
|
+
},
|
|
1430
|
+
children: [
|
|
1431
|
+
item.label,
|
|
1432
|
+
" · ",
|
|
1433
|
+
item.pageCount
|
|
1434
|
+
]
|
|
1435
|
+
}, item.id))
|
|
1436
|
+
}),
|
|
1437
|
+
renderChapter(edition, chapter, safeIndex),
|
|
1438
|
+
/* @__PURE__ */ jsxs3("nav", {
|
|
1439
|
+
className: "m3-reader-collection-navigation",
|
|
1440
|
+
"aria-label": contentsLabel,
|
|
1441
|
+
children: [
|
|
1442
|
+
/* @__PURE__ */ jsx3("button", {
|
|
1443
|
+
type: "button",
|
|
1444
|
+
className: "m3-reader-btn",
|
|
1445
|
+
disabled: safeIndex === 0,
|
|
1446
|
+
onClick: () => setChapterIndex(safeIndex - 1),
|
|
1447
|
+
children: previousLabel
|
|
1448
|
+
}),
|
|
1449
|
+
/* @__PURE__ */ jsxs3("span", {
|
|
1450
|
+
"aria-live": "polite",
|
|
1451
|
+
children: [
|
|
1452
|
+
chapter.label,
|
|
1453
|
+
" (",
|
|
1454
|
+
safeIndex + 1,
|
|
1455
|
+
"/",
|
|
1456
|
+
edition.chapters.length,
|
|
1457
|
+
")"
|
|
1458
|
+
]
|
|
1459
|
+
}),
|
|
1460
|
+
/* @__PURE__ */ jsx3("button", {
|
|
1461
|
+
type: "button",
|
|
1462
|
+
className: "m3-reader-btn",
|
|
1463
|
+
disabled: safeIndex >= edition.chapters.length - 1,
|
|
1464
|
+
onClick: () => setChapterIndex(safeIndex + 1),
|
|
1465
|
+
children: nextLabel
|
|
1466
|
+
})
|
|
1467
|
+
]
|
|
1468
|
+
})
|
|
1469
|
+
]
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
export {
|
|
1473
|
+
CollectionReader,
|
|
1474
|
+
DatabookReader,
|
|
1475
|
+
MangaReader
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
//# debugId=F914B781AD4BAB2F64756E2164756E21
|