zreader 0.0.0-stage → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +234 -2
  4. package/dist/blocks-DwR7l7pz.d.ts +20 -0
  5. package/dist/chunk-3WQBK6DE.js +47 -0
  6. package/dist/chunk-3WQBK6DE.js.map +1 -0
  7. package/dist/chunk-DYQNGV46.js +31 -0
  8. package/dist/chunk-DYQNGV46.js.map +1 -0
  9. package/dist/chunk-ISI35TCM.js +62 -0
  10. package/dist/chunk-ISI35TCM.js.map +1 -0
  11. package/dist/chunk-RNSAFDZ5.js +105 -0
  12. package/dist/chunk-RNSAFDZ5.js.map +1 -0
  13. package/dist/config/index.cjs +196 -0
  14. package/dist/config/index.cjs.map +1 -0
  15. package/dist/config/index.d.cts +159 -0
  16. package/dist/config/index.d.ts +159 -0
  17. package/dist/config/index.js +182 -0
  18. package/dist/config/index.js.map +1 -0
  19. package/dist/content/index.d.ts +143 -0
  20. package/dist/content/index.js +379 -0
  21. package/dist/content/index.js.map +1 -0
  22. package/dist/core/index.d.ts +262 -0
  23. package/dist/core/index.js +935 -0
  24. package/dist/core/index.js.map +1 -0
  25. package/dist/editor/index.d.ts +19 -0
  26. package/dist/editor/index.js +277 -0
  27. package/dist/editor/index.js.map +1 -0
  28. package/dist/index-Bp5oIiZd.d.ts +61 -0
  29. package/dist/next/client/index.d.ts +8 -0
  30. package/dist/next/client/index.js +13 -0
  31. package/dist/next/client/index.js.map +1 -0
  32. package/dist/next/index.cjs +272 -0
  33. package/dist/next/index.cjs.map +1 -0
  34. package/dist/next/index.d.cts +47 -0
  35. package/dist/next/index.d.ts +47 -0
  36. package/dist/next/index.js +262 -0
  37. package/dist/next/index.js.map +1 -0
  38. package/dist/next/server/index.d.ts +58 -0
  39. package/dist/next/server/index.js +44 -0
  40. package/dist/next/server/index.js.map +1 -0
  41. package/dist/react/index.d.ts +376 -0
  42. package/dist/react/index.js +2793 -0
  43. package/dist/react/index.js.map +1 -0
  44. package/dist/reader.css +116 -0
  45. package/dist/rehype/index.d.ts +27 -0
  46. package/dist/rehype/index.js +4 -0
  47. package/dist/rehype/index.js.map +1 -0
  48. package/dist/stubs/empty-config.d.ts +9 -0
  49. package/dist/stubs/empty-config.js +7 -0
  50. package/dist/stubs/empty-config.js.map +1 -0
  51. package/dist/stubs/server-only.d.ts +2 -0
  52. package/dist/stubs/server-only.js +6 -0
  53. package/dist/stubs/server-only.js.map +1 -0
  54. package/dist/styles.css +1 -0
  55. package/dist/tailwind.css +41 -0
  56. package/dist/types-xkdpkPxA.d.ts +147 -0
  57. package/dist/user-config/index.d.ts +17 -0
  58. package/dist/user-config/index.js +35 -0
  59. package/dist/user-config/index.js.map +1 -0
  60. package/dist/virtual-config/index.d.ts +6 -0
  61. package/dist/virtual-config/index.js +10 -0
  62. package/dist/virtual-config/index.js.map +1 -0
  63. package/package.json +225 -4
@@ -0,0 +1,2793 @@
1
+ "use client";
2
+ import * as React13 from 'react';
3
+ import { forwardRef, createContext, lazy, useMemo, useState, useEffect, useContext, useSyncExternalStore, useRef, Suspense, useId, useCallback, Component } from 'react';
4
+ import { ThemeProvider, useTheme } from 'next-themes';
5
+ import { DEFAULT_CLIENT_OPTIONS, createReaderStore, createLocalStorageAdapter, TEXT_SIZES, LEADINGS, MEASURES, FONT_FAMILIES, exportBackup, highlightsToMarkdown, requestPersistentStorage, buildTextIndex, resolveAnchor, spanToRange, describeSpan, paintHighlights, expandSpanToSentences, findBlock, BLOCK_ATTR, supportsHighlightApi, highlightToMarkdown, quoteToMarkdown, importBackup, ensureBlockIds, collectToc, IGNORE_ATTR, offsetFromPoint, highlightAtOffset, sentenceSpanAt, selectionSpan } from 'zreader';
6
+ import { cn } from 'cn';
7
+ import { Tooltip as Tooltip$1, Dialog as Dialog$1, Tabs as Tabs$1, Slot, ScrollArea as ScrollArea$1, ToggleGroup as ToggleGroup$1, Label as Label$1, DropdownMenu as DropdownMenu$1, Separator, Popover as Popover$1, Switch as Switch$1, Progress as Progress$1, Toggle as Toggle$1 } from 'radix-ui';
8
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
+ import { CheckCircle2Icon, SearchIcon, Trash2Icon, CopyIcon, QuoteIcon, EllipsisIcon, DownloadIcon, XIcon, TextSelectIcon, MessageSquarePlusIcon, RotateCcwIcon, HardDriveIcon, UploadIcon, ChevronLeftIcon, ListIcon, BookmarkIcon, HighlighterIcon, TypeIcon, NotebookPenIcon, CheckIcon, ArrowLeftIcon, ArrowRightIcon, BookOpenIcon, CircleIcon, LightbulbIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon, InfoIcon, CircleCheckIcon, GripVerticalIcon } from 'lucide-react';
10
+ import { toast, Toaster as Toaster$1 } from 'sonner';
11
+ import * as ResizablePrimitive from 'react-resizable-panels';
12
+ import { usePanelRef, useDefaultLayout } from 'react-resizable-panels';
13
+ import { cva } from 'class-variance-authority';
14
+ import { Drawer as Drawer$1 } from 'vaul';
15
+
16
+ // src/react/context.tsx
17
+ function TooltipProvider({
18
+ delayDuration = 0,
19
+ ...props
20
+ }) {
21
+ return /* @__PURE__ */ jsx(
22
+ Tooltip$1.Provider,
23
+ {
24
+ "data-slot": "tooltip-provider",
25
+ delayDuration,
26
+ ...props
27
+ }
28
+ );
29
+ }
30
+ function Tooltip({
31
+ ...props
32
+ }) {
33
+ return /* @__PURE__ */ jsx(Tooltip$1.Root, { "data-slot": "tooltip", ...props });
34
+ }
35
+ function TooltipTrigger({
36
+ ...props
37
+ }) {
38
+ return /* @__PURE__ */ jsx(Tooltip$1.Trigger, { "data-slot": "tooltip-trigger", ...props });
39
+ }
40
+ function TooltipContent({
41
+ className,
42
+ sideOffset = 0,
43
+ children,
44
+ ...props
45
+ }) {
46
+ return /* @__PURE__ */ jsx(Tooltip$1.Portal, { children: /* @__PURE__ */ jsxs(
47
+ Tooltip$1.Content,
48
+ {
49
+ "data-slot": "tooltip-content",
50
+ sideOffset,
51
+ className: cn(
52
+ "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
53
+ className
54
+ ),
55
+ ...props,
56
+ children: [
57
+ children,
58
+ /* @__PURE__ */ jsx(Tooltip$1.Arrow, { className: "z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" })
59
+ ]
60
+ }
61
+ ) });
62
+ }
63
+ var DefaultLink = forwardRef(
64
+ function DefaultLink2(props, ref) {
65
+ return /* @__PURE__ */ jsx("a", { ref, ...props });
66
+ }
67
+ );
68
+ function defaultHref(basePath, bookId, chapterId, hash) {
69
+ const base = basePath.replace(/\/+$/, "");
70
+ const path = chapterId ? `${base}/${encodeURIComponent(bookId)}/${encodeURIComponent(chapterId)}` : `${base}/${encodeURIComponent(bookId)}`;
71
+ return hash ? `${path}#${encodeURIComponent(hash)}` : path;
72
+ }
73
+ var Toaster = ({ ...props }) => {
74
+ const { theme = "system" } = useTheme();
75
+ return /* @__PURE__ */ jsx(
76
+ Toaster$1,
77
+ {
78
+ theme,
79
+ className: "toaster group",
80
+ icons: {
81
+ success: /* @__PURE__ */ jsx(CircleCheckIcon, { className: "size-4" }),
82
+ info: /* @__PURE__ */ jsx(InfoIcon, { className: "size-4" }),
83
+ warning: /* @__PURE__ */ jsx(TriangleAlertIcon, { className: "size-4" }),
84
+ error: /* @__PURE__ */ jsx(OctagonXIcon, { className: "size-4" }),
85
+ loading: /* @__PURE__ */ jsx(Loader2Icon, { className: "size-4 animate-spin" })
86
+ },
87
+ style: {
88
+ "--normal-bg": "var(--popover)",
89
+ "--normal-text": "var(--popover-foreground)",
90
+ "--normal-border": "var(--border)",
91
+ "--border-radius": "var(--radius)"
92
+ },
93
+ ...props
94
+ }
95
+ );
96
+ };
97
+ function isPlainObject(x) {
98
+ if (typeof x !== "object" || x === null || Array.isArray(x)) return false;
99
+ const proto = Object.getPrototypeOf(x);
100
+ return proto === Object.prototype || proto === null;
101
+ }
102
+ function mergeOptions(base, ...overrides) {
103
+ const merge = (a, b) => {
104
+ if (b === void 0 || b === null) return a;
105
+ if (isPlainObject(a) && isPlainObject(b)) {
106
+ const out = { ...a };
107
+ for (const [k, v] of Object.entries(b)) out[k] = merge(a[k], v);
108
+ return out;
109
+ }
110
+ return b;
111
+ };
112
+ return overrides.reduce(merge, base);
113
+ }
114
+ var ReaderContext = createContext(null);
115
+ function ReaderProvider({
116
+ options,
117
+ overrides,
118
+ adapter,
119
+ store: external,
120
+ onStorageError,
121
+ manageTheme = true,
122
+ toaster = true,
123
+ books,
124
+ Link,
125
+ hrefFor,
126
+ headerSlot,
127
+ children
128
+ }) {
129
+ const key = JSON.stringify([options ?? null, overrides ?? null]);
130
+ const resolved = useMemo(
131
+ () => mergeOptions(DEFAULT_CLIENT_OPTIONS, options, overrides),
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
133
+ [key]
134
+ );
135
+ const [store] = useState(
136
+ () => external ?? createReaderStore({ adapter: adapter ?? createLocalStorageAdapter(), options: resolved, onError: onStorageError })
137
+ );
138
+ useEffect(() => store.connect(), [store]);
139
+ const library = useMemo(() => {
140
+ const list = books ?? [];
141
+ const byId = new Map(list.map((b) => [b.id, b]));
142
+ return {
143
+ books: list,
144
+ getBook: (id) => byId.get(id),
145
+ Link: Link ?? DefaultLink,
146
+ href: hrefFor ?? ((b, c, h) => defaultHref(resolved.basePath, b, c, h)),
147
+ headerSlot
148
+ };
149
+ }, [books, Link, hrefFor, headerSlot, resolved.basePath]);
150
+ const value = useMemo(() => ({ store, options: resolved, library }), [store, resolved, library]);
151
+ const tree = /* @__PURE__ */ jsx(ReaderContext.Provider, { value, children: /* @__PURE__ */ jsxs(TooltipProvider, { delayDuration: 400, children: [
152
+ children,
153
+ toaster && /* @__PURE__ */ jsx(Toaster, { position: "top-center" })
154
+ ] }) });
155
+ if (!manageTheme) return tree;
156
+ return /* @__PURE__ */ jsx(
157
+ ThemeProvider,
158
+ {
159
+ attribute: "class",
160
+ themes: resolved.themes.filter((t) => t !== "system"),
161
+ enableSystem: resolved.themes.includes("system"),
162
+ defaultTheme: resolved.defaultTheme,
163
+ storageKey: `${resolved.storagePrefix}:v1:theme`,
164
+ disableTransitionOnChange: true,
165
+ children: tree
166
+ }
167
+ );
168
+ }
169
+ function useReader() {
170
+ const ctx = useContext(ReaderContext);
171
+ if (!ctx) throw new Error("zreader: wrap your tree in <ReaderProvider> (or <ReaderRoot>).");
172
+ return ctx;
173
+ }
174
+ function useLibrary() {
175
+ return useReader().library;
176
+ }
177
+ function useBook(book) {
178
+ const { getBook, books } = useLibrary();
179
+ if (typeof book !== "string") return book;
180
+ const found = getBook(book);
181
+ if (!found) {
182
+ throw new Error(
183
+ books.length ? `zreader: no book with id "${book}" in <ReaderProvider books>. Known: ${books.map((b) => b.id).join(", ")}.` : `zreader: <\u2026 book="${book}"> needs the catalogue. Pass books to <ReaderProvider books={\u2026}> (or use <ReaderRoot>, which loads them).`
184
+ );
185
+ }
186
+ return found;
187
+ }
188
+ function useReaderStore() {
189
+ return useReader().store;
190
+ }
191
+ function useCell(cell, serverValue) {
192
+ return useSyncExternalStore(cell.subscribe, cell.get, () => serverValue);
193
+ }
194
+ var noopSubscribe = () => () => {
195
+ };
196
+ function useHydrated() {
197
+ return useSyncExternalStore(
198
+ noopSubscribe,
199
+ () => true,
200
+ () => false
201
+ );
202
+ }
203
+ function useSettings() {
204
+ const store = useReaderStore();
205
+ const settings = useCell(store.settings(), store.server.settings);
206
+ return [settings, store.updateSettings];
207
+ }
208
+ function useHighlights(bookId, chapterId) {
209
+ const store = useReaderStore();
210
+ const all = useCell(store.highlights(bookId), store.server.list);
211
+ return useMemo(() => chapterId ? all.filter((h) => h.chapterId === chapterId) : all, [all, chapterId]);
212
+ }
213
+ function useBookmarks(bookId) {
214
+ const store = useReaderStore();
215
+ return useCell(store.bookmarks(bookId), store.server.list);
216
+ }
217
+ function useProgress(bookId) {
218
+ const store = useReaderStore();
219
+ return useCell(store.progress(bookId), store.server.progress(bookId));
220
+ }
221
+ function usePad(scope) {
222
+ const store = useReaderStore();
223
+ return useCell(store.pad(scope), store.server.pad);
224
+ }
225
+ function useMediaQuery(query, serverValue = false) {
226
+ return useSyncExternalStore(
227
+ (cb) => {
228
+ if (typeof window === "undefined" || !window.matchMedia) return () => {
229
+ };
230
+ const mql = window.matchMedia(query);
231
+ mql.addEventListener("change", cb);
232
+ return () => mql.removeEventListener("change", cb);
233
+ },
234
+ () => typeof window !== "undefined" && window.matchMedia ? window.matchMedia(query).matches : serverValue,
235
+ () => serverValue
236
+ );
237
+ }
238
+ function useIsDesktop() {
239
+ return useMediaQuery("(min-width: 1024px)", false);
240
+ }
241
+ var buttonVariants = cva(
242
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
243
+ {
244
+ variants: {
245
+ variant: {
246
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
247
+ destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
248
+ outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
249
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
250
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
251
+ link: "text-primary underline-offset-4 hover:underline"
252
+ },
253
+ size: {
254
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
255
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
256
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
257
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
258
+ icon: "size-9",
259
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
260
+ "icon-sm": "size-8",
261
+ "icon-lg": "size-10"
262
+ }
263
+ },
264
+ defaultVariants: {
265
+ variant: "default",
266
+ size: "default"
267
+ }
268
+ }
269
+ );
270
+ function Button({
271
+ className,
272
+ variant = "default",
273
+ size = "default",
274
+ asChild = false,
275
+ ...props
276
+ }) {
277
+ const Comp = asChild ? Slot.Root : "button";
278
+ return /* @__PURE__ */ jsx(
279
+ Comp,
280
+ {
281
+ "data-slot": "button",
282
+ "data-variant": variant,
283
+ "data-size": size,
284
+ className: cn(buttonVariants({ variant, size, className })),
285
+ ...props
286
+ }
287
+ );
288
+ }
289
+ function Drawer({
290
+ ...props
291
+ }) {
292
+ return /* @__PURE__ */ jsx(Drawer$1.Root, { "data-slot": "drawer", ...props });
293
+ }
294
+ function DrawerPortal({
295
+ ...props
296
+ }) {
297
+ return /* @__PURE__ */ jsx(Drawer$1.Portal, { "data-slot": "drawer-portal", ...props });
298
+ }
299
+ function DrawerOverlay({
300
+ className,
301
+ ...props
302
+ }) {
303
+ return /* @__PURE__ */ jsx(
304
+ Drawer$1.Overlay,
305
+ {
306
+ "data-slot": "drawer-overlay",
307
+ className: cn(
308
+ "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
309
+ className
310
+ ),
311
+ ...props
312
+ }
313
+ );
314
+ }
315
+ function DrawerContent({
316
+ className,
317
+ children,
318
+ ...props
319
+ }) {
320
+ return /* @__PURE__ */ jsxs(DrawerPortal, { "data-slot": "drawer-portal", children: [
321
+ /* @__PURE__ */ jsx(DrawerOverlay, {}),
322
+ /* @__PURE__ */ jsxs(
323
+ Drawer$1.Content,
324
+ {
325
+ "data-slot": "drawer-content",
326
+ className: cn(
327
+ "group/drawer-content fixed z-50 flex h-auto flex-col bg-background",
328
+ "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
329
+ "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
330
+ "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
331
+ "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
332
+ className
333
+ ),
334
+ ...props,
335
+ children: [
336
+ /* @__PURE__ */ jsx("div", { className: "mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block" }),
337
+ children
338
+ ]
339
+ }
340
+ )
341
+ ] });
342
+ }
343
+ function DrawerHeader({ className, ...props }) {
344
+ return /* @__PURE__ */ jsx(
345
+ "div",
346
+ {
347
+ "data-slot": "drawer-header",
348
+ className: cn(
349
+ "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
350
+ className
351
+ ),
352
+ ...props
353
+ }
354
+ );
355
+ }
356
+ function DrawerFooter({ className, ...props }) {
357
+ return /* @__PURE__ */ jsx(
358
+ "div",
359
+ {
360
+ "data-slot": "drawer-footer",
361
+ className: cn("mt-auto flex flex-col gap-2 p-4", className),
362
+ ...props
363
+ }
364
+ );
365
+ }
366
+ function DrawerTitle({
367
+ className,
368
+ ...props
369
+ }) {
370
+ return /* @__PURE__ */ jsx(
371
+ Drawer$1.Title,
372
+ {
373
+ "data-slot": "drawer-title",
374
+ className: cn("font-semibold text-foreground", className),
375
+ ...props
376
+ }
377
+ );
378
+ }
379
+ function DrawerDescription({
380
+ className,
381
+ ...props
382
+ }) {
383
+ return /* @__PURE__ */ jsx(
384
+ Drawer$1.Description,
385
+ {
386
+ "data-slot": "drawer-description",
387
+ className: cn("text-sm text-muted-foreground", className),
388
+ ...props
389
+ }
390
+ );
391
+ }
392
+ function Progress({
393
+ className,
394
+ value,
395
+ ...props
396
+ }) {
397
+ return /* @__PURE__ */ jsx(
398
+ Progress$1.Root,
399
+ {
400
+ "data-slot": "progress",
401
+ className: cn(
402
+ "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
403
+ className
404
+ ),
405
+ ...props,
406
+ children: /* @__PURE__ */ jsx(
407
+ Progress$1.Indicator,
408
+ {
409
+ "data-slot": "progress-indicator",
410
+ className: "h-full w-full flex-1 bg-primary transition-all",
411
+ style: { transform: `translateX(-${100 - (value || 0)}%)` }
412
+ }
413
+ )
414
+ }
415
+ );
416
+ }
417
+ function ResizablePanelGroup({
418
+ className,
419
+ ...props
420
+ }) {
421
+ return /* @__PURE__ */ jsx(
422
+ ResizablePrimitive.Group,
423
+ {
424
+ "data-slot": "resizable-panel-group",
425
+ className: cn(
426
+ "flex h-full w-full aria-[orientation=vertical]:flex-col",
427
+ className
428
+ ),
429
+ ...props
430
+ }
431
+ );
432
+ }
433
+ function ResizablePanel({ ...props }) {
434
+ return /* @__PURE__ */ jsx(ResizablePrimitive.Panel, { "data-slot": "resizable-panel", ...props });
435
+ }
436
+ function ResizableHandle({
437
+ withHandle,
438
+ className,
439
+ ...props
440
+ }) {
441
+ return /* @__PURE__ */ jsx(
442
+ ResizablePrimitive.Separator,
443
+ {
444
+ "data-slot": "resizable-handle",
445
+ className: cn(
446
+ "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90",
447
+ className
448
+ ),
449
+ ...props,
450
+ children: withHandle && /* @__PURE__ */ jsx("div", { className: "z-10 flex h-4 w-3 items-center justify-center rounded-xs border bg-border", children: /* @__PURE__ */ jsx(GripVerticalIcon, { className: "size-2.5" }) })
451
+ }
452
+ );
453
+ }
454
+ var toggleVariants = cva(
455
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
456
+ {
457
+ variants: {
458
+ variant: {
459
+ default: "bg-transparent",
460
+ outline: "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground"
461
+ },
462
+ size: {
463
+ default: "h-9 min-w-9 px-2",
464
+ sm: "h-8 min-w-8 px-1.5",
465
+ lg: "h-10 min-w-10 px-2.5"
466
+ }
467
+ },
468
+ defaultVariants: {
469
+ variant: "default",
470
+ size: "default"
471
+ }
472
+ }
473
+ );
474
+ function Toggle({
475
+ className,
476
+ variant,
477
+ size,
478
+ ...props
479
+ }) {
480
+ return /* @__PURE__ */ jsx(
481
+ Toggle$1.Root,
482
+ {
483
+ "data-slot": "toggle",
484
+ className: cn(toggleVariants({ variant, size, className })),
485
+ ...props
486
+ }
487
+ );
488
+ }
489
+ var badgeVariants = cva(
490
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
491
+ {
492
+ variants: {
493
+ variant: {
494
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
495
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
496
+ destructive: "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
497
+ outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
498
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
499
+ link: "text-primary underline-offset-4 [a&]:hover:underline"
500
+ }
501
+ },
502
+ defaultVariants: {
503
+ variant: "default"
504
+ }
505
+ }
506
+ );
507
+ function Badge({
508
+ className,
509
+ variant = "default",
510
+ asChild = false,
511
+ ...props
512
+ }) {
513
+ const Comp = asChild ? Slot.Root : "span";
514
+ return /* @__PURE__ */ jsx(
515
+ Comp,
516
+ {
517
+ "data-slot": "badge",
518
+ "data-variant": variant,
519
+ className: cn(badgeVariants({ variant }), className),
520
+ ...props
521
+ }
522
+ );
523
+ }
524
+ function Input({ className, type, ...props }) {
525
+ return /* @__PURE__ */ jsx(
526
+ "input",
527
+ {
528
+ type,
529
+ "data-slot": "input",
530
+ className: cn(
531
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
532
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
533
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
534
+ className
535
+ ),
536
+ ...props
537
+ }
538
+ );
539
+ }
540
+ function ScrollArea({
541
+ className,
542
+ children,
543
+ ...props
544
+ }) {
545
+ return /* @__PURE__ */ jsxs(
546
+ ScrollArea$1.Root,
547
+ {
548
+ "data-slot": "scroll-area",
549
+ className: cn("relative", className),
550
+ ...props,
551
+ children: [
552
+ /* @__PURE__ */ jsx(
553
+ ScrollArea$1.Viewport,
554
+ {
555
+ "data-slot": "scroll-area-viewport",
556
+ className: "size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1",
557
+ children
558
+ }
559
+ ),
560
+ /* @__PURE__ */ jsx(ScrollBar, {}),
561
+ /* @__PURE__ */ jsx(ScrollArea$1.Corner, {})
562
+ ]
563
+ }
564
+ );
565
+ }
566
+ function ScrollBar({
567
+ className,
568
+ orientation = "vertical",
569
+ ...props
570
+ }) {
571
+ return /* @__PURE__ */ jsx(
572
+ ScrollArea$1.ScrollAreaScrollbar,
573
+ {
574
+ "data-slot": "scroll-area-scrollbar",
575
+ orientation,
576
+ className: cn(
577
+ "flex touch-none p-px transition-colors select-none",
578
+ orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
579
+ orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent",
580
+ className
581
+ ),
582
+ ...props,
583
+ children: /* @__PURE__ */ jsx(
584
+ ScrollArea$1.ScrollAreaThumb,
585
+ {
586
+ "data-slot": "scroll-area-thumb",
587
+ className: "relative flex-1 rounded-full bg-border"
588
+ }
589
+ )
590
+ }
591
+ );
592
+ }
593
+ function Sheet({ ...props }) {
594
+ return /* @__PURE__ */ jsx(Dialog$1.Root, { "data-slot": "sheet", ...props });
595
+ }
596
+ function SheetPortal({
597
+ ...props
598
+ }) {
599
+ return /* @__PURE__ */ jsx(Dialog$1.Portal, { "data-slot": "sheet-portal", ...props });
600
+ }
601
+ function SheetOverlay({
602
+ className,
603
+ ...props
604
+ }) {
605
+ return /* @__PURE__ */ jsx(
606
+ Dialog$1.Overlay,
607
+ {
608
+ "data-slot": "sheet-overlay",
609
+ className: cn(
610
+ "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
611
+ className
612
+ ),
613
+ ...props
614
+ }
615
+ );
616
+ }
617
+ function SheetContent({
618
+ className,
619
+ children,
620
+ side = "right",
621
+ showCloseButton = true,
622
+ ...props
623
+ }) {
624
+ return /* @__PURE__ */ jsxs(SheetPortal, { children: [
625
+ /* @__PURE__ */ jsx(SheetOverlay, {}),
626
+ /* @__PURE__ */ jsxs(
627
+ Dialog$1.Content,
628
+ {
629
+ "data-slot": "sheet-content",
630
+ className: cn(
631
+ "fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
632
+ side === "right" && "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
633
+ side === "left" && "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
634
+ side === "top" && "inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
635
+ side === "bottom" && "inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
636
+ className
637
+ ),
638
+ ...props,
639
+ children: [
640
+ children,
641
+ showCloseButton && /* @__PURE__ */ jsxs(Dialog$1.Close, { className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary", children: [
642
+ /* @__PURE__ */ jsx(XIcon, { className: "size-4" }),
643
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
644
+ ] })
645
+ ]
646
+ }
647
+ )
648
+ ] });
649
+ }
650
+ function SheetHeader({ className, ...props }) {
651
+ return /* @__PURE__ */ jsx(
652
+ "div",
653
+ {
654
+ "data-slot": "sheet-header",
655
+ className: cn("flex flex-col gap-1.5 p-4", className),
656
+ ...props
657
+ }
658
+ );
659
+ }
660
+ function SheetFooter({ className, ...props }) {
661
+ return /* @__PURE__ */ jsx(
662
+ "div",
663
+ {
664
+ "data-slot": "sheet-footer",
665
+ className: cn("mt-auto flex flex-col gap-2 p-4", className),
666
+ ...props
667
+ }
668
+ );
669
+ }
670
+ function SheetTitle({
671
+ className,
672
+ ...props
673
+ }) {
674
+ return /* @__PURE__ */ jsx(
675
+ Dialog$1.Title,
676
+ {
677
+ "data-slot": "sheet-title",
678
+ className: cn("font-semibold text-foreground", className),
679
+ ...props
680
+ }
681
+ );
682
+ }
683
+ function SheetDescription({
684
+ className,
685
+ ...props
686
+ }) {
687
+ return /* @__PURE__ */ jsx(
688
+ Dialog$1.Description,
689
+ {
690
+ "data-slot": "sheet-description",
691
+ className: cn("text-sm text-muted-foreground", className),
692
+ ...props
693
+ }
694
+ );
695
+ }
696
+ function Tabs({
697
+ className,
698
+ orientation = "horizontal",
699
+ ...props
700
+ }) {
701
+ return /* @__PURE__ */ jsx(
702
+ Tabs$1.Root,
703
+ {
704
+ "data-slot": "tabs",
705
+ "data-orientation": orientation,
706
+ orientation,
707
+ className: cn(
708
+ "group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
709
+ className
710
+ ),
711
+ ...props
712
+ }
713
+ );
714
+ }
715
+ var tabsListVariants = cva(
716
+ "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
717
+ {
718
+ variants: {
719
+ variant: {
720
+ default: "bg-muted",
721
+ line: "gap-1 bg-transparent"
722
+ }
723
+ },
724
+ defaultVariants: {
725
+ variant: "default"
726
+ }
727
+ }
728
+ );
729
+ function TabsList({
730
+ className,
731
+ variant = "default",
732
+ ...props
733
+ }) {
734
+ return /* @__PURE__ */ jsx(
735
+ Tabs$1.List,
736
+ {
737
+ "data-slot": "tabs-list",
738
+ "data-variant": variant,
739
+ className: cn(tabsListVariants({ variant }), className),
740
+ ...props
741
+ }
742
+ );
743
+ }
744
+ function TabsTrigger({
745
+ className,
746
+ ...props
747
+ }) {
748
+ return /* @__PURE__ */ jsx(
749
+ Tabs$1.Trigger,
750
+ {
751
+ "data-slot": "tabs-trigger",
752
+ className: cn(
753
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
754
+ "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
755
+ "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground",
756
+ "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
757
+ className
758
+ ),
759
+ ...props
760
+ }
761
+ );
762
+ }
763
+ function TabsContent({
764
+ className,
765
+ ...props
766
+ }) {
767
+ return /* @__PURE__ */ jsx(
768
+ Tabs$1.Content,
769
+ {
770
+ "data-slot": "tabs-content",
771
+ className: cn("flex-1 outline-none", className),
772
+ ...props
773
+ }
774
+ );
775
+ }
776
+
777
+ // src/react/parts.ts
778
+ function groupByPart(chapters) {
779
+ const groups = [];
780
+ chapters.forEach((chapter, index) => {
781
+ const part = chapter.part ?? null;
782
+ const last = groups[groups.length - 1];
783
+ if (last && last.part === part) last.items.push({ chapter, index });
784
+ else groups.push({ part, items: [{ chapter, index }] });
785
+ });
786
+ return groups;
787
+ }
788
+ function chapterMarker(chapter, index) {
789
+ return chapter.number ?? (chapter.part || chapter.label ? "" : String(index + 1));
790
+ }
791
+
792
+ // src/react/tokens.ts
793
+ var TEXT_SIZE_CLASS = {
794
+ sm: "prose-sm",
795
+ base: "prose-base",
796
+ lg: "prose-lg",
797
+ xl: "prose-xl",
798
+ "2xl": "prose-2xl"
799
+ };
800
+ var LEADING_CLASS = {
801
+ tight: "prose-p:leading-snug prose-li:leading-snug",
802
+ normal: "prose-p:leading-normal prose-li:leading-normal",
803
+ relaxed: "prose-p:leading-relaxed prose-li:leading-relaxed"
804
+ };
805
+ var MEASURE_CLASS = {
806
+ narrow: "max-w-[58ch]",
807
+ normal: "max-w-[68ch]",
808
+ wide: "max-w-[82ch]"
809
+ };
810
+ var FONT_CLASS = {
811
+ serif: "font-serif",
812
+ sans: "font-sans",
813
+ mono: "font-mono"
814
+ };
815
+ var SWATCH_CLASS = {
816
+ yellow: "bg-yellow-300 dark:bg-yellow-400/70",
817
+ green: "bg-green-300 dark:bg-green-400/60",
818
+ blue: "bg-sky-300 dark:bg-sky-400/60",
819
+ pink: "bg-pink-300 dark:bg-pink-400/60",
820
+ purple: "bg-violet-300 dark:bg-violet-400/60",
821
+ orange: "bg-orange-300 dark:bg-orange-400/60"
822
+ };
823
+ var QUOTE_BORDER_CLASS = {
824
+ yellow: "border-yellow-400",
825
+ green: "border-green-400",
826
+ blue: "border-sky-400",
827
+ pink: "border-pink-400",
828
+ purple: "border-violet-400",
829
+ orange: "border-orange-400"
830
+ };
831
+ function swatchClass(color) {
832
+ return SWATCH_CLASS[color] ?? SWATCH_CLASS.yellow;
833
+ }
834
+ function quoteBorderClass(color) {
835
+ return QUOTE_BORDER_CLASS[color] ?? QUOTE_BORDER_CLASS.yellow;
836
+ }
837
+ var PROSE_CLASS = "prose max-w-none text-foreground prose-headings:text-foreground prose-headings:scroll-mt-20 prose-p:text-foreground prose-li:text-foreground prose-strong:text-foreground prose-a:text-primary prose-a:underline-offset-4 prose-blockquote:border-l-border prose-blockquote:text-muted-foreground prose-code:text-foreground prose-pre:bg-muted prose-pre:text-foreground prose-hr:border-border prose-th:text-foreground prose-td:text-foreground prose-figcaption:text-muted-foreground prose-img:rounded-lg prose-table:block prose-table:max-w-full prose-table:overflow-x-auto prose-th:align-bottom prose-td:align-top";
838
+ var dateFmt = (iso) => new Date(iso).toLocaleDateString(void 0, { month: "short", day: "numeric" });
839
+ function ContentsSheet(p) {
840
+ const { book, chapterId, Link } = p;
841
+ const store = useReaderStore();
842
+ const highlights = useHighlights(book.id);
843
+ const bookmarks = useBookmarks(book.id);
844
+ const progress = useProgress(book.id);
845
+ const [query, setQuery] = useState("");
846
+ const titles = useMemo(() => new Map(book.chapters.map((c) => [c.id, c.title])), [book.chapters]);
847
+ const order = useMemo(() => new Map(book.chapters.map((c, i) => [c.id, i])), [book.chapters]);
848
+ const grouped = useMemo(() => {
849
+ const q = query.trim().toLowerCase();
850
+ const groups = /* @__PURE__ */ new Map();
851
+ highlights.filter((h) => !q || h.anchor.exact.toLowerCase().includes(q) || h.note?.toLowerCase().includes(q)).sort((a, b) => (order.get(a.chapterId) ?? 0) - (order.get(b.chapterId) ?? 0) || a.anchor.start - b.anchor.start).forEach((h) => groups.set(h.chapterId, [...groups.get(h.chapterId) ?? [], h]));
852
+ return groups;
853
+ }, [highlights, query, order]);
854
+ const sortedBookmarks = useMemo(
855
+ () => [...bookmarks].sort((a, b) => (order.get(a.chapterId) ?? 0) - (order.get(b.chapterId) ?? 0)),
856
+ [bookmarks, order]
857
+ );
858
+ const close = () => p.onOpenChange(false);
859
+ const jumpOr = (targetChapter, inPage) => (e) => {
860
+ if (targetChapter !== chapterId) return close();
861
+ e.preventDefault();
862
+ close();
863
+ inPage();
864
+ };
865
+ return /* @__PURE__ */ jsx(Sheet, { open: p.open, onOpenChange: p.onOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { side: "left", className: "w-[88vw] gap-0 p-0 sm:max-w-sm", children: [
866
+ /* @__PURE__ */ jsxs(SheetHeader, { className: "border-b", children: [
867
+ /* @__PURE__ */ jsx(SheetTitle, { className: "line-clamp-2", children: book.title }),
868
+ book.authors.length > 0 && /* @__PURE__ */ jsx(SheetDescription, { children: book.authors.join(", ") })
869
+ ] }),
870
+ /* @__PURE__ */ jsxs(Tabs, { value: p.tab, onValueChange: (v) => p.onTabChange(v), className: "min-h-0 flex-1 gap-0", children: [
871
+ /* @__PURE__ */ jsx("div", { className: "px-4 pt-3", children: /* @__PURE__ */ jsxs(TabsList, { className: "w-full", children: [
872
+ /* @__PURE__ */ jsx(TabsTrigger, { value: "contents", children: "Contents" }),
873
+ /* @__PURE__ */ jsxs(TabsTrigger, { value: "highlights", children: [
874
+ "Highlights",
875
+ highlights.length > 0 && /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: highlights.length })
876
+ ] }),
877
+ /* @__PURE__ */ jsxs(TabsTrigger, { value: "bookmarks", children: [
878
+ "Bookmarks",
879
+ bookmarks.length > 0 && /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: bookmarks.length })
880
+ ] })
881
+ ] }) }),
882
+ /* @__PURE__ */ jsx(TabsContent, { value: "contents", className: "min-h-0", children: /* @__PURE__ */ jsx(ScrollArea, { className: "h-full", children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3 p-2", children: groupByPart(book.chapters).map((group) => /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-0.5", children: [
883
+ group.part && /* @__PURE__ */ jsx("h3", { className: "px-3 pt-2 pb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase", children: group.part }),
884
+ /* @__PURE__ */ jsx("ol", { className: "flex flex-col gap-0.5", children: group.items.map(({ chapter: c, index: i }) => {
885
+ const pr = progress.chapters[c.id];
886
+ const current = c.id === chapterId;
887
+ return /* @__PURE__ */ jsxs("li", { children: [
888
+ /* @__PURE__ */ jsx(Button, { asChild: true, variant: current ? "secondary" : "ghost", className: "h-auto w-full justify-start gap-3 py-2 text-left whitespace-normal", children: /* @__PURE__ */ jsxs(Link, { href: p.hrefFor(c.id), "aria-current": current ? "page" : void 0, onClick: close, children: [
889
+ /* @__PURE__ */ jsx("span", { className: "w-5 shrink-0 text-xs text-muted-foreground tabular-nums", children: chapterMarker(c, i) }),
890
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: c.title }),
891
+ pr?.completed ? /* @__PURE__ */ jsx(CheckCircle2Icon, { className: "text-primary", "aria-label": "Completed" }) : pr ? /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground tabular-nums", children: [
892
+ Math.round(pr.percent * 100),
893
+ "%"
894
+ ] }) : null
895
+ ] }) }),
896
+ current && p.toc.length > 0 && /* @__PURE__ */ jsx("ol", { className: "my-1 ml-8 flex flex-col border-l", children: p.toc.map((t) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
897
+ Button,
898
+ {
899
+ asChild: true,
900
+ variant: "link",
901
+ size: "sm",
902
+ className: cn("h-auto w-full justify-start py-1.5 text-left whitespace-normal text-muted-foreground", t.depth > 2 && "pl-6"),
903
+ children: /* @__PURE__ */ jsx("a", { href: `#${t.id}`, onClick: jumpOr(chapterId, () => p.onJumpHeading(t.id)), children: t.text })
904
+ }
905
+ ) }, t.id)) })
906
+ ] }, c.id);
907
+ }) })
908
+ ] }, group.part ?? "_")) }) }) }),
909
+ /* @__PURE__ */ jsxs(TabsContent, { value: "highlights", className: "flex min-h-0 flex-col", children: [
910
+ highlights.length > 3 && /* @__PURE__ */ jsx("div", { className: "px-4 pt-3", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
911
+ /* @__PURE__ */ jsx(SearchIcon, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" }),
912
+ /* @__PURE__ */ jsx(Input, { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search highlights and notes", "aria-label": "Search highlights", className: "pl-8" })
913
+ ] }) }),
914
+ /* @__PURE__ */ jsx(ScrollArea, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 p-4", children: [
915
+ grouped.size === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: highlights.length ? "No matches." : "Select text \u2014 or turn on highlight mode and tap a sentence \u2014 to highlight it." }),
916
+ [...grouped].map(([cid, list]) => /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-2", children: [
917
+ /* @__PURE__ */ jsx("h3", { className: "text-xs font-medium tracking-wide text-muted-foreground uppercase", children: titles.get(cid) ?? cid }),
918
+ /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-2", children: list.map((h) => /* @__PURE__ */ jsxs("li", { className: "group flex items-start gap-1", children: [
919
+ /* @__PURE__ */ jsxs(
920
+ Link,
921
+ {
922
+ href: p.hrefFor(cid, `hl-${h.id}`),
923
+ onClick: jumpOr(cid, () => p.onJumpHighlight(h.id)),
924
+ className: cn("flex-1 rounded-md border-l-4 bg-muted/40 px-3 py-2 text-sm transition-colors hover:bg-muted", quoteBorderClass(h.color)),
925
+ children: [
926
+ /* @__PURE__ */ jsx("span", { className: "line-clamp-3 font-serif", children: h.anchor.exact }),
927
+ h.note && /* @__PURE__ */ jsx("span", { className: "mt-1 line-clamp-2 block text-muted-foreground", children: h.note }),
928
+ /* @__PURE__ */ jsxs("span", { className: "mt-1 flex items-center gap-2 text-xs text-muted-foreground", children: [
929
+ dateFmt(h.updatedAt),
930
+ cid === chapterId && p.orphanedIds.has(h.id) && /* @__PURE__ */ jsx(Badge, { variant: "outline", title: "The passage changed and this highlight could not be placed", children: "Unplaced" })
931
+ ] })
932
+ ]
933
+ }
934
+ ),
935
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Delete highlight", onClick: () => store.removeHighlight(book.id, h.id), children: /* @__PURE__ */ jsx(Trash2Icon, {}) })
936
+ ] }, h.id)) })
937
+ ] }, cid))
938
+ ] }) })
939
+ ] }),
940
+ /* @__PURE__ */ jsx(TabsContent, { value: "bookmarks", className: "min-h-0", children: /* @__PURE__ */ jsx(ScrollArea, { className: "h-full", children: /* @__PURE__ */ jsxs("ul", { className: "flex flex-col gap-2 p-4", children: [
941
+ sortedBookmarks.length === 0 && /* @__PURE__ */ jsx("li", { className: "text-sm text-muted-foreground", children: "Use the bookmark button to save your place." }),
942
+ sortedBookmarks.map((b) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-1", children: [
943
+ /* @__PURE__ */ jsxs(
944
+ Link,
945
+ {
946
+ href: p.hrefFor(b.chapterId, b.blockId ? `b-${b.blockId}` : void 0),
947
+ onClick: b.blockId ? jumpOr(b.chapterId, () => p.onJumpBlock(b.blockId)) : close,
948
+ className: "flex-1 rounded-md bg-muted/40 px-3 py-2 text-sm transition-colors hover:bg-muted",
949
+ children: [
950
+ /* @__PURE__ */ jsx("span", { className: "block font-medium", children: b.label ?? titles.get(b.chapterId) ?? b.chapterId }),
951
+ b.excerpt && /* @__PURE__ */ jsx("span", { className: "mt-0.5 line-clamp-2 block text-muted-foreground", children: b.excerpt }),
952
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block text-xs text-muted-foreground", children: dateFmt(b.createdAt) })
953
+ ]
954
+ }
955
+ ),
956
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Remove bookmark", onClick: () => store.removeBookmark(book.id, b.id), children: /* @__PURE__ */ jsx(Trash2Icon, {}) })
957
+ ] }, b.id))
958
+ ] }) }) })
959
+ ] })
960
+ ] }) });
961
+ }
962
+ function Label({
963
+ className,
964
+ ...props
965
+ }) {
966
+ return /* @__PURE__ */ jsx(
967
+ Label$1.Root,
968
+ {
969
+ "data-slot": "label",
970
+ className: cn(
971
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
972
+ className
973
+ ),
974
+ ...props
975
+ }
976
+ );
977
+ }
978
+ function Textarea({ className, ...props }) {
979
+ return /* @__PURE__ */ jsx(
980
+ "textarea",
981
+ {
982
+ "data-slot": "textarea",
983
+ className: cn(
984
+ "flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
985
+ className
986
+ ),
987
+ ...props
988
+ }
989
+ );
990
+ }
991
+ var ToggleGroupContext = React13.createContext({
992
+ size: "default",
993
+ variant: "default",
994
+ spacing: 0
995
+ });
996
+ function ToggleGroup({
997
+ className,
998
+ variant,
999
+ size,
1000
+ spacing = 0,
1001
+ children,
1002
+ ...props
1003
+ }) {
1004
+ return /* @__PURE__ */ jsx(
1005
+ ToggleGroup$1.Root,
1006
+ {
1007
+ "data-slot": "toggle-group",
1008
+ "data-variant": variant,
1009
+ "data-size": size,
1010
+ "data-spacing": spacing,
1011
+ style: { "--gap": spacing },
1012
+ className: cn(
1013
+ "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
1014
+ className
1015
+ ),
1016
+ ...props,
1017
+ children: /* @__PURE__ */ jsx(ToggleGroupContext.Provider, { value: { variant, size, spacing }, children })
1018
+ }
1019
+ );
1020
+ }
1021
+ function ToggleGroupItem({
1022
+ className,
1023
+ children,
1024
+ variant,
1025
+ size,
1026
+ ...props
1027
+ }) {
1028
+ const context = React13.useContext(ToggleGroupContext);
1029
+ return /* @__PURE__ */ jsx(
1030
+ ToggleGroup$1.Item,
1031
+ {
1032
+ "data-slot": "toggle-group-item",
1033
+ "data-variant": context.variant || variant,
1034
+ "data-size": context.size || size,
1035
+ "data-spacing": context.spacing,
1036
+ className: cn(
1037
+ toggleVariants({
1038
+ variant: context.variant || variant,
1039
+ size: context.size || size
1040
+ }),
1041
+ "w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
1042
+ "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
1043
+ className
1044
+ ),
1045
+ ...props,
1046
+ children
1047
+ }
1048
+ );
1049
+ }
1050
+ function Dialog({
1051
+ ...props
1052
+ }) {
1053
+ return /* @__PURE__ */ jsx(Dialog$1.Root, { "data-slot": "dialog", ...props });
1054
+ }
1055
+ function DialogPortal({
1056
+ ...props
1057
+ }) {
1058
+ return /* @__PURE__ */ jsx(Dialog$1.Portal, { "data-slot": "dialog-portal", ...props });
1059
+ }
1060
+ function DialogOverlay({
1061
+ className,
1062
+ ...props
1063
+ }) {
1064
+ return /* @__PURE__ */ jsx(
1065
+ Dialog$1.Overlay,
1066
+ {
1067
+ "data-slot": "dialog-overlay",
1068
+ className: cn(
1069
+ "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
1070
+ className
1071
+ ),
1072
+ ...props
1073
+ }
1074
+ );
1075
+ }
1076
+ function DialogContent({
1077
+ className,
1078
+ children,
1079
+ showCloseButton = true,
1080
+ ...props
1081
+ }) {
1082
+ return /* @__PURE__ */ jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [
1083
+ /* @__PURE__ */ jsx(DialogOverlay, {}),
1084
+ /* @__PURE__ */ jsxs(
1085
+ Dialog$1.Content,
1086
+ {
1087
+ "data-slot": "dialog-content",
1088
+ className: cn(
1089
+ "fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
1090
+ className
1091
+ ),
1092
+ ...props,
1093
+ children: [
1094
+ children,
1095
+ showCloseButton && /* @__PURE__ */ jsxs(
1096
+ Dialog$1.Close,
1097
+ {
1098
+ "data-slot": "dialog-close",
1099
+ className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
1100
+ children: [
1101
+ /* @__PURE__ */ jsx(XIcon, {}),
1102
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Close" })
1103
+ ]
1104
+ }
1105
+ )
1106
+ ]
1107
+ }
1108
+ )
1109
+ ] });
1110
+ }
1111
+ function DialogHeader({ className, ...props }) {
1112
+ return /* @__PURE__ */ jsx(
1113
+ "div",
1114
+ {
1115
+ "data-slot": "dialog-header",
1116
+ className: cn("flex flex-col gap-2 text-center sm:text-left", className),
1117
+ ...props
1118
+ }
1119
+ );
1120
+ }
1121
+ function DialogFooter({
1122
+ className,
1123
+ showCloseButton = false,
1124
+ children,
1125
+ ...props
1126
+ }) {
1127
+ return /* @__PURE__ */ jsxs(
1128
+ "div",
1129
+ {
1130
+ "data-slot": "dialog-footer",
1131
+ className: cn(
1132
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1133
+ className
1134
+ ),
1135
+ ...props,
1136
+ children: [
1137
+ children,
1138
+ showCloseButton && /* @__PURE__ */ jsx(Dialog$1.Close, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "outline", children: "Close" }) })
1139
+ ]
1140
+ }
1141
+ );
1142
+ }
1143
+ function DialogTitle({
1144
+ className,
1145
+ ...props
1146
+ }) {
1147
+ return /* @__PURE__ */ jsx(
1148
+ Dialog$1.Title,
1149
+ {
1150
+ "data-slot": "dialog-title",
1151
+ className: cn("text-lg leading-none font-semibold", className),
1152
+ ...props
1153
+ }
1154
+ );
1155
+ }
1156
+ function DialogDescription({
1157
+ className,
1158
+ ...props
1159
+ }) {
1160
+ return /* @__PURE__ */ jsx(
1161
+ Dialog$1.Description,
1162
+ {
1163
+ "data-slot": "dialog-description",
1164
+ className: cn("text-sm text-muted-foreground", className),
1165
+ ...props
1166
+ }
1167
+ );
1168
+ }
1169
+ function ResponsivePanel({
1170
+ open,
1171
+ onOpenChange,
1172
+ title,
1173
+ description,
1174
+ desktop = "dialog",
1175
+ hideHeader,
1176
+ footer,
1177
+ className,
1178
+ children
1179
+ }) {
1180
+ const isDesktop = useIsDesktop();
1181
+ const headerClass = hideHeader ? "sr-only" : void 0;
1182
+ if (isDesktop && desktop === "dialog") {
1183
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: cn("sm:max-w-lg", className), children: [
1184
+ /* @__PURE__ */ jsxs(DialogHeader, { className: headerClass, children: [
1185
+ /* @__PURE__ */ jsx(DialogTitle, { children: title }),
1186
+ description ? /* @__PURE__ */ jsx(DialogDescription, { children: description }) : null
1187
+ ] }),
1188
+ children,
1189
+ footer ? /* @__PURE__ */ jsx(DialogFooter, { children: footer }) : null
1190
+ ] }) });
1191
+ }
1192
+ if (isDesktop) {
1193
+ return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs(SheetContent, { side: desktop === "sheet-left" ? "left" : "right", className: cn("w-full gap-0 sm:max-w-md", className), children: [
1194
+ /* @__PURE__ */ jsxs(SheetHeader, { className: headerClass, children: [
1195
+ /* @__PURE__ */ jsx(SheetTitle, { children: title }),
1196
+ description ? /* @__PURE__ */ jsx(SheetDescription, { children: description }) : null
1197
+ ] }),
1198
+ /* @__PURE__ */ jsx(ScrollArea, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx("div", { className: "px-4 pb-4", children }) }),
1199
+ footer ? /* @__PURE__ */ jsx(SheetFooter, { children: footer }) : null
1200
+ ] }) });
1201
+ }
1202
+ return /* @__PURE__ */ jsx(Drawer, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DrawerContent, { className: cn("max-h-[92svh]", className), children: [
1203
+ /* @__PURE__ */ jsxs(DrawerHeader, { className: cn("text-left", headerClass), children: [
1204
+ /* @__PURE__ */ jsx(DrawerTitle, { children: title }),
1205
+ description ? /* @__PURE__ */ jsx(DrawerDescription, { children: description }) : null
1206
+ ] }),
1207
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-4", children }),
1208
+ footer ? /* @__PURE__ */ jsx(DrawerFooter, { className: "pb-[max(1rem,env(safe-area-inset-bottom))]", children: footer }) : null
1209
+ ] }) });
1210
+ }
1211
+ function HighlightPanel({ highlight, colors, focusNote, onColor, onNote, onDelete, onQuote, onCopy, onClose }) {
1212
+ const [note, setNote] = useState(highlight?.note ?? "");
1213
+ const saved = useRef(highlight?.note ?? "");
1214
+ const textRef = useRef(null);
1215
+ const id = highlight?.id;
1216
+ useEffect(() => {
1217
+ setNote(highlight?.note ?? "");
1218
+ saved.current = highlight?.note ?? "";
1219
+ }, [id]);
1220
+ useEffect(() => {
1221
+ if (note === saved.current) return;
1222
+ const t = setTimeout(() => {
1223
+ saved.current = note;
1224
+ onNote(note);
1225
+ }, 400);
1226
+ return () => clearTimeout(t);
1227
+ }, [note, onNote]);
1228
+ useEffect(() => {
1229
+ if (!focusNote || !id) return;
1230
+ const t = setTimeout(() => textRef.current?.focus(), 250);
1231
+ return () => clearTimeout(t);
1232
+ }, [focusNote, id]);
1233
+ const flush = () => {
1234
+ if (note !== saved.current) {
1235
+ saved.current = note;
1236
+ onNote(note);
1237
+ }
1238
+ };
1239
+ const exact = highlight?.anchor.exact ?? "";
1240
+ return /* @__PURE__ */ jsx(
1241
+ ResponsivePanel,
1242
+ {
1243
+ open: !!highlight,
1244
+ onOpenChange: (open) => {
1245
+ if (!open) {
1246
+ flush();
1247
+ onClose();
1248
+ }
1249
+ },
1250
+ title: "Highlight",
1251
+ description: "Change the colour, add a note, or remove it.",
1252
+ footer: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
1253
+ onCopy && /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: onCopy, children: [
1254
+ /* @__PURE__ */ jsx(CopyIcon, {}),
1255
+ " Copy"
1256
+ ] }),
1257
+ onQuote && /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: onQuote, children: [
1258
+ /* @__PURE__ */ jsx(QuoteIcon, {}),
1259
+ " To scratchpad"
1260
+ ] }),
1261
+ /* @__PURE__ */ jsxs(Button, { type: "button", variant: "destructive", onClick: onDelete, children: [
1262
+ /* @__PURE__ */ jsx(Trash2Icon, {}),
1263
+ " Delete"
1264
+ ] })
1265
+ ] }),
1266
+ children: highlight && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
1267
+ /* @__PURE__ */ jsx("blockquote", { className: cn("line-clamp-6 border-l-4 pl-3 font-serif text-sm text-muted-foreground", quoteBorderClass(highlight.color)), children: exact }),
1268
+ /* @__PURE__ */ jsx(
1269
+ ToggleGroup,
1270
+ {
1271
+ type: "single",
1272
+ variant: "outline",
1273
+ value: highlight.color,
1274
+ onValueChange: (v) => v && onColor(v),
1275
+ "aria-label": "Highlight colour",
1276
+ className: "justify-start",
1277
+ children: colors.map((c) => /* @__PURE__ */ jsx(ToggleGroupItem, { value: c, "aria-label": c, children: /* @__PURE__ */ jsx("span", { className: cn("size-4 rounded-full ring-1 ring-foreground/15", swatchClass(c)) }) }, c))
1278
+ }
1279
+ ),
1280
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
1281
+ /* @__PURE__ */ jsx(Label, { htmlFor: `zr-note-${highlight.id}`, children: "Note" }),
1282
+ /* @__PURE__ */ jsx(
1283
+ Textarea,
1284
+ {
1285
+ id: `zr-note-${highlight.id}`,
1286
+ ref: textRef,
1287
+ rows: 4,
1288
+ value: note,
1289
+ placeholder: "Add a note\u2026",
1290
+ onChange: (e) => setNote(e.target.value),
1291
+ onBlur: flush
1292
+ }
1293
+ )
1294
+ ] })
1295
+ ] })
1296
+ }
1297
+ );
1298
+ }
1299
+ function DropdownMenu({
1300
+ ...props
1301
+ }) {
1302
+ return /* @__PURE__ */ jsx(DropdownMenu$1.Root, { "data-slot": "dropdown-menu", ...props });
1303
+ }
1304
+ function DropdownMenuTrigger({
1305
+ ...props
1306
+ }) {
1307
+ return /* @__PURE__ */ jsx(
1308
+ DropdownMenu$1.Trigger,
1309
+ {
1310
+ "data-slot": "dropdown-menu-trigger",
1311
+ ...props
1312
+ }
1313
+ );
1314
+ }
1315
+ function DropdownMenuContent({
1316
+ className,
1317
+ sideOffset = 4,
1318
+ ...props
1319
+ }) {
1320
+ return /* @__PURE__ */ jsx(DropdownMenu$1.Portal, { children: /* @__PURE__ */ jsx(
1321
+ DropdownMenu$1.Content,
1322
+ {
1323
+ "data-slot": "dropdown-menu-content",
1324
+ sideOffset,
1325
+ className: cn(
1326
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
1327
+ className
1328
+ ),
1329
+ ...props
1330
+ }
1331
+ ) });
1332
+ }
1333
+ function DropdownMenuItem({
1334
+ className,
1335
+ inset,
1336
+ variant = "default",
1337
+ ...props
1338
+ }) {
1339
+ return /* @__PURE__ */ jsx(
1340
+ DropdownMenu$1.Item,
1341
+ {
1342
+ "data-slot": "dropdown-menu-item",
1343
+ "data-inset": inset,
1344
+ "data-variant": variant,
1345
+ className: cn(
1346
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
1347
+ className
1348
+ ),
1349
+ ...props
1350
+ }
1351
+ );
1352
+ }
1353
+ function DropdownMenuSeparator({
1354
+ className,
1355
+ ...props
1356
+ }) {
1357
+ return /* @__PURE__ */ jsx(
1358
+ DropdownMenu$1.Separator,
1359
+ {
1360
+ "data-slot": "dropdown-menu-separator",
1361
+ className: cn("-mx-1 my-1 h-px bg-border", className),
1362
+ ...props
1363
+ }
1364
+ );
1365
+ }
1366
+ function Skeleton({ className, ...props }) {
1367
+ return /* @__PURE__ */ jsx(
1368
+ "div",
1369
+ {
1370
+ "data-slot": "skeleton",
1371
+ className: cn("animate-pulse rounded-md bg-accent", className),
1372
+ ...props
1373
+ }
1374
+ );
1375
+ }
1376
+
1377
+ // src/react/download.ts
1378
+ function downloadFile(filename, content, type = "application/json") {
1379
+ const url = URL.createObjectURL(new Blob([content], { type }));
1380
+ const a = document.createElement("a");
1381
+ a.href = url;
1382
+ a.download = filename;
1383
+ document.body.appendChild(a);
1384
+ a.click();
1385
+ a.remove();
1386
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
1387
+ }
1388
+ async function copyText(text) {
1389
+ try {
1390
+ await navigator.clipboard.writeText(text);
1391
+ return true;
1392
+ } catch {
1393
+ return false;
1394
+ }
1395
+ }
1396
+ var ScratchpadEditor = lazy(() => import('zreader/editor').then((m) => ({ default: m.ScratchpadEditor })));
1397
+ var SCOPE_LABEL = { chapter: "Chapter", book: "Book", global: "All books" };
1398
+ function scopeFor(kind, bookId, chapterId) {
1399
+ if (kind === "global") return { kind };
1400
+ if (kind === "book") return { kind, bookId };
1401
+ return { kind, bookId, chapterId };
1402
+ }
1403
+ function scopeKey(scope) {
1404
+ return scope.kind === "global" ? "global" : scope.kind === "book" ? `book:${scope.bookId}` : `chapter:${scope.bookId}:${scope.chapterId}`;
1405
+ }
1406
+ var EditorBoundary = class extends Component {
1407
+ state = { failed: false };
1408
+ static getDerivedStateFromError() {
1409
+ return { failed: true };
1410
+ }
1411
+ render() {
1412
+ return this.state.failed ? this.props.fallback : this.props.children;
1413
+ }
1414
+ };
1415
+ function PlainEditor({ scope, onFocus }) {
1416
+ const { store } = useReader();
1417
+ const pad = usePad(scope);
1418
+ return /* @__PURE__ */ jsx(
1419
+ Textarea,
1420
+ {
1421
+ value: pad.markdown,
1422
+ onChange: (e) => store.savePad(scope, e.target.value),
1423
+ onFocus,
1424
+ placeholder: "Write in markdown. - [ ] makes a checklist item.",
1425
+ className: "h-full min-h-48 flex-1 resize-none font-mono text-sm",
1426
+ "aria-label": "Scratchpad (markdown)"
1427
+ }
1428
+ );
1429
+ }
1430
+ function ScratchpadPanel({ bookId, chapterId, kind, onKindChange, onClose, onEditorFocus, title }) {
1431
+ const { store, options } = useReader();
1432
+ const scope = useMemo(() => scopeFor(kind, bookId, chapterId), [kind, bookId, chapterId]);
1433
+ const pad = usePad(scope);
1434
+ const scopes = options.scratchpad.scopes;
1435
+ const stem = `${title ?? bookId}-${kind}-notes`.replace(/[^a-z0-9-]+/gi, "-").toLowerCase();
1436
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
1437
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b px-3 py-2", children: [
1438
+ scopes.length > 1 ? /* @__PURE__ */ jsx(Tabs, { value: kind, onValueChange: (v) => onKindChange(v), className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(TabsList, { className: "w-full", children: scopes.map((s) => /* @__PURE__ */ jsx(TabsTrigger, { value: s, children: SCOPE_LABEL[s] }, s)) }) }) : /* @__PURE__ */ jsx("h2", { className: "flex-1 text-sm font-medium", children: "Scratchpad" }),
1439
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
1440
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Scratchpad options", children: /* @__PURE__ */ jsx(EllipsisIcon, {}) }) }),
1441
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
1442
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { onSelect: () => downloadFile(`${stem}.md`, pad.markdown, "text/markdown"), disabled: !pad.markdown.trim(), children: [
1443
+ /* @__PURE__ */ jsx(DownloadIcon, {}),
1444
+ " Download .md"
1445
+ ] }),
1446
+ /* @__PURE__ */ jsxs(
1447
+ DropdownMenuItem,
1448
+ {
1449
+ onSelect: async () => toast(await copyText(pad.markdown) ? "Markdown copied" : "Could not copy"),
1450
+ disabled: !pad.markdown.trim(),
1451
+ children: [
1452
+ /* @__PURE__ */ jsx(CopyIcon, {}),
1453
+ " Copy markdown"
1454
+ ]
1455
+ }
1456
+ ),
1457
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1458
+ /* @__PURE__ */ jsxs(
1459
+ DropdownMenuItem,
1460
+ {
1461
+ variant: "destructive",
1462
+ disabled: !pad.markdown.trim(),
1463
+ onSelect: () => {
1464
+ const previous = pad;
1465
+ store.clearPad(scope);
1466
+ toast("Scratchpad cleared", {
1467
+ action: { label: "Undo", onClick: () => store.savePad(scope, previous.markdown, previous.json) }
1468
+ });
1469
+ },
1470
+ children: [
1471
+ /* @__PURE__ */ jsx(Trash2Icon, {}),
1472
+ " Clear"
1473
+ ]
1474
+ }
1475
+ )
1476
+ ] })
1477
+ ] }),
1478
+ onClose && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Close scratchpad", onClick: onClose, children: /* @__PURE__ */ jsx(XIcon, {}) })
1479
+ ] }),
1480
+ /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx(EditorBoundary, { fallback: /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col p-3", children: /* @__PURE__ */ jsx(PlainEditor, { scope, onFocus: onEditorFocus }) }), children: /* @__PURE__ */ jsx(
1481
+ Suspense,
1482
+ {
1483
+ fallback: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 p-4", children: [
1484
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-8 w-full" }),
1485
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/4" }),
1486
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-1/2" })
1487
+ ] }),
1488
+ children: /* @__PURE__ */ jsx(ScratchpadEditor, { scope, onFocus: onEditorFocus }, scopeKey(scope))
1489
+ }
1490
+ ) }, scopeKey(scope)) })
1491
+ ] });
1492
+ }
1493
+ function Popover({
1494
+ ...props
1495
+ }) {
1496
+ return /* @__PURE__ */ jsx(Popover$1.Root, { "data-slot": "popover", ...props });
1497
+ }
1498
+ function PopoverContent({
1499
+ className,
1500
+ align = "center",
1501
+ sideOffset = 4,
1502
+ ...props
1503
+ }) {
1504
+ return /* @__PURE__ */ jsx(Popover$1.Portal, { children: /* @__PURE__ */ jsx(
1505
+ Popover$1.Content,
1506
+ {
1507
+ "data-slot": "popover-content",
1508
+ align,
1509
+ sideOffset,
1510
+ className: cn(
1511
+ "z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
1512
+ className
1513
+ ),
1514
+ ...props
1515
+ }
1516
+ ) });
1517
+ }
1518
+ function PopoverAnchor({
1519
+ ...props
1520
+ }) {
1521
+ return /* @__PURE__ */ jsx(Popover$1.Anchor, { "data-slot": "popover-anchor", ...props });
1522
+ }
1523
+ function Separator2({
1524
+ className,
1525
+ orientation = "horizontal",
1526
+ decorative = true,
1527
+ ...props
1528
+ }) {
1529
+ return /* @__PURE__ */ jsx(
1530
+ Separator.Root,
1531
+ {
1532
+ "data-slot": "separator",
1533
+ decorative,
1534
+ orientation,
1535
+ className: cn(
1536
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
1537
+ className
1538
+ ),
1539
+ ...props
1540
+ }
1541
+ );
1542
+ }
1543
+ function Action({ label, onSelect, children }) {
1544
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
1545
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": label, onClick: onSelect, children }) }),
1546
+ /* @__PURE__ */ jsx(TooltipContent, { children: label })
1547
+ ] });
1548
+ }
1549
+ function SelectionToolbar({ selection, colors, onHighlight, onNote, onSentence, onQuote, onCopy, onDismiss }) {
1550
+ const isDesktop = useIsDesktop();
1551
+ const last = useRef(null);
1552
+ if (selection) last.current = selection;
1553
+ const current = () => last.current;
1554
+ const anchorRef = useMemo(
1555
+ () => ({
1556
+ current: {
1557
+ getBoundingClientRect: () => {
1558
+ const r = last.current?.rect;
1559
+ return DOMRect.fromRect(r ? { x: r.left, y: r.top, width: r.width, height: r.height } : void 0);
1560
+ }
1561
+ }
1562
+ }),
1563
+ []
1564
+ );
1565
+ const keepSelection = (e) => e.preventDefault();
1566
+ const actions = /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", role: "toolbar", "aria-label": "Selection actions", onPointerDown: keepSelection, onMouseDown: keepSelection, children: [
1567
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 px-1", children: colors.map((c) => /* @__PURE__ */ jsx(
1568
+ Button,
1569
+ {
1570
+ type: "button",
1571
+ variant: "ghost",
1572
+ size: "icon-sm",
1573
+ "aria-label": `Highlight ${c}`,
1574
+ onClick: () => current() && onHighlight(current().span, c),
1575
+ children: /* @__PURE__ */ jsx("span", { className: cn("size-5 rounded-full ring-1 ring-foreground/15", swatchClass(c)) })
1576
+ },
1577
+ c
1578
+ )) }),
1579
+ /* @__PURE__ */ jsx(Separator2, { orientation: "vertical", className: "mx-1 h-6" }),
1580
+ /* @__PURE__ */ jsx(Action, { label: "Whole sentence", onSelect: () => current() && onSentence(current().span), children: /* @__PURE__ */ jsx(TextSelectIcon, {}) }),
1581
+ /* @__PURE__ */ jsx(Action, { label: "Add note", onSelect: () => current() && onNote(current().span), children: /* @__PURE__ */ jsx(MessageSquarePlusIcon, {}) }),
1582
+ onQuote && /* @__PURE__ */ jsx(Action, { label: "Quote to scratchpad", onSelect: () => current() && onQuote(current().span, current().text), children: /* @__PURE__ */ jsx(QuoteIcon, {}) }),
1583
+ onCopy && /* @__PURE__ */ jsx(Action, { label: "Copy", onSelect: () => current() && onCopy(current().text), children: /* @__PURE__ */ jsx(CopyIcon, {}) }),
1584
+ !isDesktop && /* @__PURE__ */ jsx(Action, { label: "Dismiss", onSelect: onDismiss, children: /* @__PURE__ */ jsx(XIcon, {}) })
1585
+ ] });
1586
+ if (isDesktop) {
1587
+ return /* @__PURE__ */ jsxs(Popover, { open: !!selection, modal: false, children: [
1588
+ /* @__PURE__ */ jsx(PopoverAnchor, { virtualRef: anchorRef }),
1589
+ /* @__PURE__ */ jsx(
1590
+ PopoverContent,
1591
+ {
1592
+ side: "top",
1593
+ align: "center",
1594
+ sideOffset: 10,
1595
+ className: "w-auto p-1",
1596
+ onOpenAutoFocus: (e) => e.preventDefault(),
1597
+ onCloseAutoFocus: (e) => e.preventDefault(),
1598
+ onEscapeKeyDown: onDismiss,
1599
+ children: actions
1600
+ }
1601
+ )
1602
+ ] });
1603
+ }
1604
+ if (!selection) return null;
1605
+ return /* @__PURE__ */ jsx("div", { className: "fixed inset-x-2 bottom-[max(0.75rem,env(safe-area-inset-bottom))] z-40 flex justify-center animate-in fade-in-0 slide-in-from-bottom-2", children: /* @__PURE__ */ jsx("div", { className: "max-w-full overflow-x-auto rounded-xl border bg-popover p-1 text-popover-foreground shadow-lg", children: actions }) });
1606
+ }
1607
+ var alertVariants = cva(
1608
+ "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
1609
+ {
1610
+ variants: {
1611
+ variant: {
1612
+ default: "bg-card text-card-foreground",
1613
+ destructive: "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current"
1614
+ }
1615
+ },
1616
+ defaultVariants: {
1617
+ variant: "default"
1618
+ }
1619
+ }
1620
+ );
1621
+ function Alert({
1622
+ className,
1623
+ variant,
1624
+ ...props
1625
+ }) {
1626
+ return /* @__PURE__ */ jsx(
1627
+ "div",
1628
+ {
1629
+ "data-slot": "alert",
1630
+ role: "alert",
1631
+ className: cn(alertVariants({ variant }), className),
1632
+ ...props
1633
+ }
1634
+ );
1635
+ }
1636
+ function AlertTitle({ className, ...props }) {
1637
+ return /* @__PURE__ */ jsx(
1638
+ "div",
1639
+ {
1640
+ "data-slot": "alert-title",
1641
+ className: cn(
1642
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
1643
+ className
1644
+ ),
1645
+ ...props
1646
+ }
1647
+ );
1648
+ }
1649
+ function AlertDescription({
1650
+ className,
1651
+ ...props
1652
+ }) {
1653
+ return /* @__PURE__ */ jsx(
1654
+ "div",
1655
+ {
1656
+ "data-slot": "alert-description",
1657
+ className: cn(
1658
+ "col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed",
1659
+ className
1660
+ ),
1661
+ ...props
1662
+ }
1663
+ );
1664
+ }
1665
+ function Switch({
1666
+ className,
1667
+ size = "default",
1668
+ ...props
1669
+ }) {
1670
+ return /* @__PURE__ */ jsx(
1671
+ Switch$1.Root,
1672
+ {
1673
+ "data-slot": "switch",
1674
+ "data-size": size,
1675
+ className: cn(
1676
+ "peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
1677
+ className
1678
+ ),
1679
+ ...props,
1680
+ children: /* @__PURE__ */ jsx(
1681
+ Switch$1.Thumb,
1682
+ {
1683
+ "data-slot": "switch-thumb",
1684
+ className: cn(
1685
+ "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground"
1686
+ )
1687
+ }
1688
+ )
1689
+ }
1690
+ );
1691
+ }
1692
+ var THEME_LABEL = { system: "Auto", light: "Light", sepia: "Sepia", dark: "Dark" };
1693
+ var SIZE_LABEL = { sm: "S", base: "M", lg: "L", xl: "XL", "2xl": "2XL" };
1694
+ var LEADING_LABEL = { tight: "Tight", normal: "Normal", relaxed: "Relaxed" };
1695
+ var MEASURE_LABEL = { narrow: "Narrow", normal: "Normal", wide: "Wide" };
1696
+ var FONT_LABEL = { serif: "Serif", sans: "Sans", mono: "Mono" };
1697
+ function Choice({
1698
+ label,
1699
+ value,
1700
+ values,
1701
+ labels,
1702
+ onChange
1703
+ }) {
1704
+ const id = useId();
1705
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
1706
+ /* @__PURE__ */ jsx(Label, { id, children: label }),
1707
+ /* @__PURE__ */ jsx(
1708
+ ToggleGroup,
1709
+ {
1710
+ type: "single",
1711
+ variant: "outline",
1712
+ value,
1713
+ onValueChange: (v) => v && onChange(v),
1714
+ "aria-labelledby": id,
1715
+ className: "w-full",
1716
+ children: values.map((v) => /* @__PURE__ */ jsx(ToggleGroupItem, { value: v, className: "flex-1", children: labels[v] }, v))
1717
+ }
1718
+ )
1719
+ ] });
1720
+ }
1721
+ function SettingsPanel({ open, onOpenChange, book }) {
1722
+ const { store, options } = useReader();
1723
+ const [settings, update] = useSettings();
1724
+ const { theme, setTheme } = useTheme();
1725
+ const hydrated = useHydrated();
1726
+ const highlights = useHighlights(book?.id ?? "");
1727
+ const fileRef = useRef(null);
1728
+ const [replace, setReplace] = useState(false);
1729
+ const modeId = useId();
1730
+ const hlModeId = useId();
1731
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1732
+ const onImport = async (file) => {
1733
+ try {
1734
+ const res = importBackup(store, await file.text(), { mode: replace ? "replace" : "merge" });
1735
+ toast.success(`Imported ${res.imported} record${res.imported === 1 ? "" : "s"}`, {
1736
+ description: res.skipped ? `${res.skipped} skipped` : void 0
1737
+ });
1738
+ } catch (e) {
1739
+ toast.error("Import failed", { description: e.message });
1740
+ }
1741
+ };
1742
+ return /* @__PURE__ */ jsx(ResponsivePanel, { open, onOpenChange, title: "Reading settings", desktop: "sheet-right", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-5", children: [
1743
+ hydrated && /* @__PURE__ */ jsx(
1744
+ Choice,
1745
+ {
1746
+ label: "Theme",
1747
+ value: theme ?? options.defaultTheme,
1748
+ values: options.themes,
1749
+ labels: THEME_LABEL,
1750
+ onChange: setTheme
1751
+ }
1752
+ ),
1753
+ /* @__PURE__ */ jsx(Choice, { label: "Text size", value: settings.textSize, values: TEXT_SIZES, labels: SIZE_LABEL, onChange: (textSize) => update({ textSize }) }),
1754
+ /* @__PURE__ */ jsx(Choice, { label: "Line spacing", value: settings.leading, values: LEADINGS, labels: LEADING_LABEL, onChange: (leading) => update({ leading }) }),
1755
+ /* @__PURE__ */ jsx(Choice, { label: "Line width", value: settings.measure, values: MEASURES, labels: MEASURE_LABEL, onChange: (measure) => update({ measure }) }),
1756
+ /* @__PURE__ */ jsx(Choice, { label: "Font", value: settings.fontFamily, values: FONT_FAMILIES, labels: FONT_LABEL, onChange: (fontFamily) => update({ fontFamily }) }),
1757
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4 rounded-lg border p-3", children: [
1758
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
1759
+ /* @__PURE__ */ jsx(Label, { htmlFor: hlModeId, children: "Highlight mode" }),
1760
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Tap any sentence to highlight it." })
1761
+ ] }),
1762
+ /* @__PURE__ */ jsx(Switch, { id: hlModeId, checked: settings.highlightMode, onCheckedChange: (highlightMode) => update({ highlightMode }) })
1763
+ ] }),
1764
+ /* @__PURE__ */ jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "self-start", onClick: () => store.resetSettings(), children: [
1765
+ /* @__PURE__ */ jsx(RotateCcwIcon, {}),
1766
+ " Reset reading settings"
1767
+ ] }),
1768
+ /* @__PURE__ */ jsx(Separator2, {}),
1769
+ /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-3", "aria-labelledby": `${modeId}-title`, children: [
1770
+ /* @__PURE__ */ jsx("h3", { id: `${modeId}-title`, className: "text-sm font-medium", children: "Your data" }),
1771
+ options.backupReminder && /* @__PURE__ */ jsxs(Alert, { children: [
1772
+ /* @__PURE__ */ jsx(HardDriveIcon, {}),
1773
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Stored only in this browser" }),
1774
+ /* @__PURE__ */ jsx(AlertDescription, { children: "Highlights, notes, bookmarks and scratchpads are removed if site data is cleared. Export a backup now and then." })
1775
+ ] }),
1776
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
1777
+ /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: () => downloadFile(`reader-backup-${stamp}.json`, JSON.stringify(exportBackup(store), null, 2)), children: [
1778
+ /* @__PURE__ */ jsx(DownloadIcon, {}),
1779
+ " Export backup"
1780
+ ] }),
1781
+ /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: () => fileRef.current?.click(), children: [
1782
+ /* @__PURE__ */ jsx(UploadIcon, {}),
1783
+ " Import backup"
1784
+ ] }),
1785
+ book && highlights.length > 0 && /* @__PURE__ */ jsxs(
1786
+ Button,
1787
+ {
1788
+ type: "button",
1789
+ variant: "outline",
1790
+ onClick: () => {
1791
+ const titles = new Map(book.chapters.map((c) => [c.id, c.title]));
1792
+ downloadFile(`${book.id}-highlights.md`, highlightsToMarkdown(book.title, highlights, (id) => titles.get(id) ?? id), "text/markdown");
1793
+ },
1794
+ children: [
1795
+ /* @__PURE__ */ jsx(DownloadIcon, {}),
1796
+ " Highlights (.md)"
1797
+ ]
1798
+ }
1799
+ )
1800
+ ] }),
1801
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1802
+ /* @__PURE__ */ jsx(Switch, { id: modeId, checked: replace, onCheckedChange: setReplace }),
1803
+ /* @__PURE__ */ jsx(Label, { htmlFor: modeId, className: "font-normal", children: "Replace existing data on import" })
1804
+ ] }),
1805
+ /* @__PURE__ */ jsx(
1806
+ "input",
1807
+ {
1808
+ ref: fileRef,
1809
+ type: "file",
1810
+ accept: "application/json,.json",
1811
+ className: "hidden",
1812
+ onChange: (e) => {
1813
+ const f = e.target.files?.[0];
1814
+ if (f) void onImport(f);
1815
+ e.target.value = "";
1816
+ }
1817
+ }
1818
+ ),
1819
+ /* @__PURE__ */ jsx(
1820
+ Button,
1821
+ {
1822
+ type: "button",
1823
+ variant: "link",
1824
+ className: "self-start px-0",
1825
+ onClick: async () => await requestPersistentStorage() ? toast.success("This browser will keep your reader data.") : toast.info("The browser did not grant persistent storage."),
1826
+ children: "Ask the browser to keep this data"
1827
+ }
1828
+ )
1829
+ ] })
1830
+ ] }) });
1831
+ }
1832
+ var INTERACTIVE = `a,button,input,textarea,select,summary,label,video,audio,iframe,[contenteditable],[${IGNORE_ATTR}]`;
1833
+ function prefersReducedMotion() {
1834
+ return typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1835
+ }
1836
+ function createValueStore(initial) {
1837
+ let value = initial;
1838
+ const listeners = /* @__PURE__ */ new Set();
1839
+ return {
1840
+ get: () => value,
1841
+ set(v) {
1842
+ if (Math.abs(v - value) < 1e-3) return;
1843
+ value = v;
1844
+ listeners.forEach((l) => l());
1845
+ },
1846
+ subscribe(l) {
1847
+ listeners.add(l);
1848
+ return () => listeners.delete(l);
1849
+ }
1850
+ };
1851
+ }
1852
+ function useValueStore(store) {
1853
+ return useSyncExternalStore(store.subscribe, store.get, () => 0);
1854
+ }
1855
+ function useChapterEngine(opts) {
1856
+ const { bookId, chapterId, contentHash, root: rootEl, scroller: scrollEl = null, topOffset = 64 } = opts;
1857
+ const rootRef = useRef(rootEl);
1858
+ rootRef.current = rootEl;
1859
+ const { store, options } = useReader();
1860
+ const [settings] = useSettings();
1861
+ const highlights = useHighlights(bookId, chapterId);
1862
+ const indexRef = useRef(null);
1863
+ const [version, setVersion] = useState(0);
1864
+ const [selection, setSelection] = useState(null);
1865
+ const [activeId, setActiveId] = useState(null);
1866
+ const [resume, setResume] = useState(null);
1867
+ const [percentStore] = useState(() => createValueStore(0));
1868
+ const [domToc, setDomToc] = useState([]);
1869
+ const onTapRef = useRef(opts.onHighlightTap);
1870
+ onTapRef.current = opts.onHighlightTap;
1871
+ const scroller = useCallback(() => scrollEl, [scrollEl]);
1872
+ const viewportTop = useCallback(() => scroller()?.getBoundingClientRect().top ?? 0, [scroller]);
1873
+ const viewportHeight = useCallback(() => scroller()?.clientHeight ?? window.innerHeight, [scroller]);
1874
+ const scrollTop = useCallback(() => scroller()?.scrollTop ?? window.scrollY, [scroller]);
1875
+ const scrollToY = useCallback(
1876
+ (y, smooth = true) => {
1877
+ const opts2 = { top: Math.max(0, y), behavior: smooth && !prefersReducedMotion() ? "smooth" : "auto" };
1878
+ const el = scroller();
1879
+ if (el) el.scrollTo(opts2);
1880
+ else window.scrollTo(opts2);
1881
+ },
1882
+ [scroller]
1883
+ );
1884
+ const targetFor = useCallback(
1885
+ (rectTop, extra = 8) => scrollTop() + rectTop - viewportTop() - topOffset - extra,
1886
+ [scrollTop, viewportTop, topOffset]
1887
+ );
1888
+ useEffect(() => {
1889
+ const root = rootEl;
1890
+ if (!root) return;
1891
+ const rebuild = () => {
1892
+ ensureBlockIds(root);
1893
+ setDomToc((prev) => {
1894
+ const next = collectToc(root);
1895
+ return JSON.stringify(prev) === JSON.stringify(next) ? prev : next;
1896
+ });
1897
+ indexRef.current = buildTextIndex(root);
1898
+ setVersion((v) => v + 1);
1899
+ };
1900
+ rebuild();
1901
+ let t;
1902
+ const mo = new MutationObserver(() => {
1903
+ clearTimeout(t);
1904
+ t = setTimeout(rebuild, 60);
1905
+ });
1906
+ mo.observe(root, { childList: true, subtree: true, characterData: true });
1907
+ return () => {
1908
+ mo.disconnect();
1909
+ clearTimeout(t);
1910
+ };
1911
+ }, [rootEl, chapterId]);
1912
+ const getIndex = useCallback(() => {
1913
+ const root = rootRef.current;
1914
+ if (root && (!indexRef.current || indexRef.current.root !== root)) indexRef.current = buildTextIndex(root);
1915
+ return indexRef.current;
1916
+ }, []);
1917
+ const { resolved, orphaned } = useMemo(() => {
1918
+ const index = indexRef.current;
1919
+ const ok = [];
1920
+ const lost = [];
1921
+ if (index) {
1922
+ for (const h of highlights) {
1923
+ const span = resolveAnchor(index, h.anchor);
1924
+ const range = span ? spanToRange(index, span) : null;
1925
+ if (span && range) ok.push({ highlight: h, span, range });
1926
+ else lost.push(h);
1927
+ }
1928
+ }
1929
+ return { resolved: ok, orphaned: lost };
1930
+ }, [highlights, version]);
1931
+ const resolvedRef = useRef(resolved);
1932
+ resolvedRef.current = resolved;
1933
+ useEffect(() => {
1934
+ const index = indexRef.current;
1935
+ if (!index) return;
1936
+ for (const r of resolved) {
1937
+ const a = r.highlight.anchor;
1938
+ if (a.start === r.span.start && a.end === r.span.end) continue;
1939
+ const fresh = describeSpan(index, r.span, contentHash);
1940
+ if (fresh) store.updateHighlight(bookId, r.highlight.id, { anchor: fresh });
1941
+ }
1942
+ }, [resolved, bookId, contentHash, store]);
1943
+ useEffect(() => paintHighlights(resolved, { activeId }), [resolved, activeId]);
1944
+ useEffect(() => {
1945
+ let t;
1946
+ const update = () => {
1947
+ const index = getIndex();
1948
+ const sel = document.getSelection();
1949
+ const span = index ? selectionSpan(index, sel) : null;
1950
+ if (!index || !span || !sel || sel.rangeCount === 0) {
1951
+ setSelection(null);
1952
+ return;
1953
+ }
1954
+ const r = sel.getRangeAt(0).getBoundingClientRect();
1955
+ setSelection({
1956
+ span,
1957
+ text: index.text.slice(span.start, span.end),
1958
+ rect: { top: r.top, left: r.left, bottom: r.bottom, right: r.right, width: r.width, height: r.height }
1959
+ });
1960
+ };
1961
+ const onChange = () => {
1962
+ clearTimeout(t);
1963
+ t = setTimeout(update, 120);
1964
+ };
1965
+ document.addEventListener("selectionchange", onChange);
1966
+ return () => {
1967
+ document.removeEventListener("selectionchange", onChange);
1968
+ clearTimeout(t);
1969
+ };
1970
+ }, [getIndex]);
1971
+ const clearSelection = useCallback(() => {
1972
+ document.getSelection()?.removeAllRanges();
1973
+ setSelection(null);
1974
+ }, []);
1975
+ const selectSpan = useCallback(
1976
+ (span) => {
1977
+ const index = getIndex();
1978
+ const range = index && spanToRange(index, span);
1979
+ const sel = document.getSelection();
1980
+ if (!range || !sel) return;
1981
+ sel.removeAllRanges();
1982
+ sel.addRange(range);
1983
+ },
1984
+ [getIndex]
1985
+ );
1986
+ const expandSelectionToSentences = useCallback(
1987
+ (span) => {
1988
+ const index = getIndex();
1989
+ const base = span ?? selection?.span;
1990
+ if (!index || !base) return;
1991
+ selectSpan(expandSpanToSentences(index, base, document.documentElement.lang || void 0));
1992
+ },
1993
+ [getIndex, selection, selectSpan]
1994
+ );
1995
+ const createHighlight = useCallback(
1996
+ (span, color, note) => {
1997
+ const index = getIndex();
1998
+ if (!index) return null;
1999
+ const anchor = describeSpan(index, span, contentHash);
2000
+ if (!anchor) return null;
2001
+ const dup = highlights.find((h) => h.anchor.start === anchor.start && h.anchor.end === anchor.end);
2002
+ if (dup) {
2003
+ if (color && dup.color !== color) store.updateHighlight(bookId, dup.id, { color });
2004
+ return dup;
2005
+ }
2006
+ return store.addHighlight({ bookId, chapterId, anchor, color, note });
2007
+ },
2008
+ [getIndex, contentHash, highlights, store, bookId, chapterId]
2009
+ );
2010
+ const highlightModeRef = useRef(settings.highlightMode);
2011
+ highlightModeRef.current = settings.highlightMode;
2012
+ const createRef = useRef(createHighlight);
2013
+ createRef.current = createHighlight;
2014
+ useEffect(() => {
2015
+ const root = rootEl;
2016
+ if (!root) return;
2017
+ const onClick = (e) => {
2018
+ const sel = document.getSelection();
2019
+ if (sel && !sel.isCollapsed) return;
2020
+ if (e.target.closest?.(INTERACTIVE)) return;
2021
+ const index = getIndex();
2022
+ if (!index) return;
2023
+ const offset = offsetFromPoint(index, e.clientX, e.clientY);
2024
+ if (offset == null) return;
2025
+ const hit = highlightAtOffset(resolvedRef.current, offset);
2026
+ if (hit) {
2027
+ setActiveId(hit.highlight.id);
2028
+ onTapRef.current?.(hit.highlight.id);
2029
+ return;
2030
+ }
2031
+ if (highlightModeRef.current) {
2032
+ const span = sentenceSpanAt(index, offset, document.documentElement.lang || void 0);
2033
+ if (span && createRef.current(span)) navigator.vibrate?.(8);
2034
+ }
2035
+ };
2036
+ root.addEventListener("click", onClick);
2037
+ return () => root.removeEventListener("click", onClick);
2038
+ }, [rootEl, getIndex]);
2039
+ const scrollToSpan = useCallback(
2040
+ (span, smooth = true) => {
2041
+ const index = getIndex();
2042
+ const range = index && spanToRange(index, span);
2043
+ if (!range) return false;
2044
+ scrollToY(targetFor(range.getBoundingClientRect().top, viewportHeight() * 0.15), smooth);
2045
+ return true;
2046
+ },
2047
+ [getIndex, scrollToY, targetFor, viewportHeight]
2048
+ );
2049
+ const scrollToHighlight = useCallback(
2050
+ (id, smooth = true) => {
2051
+ const r = resolvedRef.current.find((x) => x.highlight.id === id);
2052
+ if (!r) return false;
2053
+ setActiveId(id);
2054
+ return scrollToSpan(r.span, smooth);
2055
+ },
2056
+ [scrollToSpan]
2057
+ );
2058
+ const scrollToElement = useCallback(
2059
+ (el, smooth = true) => {
2060
+ if (!el) return false;
2061
+ scrollToY(targetFor(el.getBoundingClientRect().top), smooth);
2062
+ return true;
2063
+ },
2064
+ [scrollToY, targetFor]
2065
+ );
2066
+ const scrollToBlock = useCallback(
2067
+ (blockId, smooth = true) => {
2068
+ const root = rootRef.current;
2069
+ return scrollToElement(root && findBlock(root, blockId), smooth);
2070
+ },
2071
+ [scrollToElement]
2072
+ );
2073
+ const scrollToHeading = useCallback(
2074
+ (id, smooth = true) => scrollToElement(rootRef.current?.ownerDocument.getElementById(id) ?? null, smooth),
2075
+ [scrollToElement]
2076
+ );
2077
+ const scrollToTop = useCallback(() => scrollToY(0), [scrollToY]);
2078
+ const getTopBlock = useCallback(() => {
2079
+ const root = rootRef.current;
2080
+ if (!root) return null;
2081
+ const line = viewportTop() + topOffset + 8;
2082
+ for (const el of Array.from(root.querySelectorAll(`[${BLOCK_ATTR}]`))) {
2083
+ if (el.getBoundingClientRect().bottom > line) return el;
2084
+ }
2085
+ return null;
2086
+ }, [viewportTop, topOffset]);
2087
+ useEffect(() => {
2088
+ const root = rootEl;
2089
+ if (!root) return;
2090
+ const target = scroller() ?? window;
2091
+ let raf = 0;
2092
+ let lastSave = 0;
2093
+ let pending;
2094
+ const compute = () => {
2095
+ const box = root.getBoundingClientRect();
2096
+ const top = box.top - viewportTop();
2097
+ const total = box.height - viewportHeight() + topOffset;
2098
+ const percent = total <= 0 ? 1 : Math.max(0, Math.min(1, (topOffset - top) / total));
2099
+ percentStore.set(percent);
2100
+ return percent;
2101
+ };
2102
+ const save = () => {
2103
+ const percent = compute();
2104
+ const block = getTopBlock();
2105
+ store.recordProgress(bookId, chapterId, { blockId: block?.getAttribute(BLOCK_ATTR) ?? void 0, percent });
2106
+ lastSave = Date.now();
2107
+ };
2108
+ const onScroll = () => {
2109
+ cancelAnimationFrame(raf);
2110
+ raf = requestAnimationFrame(() => {
2111
+ compute();
2112
+ clearTimeout(pending);
2113
+ pending = setTimeout(save, Math.max(0, options.progress.saveIntervalMs - (Date.now() - lastSave)));
2114
+ });
2115
+ };
2116
+ compute();
2117
+ target.addEventListener("scroll", onScroll, { passive: true });
2118
+ window.addEventListener("resize", onScroll, { passive: true });
2119
+ window.addEventListener("pagehide", save);
2120
+ return () => {
2121
+ target.removeEventListener("scroll", onScroll);
2122
+ window.removeEventListener("resize", onScroll);
2123
+ window.removeEventListener("pagehide", save);
2124
+ cancelAnimationFrame(raf);
2125
+ clearTimeout(pending);
2126
+ };
2127
+ }, [rootEl, scroller, viewportTop, viewportHeight, bookId, chapterId, store, options.progress.saveIntervalMs, getTopBlock, topOffset, percentStore]);
2128
+ const latest = useRef({ scrollToHighlight, scrollToBlock, scrollToHeading, scrollTop });
2129
+ latest.current = { scrollToHighlight, scrollToBlock, scrollToHeading, scrollTop };
2130
+ const restoredFor = useRef(null);
2131
+ useEffect(() => {
2132
+ if (!rootEl || version === 0 || restoredFor.current === chapterId) return;
2133
+ restoredFor.current = chapterId;
2134
+ const hash = decodeURIComponent(window.location.hash.slice(1));
2135
+ const run = () => {
2136
+ const l = latest.current;
2137
+ if (hash.startsWith("hl-")) {
2138
+ if (l.scrollToHighlight(hash.slice(3), false)) return;
2139
+ } else if (hash.startsWith("b-")) {
2140
+ if (l.scrollToBlock(hash.slice(2), false)) return;
2141
+ } else if (hash) {
2142
+ l.scrollToHeading(hash, false);
2143
+ return;
2144
+ }
2145
+ const saved = store.progress(bookId).get().chapters[chapterId];
2146
+ if (saved?.blockId && saved.percent > 0.03 && saved.percent < 0.98 && l.scrollTop() < 40) {
2147
+ if (l.scrollToBlock(saved.blockId, false)) setResume({ blockId: saved.blockId, percent: saved.percent });
2148
+ }
2149
+ };
2150
+ const fonts = document.fonts;
2151
+ const go = () => requestAnimationFrame(() => requestAnimationFrame(run));
2152
+ if (fonts?.ready) void fonts.ready.then(go);
2153
+ else go();
2154
+ }, [version, chapterId, bookId, store, rootEl]);
2155
+ const dismissResume = useCallback(() => setResume(null), []);
2156
+ return {
2157
+ highlights,
2158
+ resolved,
2159
+ orphaned,
2160
+ selection,
2161
+ activeId,
2162
+ setActiveId,
2163
+ resume,
2164
+ dismissResume,
2165
+ percentStore,
2166
+ domToc,
2167
+ clearSelection,
2168
+ selectSpan,
2169
+ expandSelectionToSentences,
2170
+ createHighlight,
2171
+ scrollToHighlight,
2172
+ scrollToBlock,
2173
+ scrollToHeading,
2174
+ scrollToTop,
2175
+ getTopBlock,
2176
+ getIndex
2177
+ };
2178
+ }
2179
+ var ReaderChapterContext = createContext(null);
2180
+ function useReaderChapter() {
2181
+ const ctx = useContext(ReaderChapterContext);
2182
+ if (!ctx) throw new Error("zreader: useReaderChapter() must be used inside <Reader>.");
2183
+ return ctx;
2184
+ }
2185
+ function ReaderTitle({ className }) {
2186
+ const { book, chapter } = useReaderChapter();
2187
+ return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0", className), children: [
2188
+ /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: book.title }),
2189
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: chapter.title })
2190
+ ] });
2191
+ }
2192
+ function HeaderContent({ slot, book, chapter }) {
2193
+ const content = typeof slot === "function" ? slot({ book, chapter }) : slot;
2194
+ if (content === void 0) return /* @__PURE__ */ jsx(ReaderTitle, {});
2195
+ if (typeof content === "string" || typeof content === "number") {
2196
+ return /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: content });
2197
+ }
2198
+ return /* @__PURE__ */ jsx(Fragment, { children: content });
2199
+ }
2200
+ function ReadingProgress({ store, className }) {
2201
+ const value = useValueStore(store);
2202
+ return /* @__PURE__ */ jsx(Progress, { value: Math.round(value * 100), "aria-label": "Chapter progress", className: cn("h-0.5 rounded-none bg-transparent", className) });
2203
+ }
2204
+ function IconAction({ label, children, ...props }) {
2205
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
2206
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": label, ...props, children }) }),
2207
+ /* @__PURE__ */ jsx(TooltipContent, { children: label })
2208
+ ] });
2209
+ }
2210
+ function IconToggle({ label, children, ...props }) {
2211
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
2212
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Toggle, { "aria-label": label, ...props, children }) }),
2213
+ /* @__PURE__ */ jsx(TooltipContent, { children: label })
2214
+ ] });
2215
+ }
2216
+ function useAutoHide(enabled) {
2217
+ const [hidden, setHidden] = useState(false);
2218
+ useEffect(() => {
2219
+ if (!enabled) {
2220
+ setHidden(false);
2221
+ return;
2222
+ }
2223
+ let last = window.scrollY;
2224
+ let raf = 0;
2225
+ const onScroll = () => {
2226
+ cancelAnimationFrame(raf);
2227
+ raf = requestAnimationFrame(() => {
2228
+ const y = window.scrollY;
2229
+ if (Math.abs(y - last) < 8) return;
2230
+ setHidden(y > last && y > 80);
2231
+ last = y;
2232
+ });
2233
+ };
2234
+ window.addEventListener("scroll", onScroll, { passive: true });
2235
+ return () => {
2236
+ window.removeEventListener("scroll", onScroll);
2237
+ cancelAnimationFrame(raf);
2238
+ };
2239
+ }, [enabled]);
2240
+ return hidden;
2241
+ }
2242
+ var isEditable = (el) => !!el?.closest("input,textarea,select,[contenteditable='true']");
2243
+ function Reader({
2244
+ book: bookArg,
2245
+ chapter: chapterArg,
2246
+ prev: prevArg,
2247
+ next: nextArg,
2248
+ toc: tocProp,
2249
+ contentHash,
2250
+ html,
2251
+ children,
2252
+ Link: LinkProp,
2253
+ hrefFor,
2254
+ header,
2255
+ backHref,
2256
+ className
2257
+ }) {
2258
+ const { store, options } = useReader();
2259
+ const library = useLibrary();
2260
+ const book = useBook(bookArg);
2261
+ const chapterIndex = book.chapters.findIndex((c) => c.id === (typeof chapterArg === "string" ? chapterArg : chapterArg.id));
2262
+ const chapter = typeof chapterArg === "string" ? book.chapters[chapterIndex] : chapterArg;
2263
+ if (!chapter) {
2264
+ throw new Error(`zreader: book "${book.id}" has no chapter "${String(chapterArg)}".`);
2265
+ }
2266
+ const prev = prevArg !== void 0 ? prevArg : book.chapters[chapterIndex - 1] ?? null;
2267
+ const next = nextArg !== void 0 ? nextArg : chapterIndex >= 0 ? book.chapters[chapterIndex + 1] ?? null : null;
2268
+ const Link = LinkProp ?? library.Link;
2269
+ const slot = header !== void 0 ? header : library.headerSlot;
2270
+ const chapterCtx = useMemo(() => ({ book, chapter }), [book, chapter]);
2271
+ const [settings, updateSettings] = useSettings();
2272
+ const isDesktop = useIsDesktop();
2273
+ const bookmarks = useBookmarks(book.id);
2274
+ const progress = useProgress(book.id);
2275
+ const build = hrefFor ?? library.href;
2276
+ const href = useCallback((chapterId, hash) => build(book.id, chapterId, hash), [build, book.id]);
2277
+ const [rootEl, setRootEl] = useState(null);
2278
+ const [scrollEl, setScrollEl] = useState(null);
2279
+ const [contentsOpen, setContentsOpen] = useState(false);
2280
+ const [contentsTab, setContentsTab] = useState("contents");
2281
+ const [settingsOpen, setSettingsOpen] = useState(false);
2282
+ const [padOpen, setPadOpen] = useState(false);
2283
+ const [padKind, setPadKind] = useState(options.scratchpad.scopes[0] ?? "chapter");
2284
+ const [padSnap, setPadSnap] = useState(0.55);
2285
+ const [openHighlightId, setOpenHighlightId] = useState(null);
2286
+ const [focusNote, setFocusNote] = useState(false);
2287
+ const engine = useChapterEngine({
2288
+ bookId: book.id,
2289
+ chapterId: chapter.id,
2290
+ contentHash,
2291
+ root: rootEl,
2292
+ scroller: isDesktop ? scrollEl : null,
2293
+ topOffset: isDesktop ? 16 : 64,
2294
+ onHighlightTap: (id) => {
2295
+ setFocusNote(false);
2296
+ setOpenHighlightId(id);
2297
+ }
2298
+ });
2299
+ const toc = tocProp && tocProp.length > 0 ? tocProp : engine.domToc;
2300
+ const headerHidden = useAutoHide(!isDesktop && !engine.selection);
2301
+ const chapterBookmarks = useMemo(() => bookmarks.filter((b) => b.chapterId === chapter.id), [bookmarks, chapter.id]);
2302
+ const completed = progress.chapters[chapter.id]?.completed ?? false;
2303
+ const openHighlight = engine.highlights.find((h) => h.id === openHighlightId) ?? null;
2304
+ const orphanedIds = useMemo(() => new Set(engine.orphaned.map((h) => h.id)), [engine.orphaned]);
2305
+ const padEnabled = options.scratchpad.enabled;
2306
+ useEffect(() => {
2307
+ if (!supportsHighlightApi()) {
2308
+ toast.info("Highlights can't be drawn in this browser", { description: "They are still saved and listed under Contents \u2192 Highlights." });
2309
+ }
2310
+ }, []);
2311
+ useEffect(() => {
2312
+ if (!engine.resume) return;
2313
+ toast(`Resumed at ${Math.round(engine.resume.percent * 100)}%`, {
2314
+ id: `zr-resume-${chapter.id}`,
2315
+ action: { label: "Start from top", onClick: () => engine.scrollToTop() }
2316
+ });
2317
+ engine.dismissResume();
2318
+ }, [engine.resume, engine, chapter.id]);
2319
+ const openPad = useCallback(() => {
2320
+ setPadOpen(true);
2321
+ setPadSnap(0.55);
2322
+ }, []);
2323
+ const quoteToPad = useCallback(
2324
+ (markdown) => {
2325
+ store.appendToPad(scopeFor(padKind, book.id, chapter.id), markdown);
2326
+ openPad();
2327
+ toast.success("Added to scratchpad");
2328
+ },
2329
+ [store, padKind, book.id, chapter.id, openPad]
2330
+ );
2331
+ const quoteSpan = (span, text) => {
2332
+ const h = engine.createHighlight(span);
2333
+ engine.clearSelection();
2334
+ quoteToPad(h ? highlightToMarkdown(h, href(chapter.id, `hl-${h.id}`)) : quoteToMarkdown(text));
2335
+ };
2336
+ const copy = async (text) => {
2337
+ toast(await copyText(text) ? "Copied" : "Could not copy");
2338
+ };
2339
+ const toggleBookmark = () => {
2340
+ if (chapterBookmarks.length) {
2341
+ const removed = chapterBookmarks;
2342
+ removed.forEach((b) => store.removeBookmark(book.id, b.id));
2343
+ toast("Bookmark removed", {
2344
+ action: {
2345
+ label: "Undo",
2346
+ onClick: () => removed.forEach(
2347
+ (b) => store.addBookmark({ bookId: b.bookId, chapterId: b.chapterId, blockId: b.blockId, label: b.label, excerpt: b.excerpt })
2348
+ )
2349
+ }
2350
+ });
2351
+ return;
2352
+ }
2353
+ const block = engine.getTopBlock();
2354
+ store.addBookmark({
2355
+ bookId: book.id,
2356
+ chapterId: chapter.id,
2357
+ ...block ? { blockId: block.getAttribute(BLOCK_ATTR) ?? void 0, excerpt: block.textContent?.trim().slice(0, 120) } : {},
2358
+ label: chapter.title
2359
+ });
2360
+ toast.success("Bookmarked", { description: "Find it under Contents \u2192 Bookmarks." });
2361
+ };
2362
+ useEffect(() => {
2363
+ const onKey = (e) => {
2364
+ if (!engine.selection || e.metaKey || e.ctrlKey || e.altKey || isEditable(document.activeElement)) return;
2365
+ if (e.key === "h") {
2366
+ e.preventDefault();
2367
+ engine.createHighlight(engine.selection.span);
2368
+ engine.clearSelection();
2369
+ } else if (e.key === "n") {
2370
+ e.preventDefault();
2371
+ const h = engine.createHighlight(engine.selection.span);
2372
+ engine.clearSelection();
2373
+ if (h) {
2374
+ setFocusNote(true);
2375
+ setOpenHighlightId(h.id);
2376
+ }
2377
+ }
2378
+ };
2379
+ document.addEventListener("keydown", onKey);
2380
+ return () => document.removeEventListener("keydown", onKey);
2381
+ }, [engine]);
2382
+ const topBar = /* @__PURE__ */ jsxs(
2383
+ "header",
2384
+ {
2385
+ className: cn(
2386
+ "sticky top-0 z-30 border-b bg-background/85 backdrop-blur-md transition-transform duration-200 supports-[backdrop-filter]:bg-background/70",
2387
+ headerHidden && "-translate-y-full"
2388
+ ),
2389
+ children: [
2390
+ /* @__PURE__ */ jsxs("div", { className: "flex h-14 items-center gap-1 px-2 pt-[env(safe-area-inset-top)] sm:px-3", children: [
2391
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
2392
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { asChild: true, variant: "ghost", size: "icon", "aria-label": "Back to book", children: /* @__PURE__ */ jsx(Link, { href: backHref ?? href(), children: /* @__PURE__ */ jsx(ChevronLeftIcon, {}) }) }) }),
2393
+ /* @__PURE__ */ jsx(TooltipContent, { children: "Back to book" })
2394
+ ] }),
2395
+ /* @__PURE__ */ jsx(IconAction, { label: "Contents, highlights and bookmarks", onClick: () => setContentsOpen(true), children: /* @__PURE__ */ jsx(ListIcon, {}) }),
2396
+ /* @__PURE__ */ jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2 px-1", children: /* @__PURE__ */ jsx(HeaderContent, { slot, book, chapter }) }),
2397
+ /* @__PURE__ */ jsx(IconToggle, { label: chapterBookmarks.length ? "Remove bookmark" : "Bookmark this place", pressed: chapterBookmarks.length > 0, onPressedChange: toggleBookmark, className: "data-[state=on]:[&_svg]:fill-current", children: /* @__PURE__ */ jsx(BookmarkIcon, {}) }),
2398
+ /* @__PURE__ */ jsx(IconToggle, { label: "Highlight mode: tap a sentence", pressed: settings.highlightMode, onPressedChange: (highlightMode) => updateSettings({ highlightMode }), children: /* @__PURE__ */ jsx(HighlighterIcon, {}) }),
2399
+ /* @__PURE__ */ jsx(IconAction, { label: "Reading settings", onClick: () => setSettingsOpen(true), children: /* @__PURE__ */ jsx(TypeIcon, {}) }),
2400
+ padEnabled && /* @__PURE__ */ jsx(IconToggle, { label: "Scratchpad", pressed: padOpen, onPressedChange: (open) => open ? openPad() : setPadOpen(false), children: /* @__PURE__ */ jsx(NotebookPenIcon, {}) })
2401
+ ] }),
2402
+ /* @__PURE__ */ jsx(ReadingProgress, { store: engine.percentStore, className: "absolute inset-x-0 bottom-0" })
2403
+ ]
2404
+ }
2405
+ );
2406
+ const body = /* @__PURE__ */ jsxs("div", { className: cn("mx-auto w-full px-5 pt-8 pb-[calc(7rem+env(safe-area-inset-bottom))] sm:px-8 lg:pt-12", MEASURE_CLASS[settings.measure]), children: [
2407
+ /* @__PURE__ */ jsxs("p", { className: "flex flex-wrap items-center gap-x-2 text-sm text-muted-foreground", children: [
2408
+ chapter.part && /* @__PURE__ */ jsx("span", { children: chapter.part }),
2409
+ chapter.part && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
2410
+ /* @__PURE__ */ jsx("span", { children: chapter.label ?? `Chapter ${chapterIndex + 1} of ${book.chapters.length}` })
2411
+ ] }),
2412
+ /* @__PURE__ */ jsx("h1", { className: cn("mt-1 text-3xl font-semibold tracking-tight text-balance sm:text-4xl", FONT_CLASS[settings.fontFamily]), children: chapter.title }),
2413
+ /* @__PURE__ */ jsx(
2414
+ "article",
2415
+ {
2416
+ ref: setRootEl,
2417
+ className: cn("mt-8", PROSE_CLASS, TEXT_SIZE_CLASS[settings.textSize], LEADING_CLASS[settings.leading], FONT_CLASS[settings.fontFamily], settings.highlightMode && "cursor-pointer"),
2418
+ "aria-label": chapter.title,
2419
+ children: html !== void 0 ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } }) : children
2420
+ }
2421
+ ),
2422
+ /* @__PURE__ */ jsxs("footer", { className: "mt-16 flex flex-col gap-4 border-t pt-8", children: [
2423
+ !completed && /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", className: "self-center", onClick: () => store.markChapterComplete(book.id, chapter.id), children: [
2424
+ /* @__PURE__ */ jsx(CheckIcon, {}),
2425
+ " Mark chapter as read"
2426
+ ] }),
2427
+ /* @__PURE__ */ jsxs("nav", { "aria-label": "Chapters", className: "grid grid-cols-2 gap-3", children: [
2428
+ prev ? /* @__PURE__ */ jsx(Button, { asChild: true, variant: "ghost", className: "h-auto flex-col items-start gap-1 py-3 text-left whitespace-normal", children: /* @__PURE__ */ jsxs(Link, { href: href(prev.id), children: [
2429
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
2430
+ /* @__PURE__ */ jsx(ArrowLeftIcon, {}),
2431
+ " Previous"
2432
+ ] }),
2433
+ /* @__PURE__ */ jsx("span", { className: "line-clamp-2 font-medium", children: prev.title })
2434
+ ] }) }) : /* @__PURE__ */ jsx("span", {}),
2435
+ next ? /* @__PURE__ */ jsx(Button, { asChild: true, variant: "secondary", className: "col-start-2 h-auto flex-col items-end gap-1 py-3 text-right whitespace-normal", children: /* @__PURE__ */ jsxs(Link, { href: href(next.id), children: [
2436
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
2437
+ "Next ",
2438
+ /* @__PURE__ */ jsx(ArrowRightIcon, {})
2439
+ ] }),
2440
+ /* @__PURE__ */ jsx("span", { className: "line-clamp-2 font-medium", children: next.title })
2441
+ ] }) }) : null
2442
+ ] })
2443
+ ] })
2444
+ ] });
2445
+ const pad = (onClose) => /* @__PURE__ */ jsx(
2446
+ ScratchpadPanel,
2447
+ {
2448
+ bookId: book.id,
2449
+ chapterId: chapter.id,
2450
+ kind: padKind,
2451
+ onKindChange: setPadKind,
2452
+ onClose,
2453
+ onEditorFocus: () => setPadSnap(0.95),
2454
+ title: book.id
2455
+ }
2456
+ );
2457
+ const padPanelRef = usePanelRef();
2458
+ const layoutStorage = useMemo(
2459
+ () => ({ getItem: (k) => store.adapter.get(k), setItem: (k, v) => store.adapter.set(k, v) }),
2460
+ [store]
2461
+ );
2462
+ const layout = useDefaultLayout({ id: `${options.storagePrefix}:v1:layout`, panelIds: ["content", "pad"], storage: layoutStorage });
2463
+ useEffect(() => {
2464
+ const p = padPanelRef.current;
2465
+ if (!isDesktop || !p) return;
2466
+ if (padOpen && p.isCollapsed()) p.expand();
2467
+ if (!padOpen && !p.isCollapsed()) p.collapse();
2468
+ }, [padOpen, isDesktop, padPanelRef]);
2469
+ return /* @__PURE__ */ jsx(ReaderChapterContext.Provider, { value: chapterCtx, children: /* @__PURE__ */ jsxs("div", { "data-zr-reader": "", className: cn("min-h-svh bg-background text-foreground", isDesktop && "flex h-svh flex-col overflow-hidden", className), children: [
2470
+ topBar,
2471
+ isDesktop ? /* @__PURE__ */ jsxs(ResizablePanelGroup, { orientation: "horizontal", className: "min-h-0 flex-1", defaultLayout: layout.defaultLayout, onLayoutChanged: layout.onLayoutChanged, children: [
2472
+ /* @__PURE__ */ jsx(ResizablePanel, { id: "content", minSize: "40%", children: /* @__PURE__ */ jsx("div", { ref: setScrollEl, className: "h-full overflow-y-auto overscroll-contain", children: body }) }),
2473
+ padEnabled && /* @__PURE__ */ jsxs(Fragment, { children: [
2474
+ /* @__PURE__ */ jsx(ResizableHandle, { withHandle: true, className: cn(!padOpen && "hidden") }),
2475
+ /* @__PURE__ */ jsx(
2476
+ ResizablePanel,
2477
+ {
2478
+ id: "pad",
2479
+ panelRef: padPanelRef,
2480
+ collapsible: true,
2481
+ collapsedSize: "0%",
2482
+ defaultSize: padOpen ? "32%" : "0%",
2483
+ minSize: "22%",
2484
+ maxSize: "55%",
2485
+ onResize: (size) => setPadOpen(size.asPercentage > 0),
2486
+ className: "bg-background",
2487
+ children: padOpen && pad(() => setPadOpen(false))
2488
+ }
2489
+ )
2490
+ ] })
2491
+ ] }) : /* @__PURE__ */ jsx("main", { children: body }),
2492
+ !isDesktop && padEnabled && /* @__PURE__ */ jsx(Drawer, { open: padOpen, onOpenChange: setPadOpen, snapPoints: [0.55, 0.95], activeSnapPoint: padSnap, setActiveSnapPoint: setPadSnap, repositionInputs: false, children: /* @__PURE__ */ jsxs(DrawerContent, { className: "h-[95svh] max-h-[95svh]", children: [
2493
+ /* @__PURE__ */ jsx(DrawerTitle, { className: "sr-only", children: "Scratchpad" }),
2494
+ /* @__PURE__ */ jsx(DrawerDescription, { className: "sr-only", children: "Notes and checklists saved in this browser." }),
2495
+ /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col pb-[env(safe-area-inset-bottom)]", children: pad(() => setPadOpen(false)) })
2496
+ ] }) }),
2497
+ /* @__PURE__ */ jsx(
2498
+ SelectionToolbar,
2499
+ {
2500
+ selection: openHighlightId || settingsOpen || contentsOpen ? null : engine.selection,
2501
+ colors: options.highlightColors,
2502
+ onHighlight: (span, color) => {
2503
+ engine.createHighlight(span, color);
2504
+ engine.clearSelection();
2505
+ },
2506
+ onSentence: (span) => engine.expandSelectionToSentences(span),
2507
+ onNote: (span) => {
2508
+ const h = engine.createHighlight(span);
2509
+ engine.clearSelection();
2510
+ if (h) {
2511
+ setFocusNote(true);
2512
+ setOpenHighlightId(h.id);
2513
+ }
2514
+ },
2515
+ onQuote: padEnabled ? quoteSpan : void 0,
2516
+ onCopy: (text) => {
2517
+ void copy(text);
2518
+ engine.clearSelection();
2519
+ },
2520
+ onDismiss: engine.clearSelection
2521
+ }
2522
+ ),
2523
+ /* @__PURE__ */ jsx(
2524
+ HighlightPanel,
2525
+ {
2526
+ highlight: openHighlight,
2527
+ colors: options.highlightColors,
2528
+ focusNote,
2529
+ onColor: (color) => openHighlight && store.updateHighlight(book.id, openHighlight.id, { color }),
2530
+ onNote: (note) => openHighlight && store.updateHighlight(book.id, openHighlight.id, { note }),
2531
+ onDelete: () => {
2532
+ if (!openHighlight) return;
2533
+ const removed = openHighlight;
2534
+ store.removeHighlight(book.id, removed.id);
2535
+ setOpenHighlightId(null);
2536
+ engine.setActiveId(null);
2537
+ toast("Highlight deleted", {
2538
+ action: {
2539
+ label: "Undo",
2540
+ onClick: () => store.addHighlight({ bookId: removed.bookId, chapterId: removed.chapterId, anchor: removed.anchor, color: removed.color, note: removed.note })
2541
+ }
2542
+ });
2543
+ },
2544
+ onQuote: padEnabled && openHighlight ? () => quoteToPad(highlightToMarkdown(openHighlight, href(chapter.id, `hl-${openHighlight.id}`))) : void 0,
2545
+ onCopy: openHighlight ? () => void copy(openHighlight.anchor.exact) : void 0,
2546
+ onClose: () => {
2547
+ setOpenHighlightId(null);
2548
+ setFocusNote(false);
2549
+ engine.setActiveId(null);
2550
+ }
2551
+ }
2552
+ ),
2553
+ /* @__PURE__ */ jsx(
2554
+ ContentsSheet,
2555
+ {
2556
+ open: contentsOpen,
2557
+ onOpenChange: setContentsOpen,
2558
+ tab: contentsTab,
2559
+ onTabChange: setContentsTab,
2560
+ book,
2561
+ chapterId: chapter.id,
2562
+ toc,
2563
+ hrefFor: (c, h) => href(c, h),
2564
+ Link,
2565
+ orphanedIds,
2566
+ onJumpHighlight: (id) => engine.scrollToHighlight(id),
2567
+ onJumpBlock: (id) => engine.scrollToBlock(id),
2568
+ onJumpHeading: (id) => engine.scrollToHeading(id)
2569
+ }
2570
+ ),
2571
+ /* @__PURE__ */ jsx(SettingsPanel, { open: settingsOpen, onOpenChange: setSettingsOpen, book })
2572
+ ] }) });
2573
+ }
2574
+ function Card({ className, ...props }) {
2575
+ return /* @__PURE__ */ jsx(
2576
+ "div",
2577
+ {
2578
+ "data-slot": "card",
2579
+ className: cn(
2580
+ "flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
2581
+ className
2582
+ ),
2583
+ ...props
2584
+ }
2585
+ );
2586
+ }
2587
+ function CardHeader({ className, ...props }) {
2588
+ return /* @__PURE__ */ jsx(
2589
+ "div",
2590
+ {
2591
+ "data-slot": "card-header",
2592
+ className: cn(
2593
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
2594
+ className
2595
+ ),
2596
+ ...props
2597
+ }
2598
+ );
2599
+ }
2600
+ function CardTitle({ className, ...props }) {
2601
+ return /* @__PURE__ */ jsx(
2602
+ "div",
2603
+ {
2604
+ "data-slot": "card-title",
2605
+ className: cn("leading-none font-semibold", className),
2606
+ ...props
2607
+ }
2608
+ );
2609
+ }
2610
+ function CardDescription({ className, ...props }) {
2611
+ return /* @__PURE__ */ jsx(
2612
+ "div",
2613
+ {
2614
+ "data-slot": "card-description",
2615
+ className: cn("text-sm text-muted-foreground", className),
2616
+ ...props
2617
+ }
2618
+ );
2619
+ }
2620
+ function CardContent({ className, ...props }) {
2621
+ return /* @__PURE__ */ jsx(
2622
+ "div",
2623
+ {
2624
+ "data-slot": "card-content",
2625
+ className: cn("px-6", className),
2626
+ ...props
2627
+ }
2628
+ );
2629
+ }
2630
+ function CardFooter({ className, ...props }) {
2631
+ return /* @__PURE__ */ jsx(
2632
+ "div",
2633
+ {
2634
+ "data-slot": "card-footer",
2635
+ className: cn("flex items-center px-6 [.border-t]:pt-6", className),
2636
+ ...props
2637
+ }
2638
+ );
2639
+ }
2640
+ function useNav(bookArg, LinkProp, hrefFor) {
2641
+ const library = useLibrary();
2642
+ const book = useBook(bookArg);
2643
+ const build = hrefFor ?? library.href;
2644
+ return {
2645
+ book,
2646
+ Link: LinkProp ?? library.Link,
2647
+ href: (chapterId, hash) => build(book.id, chapterId, hash)
2648
+ };
2649
+ }
2650
+ function ContinueReading({ book: bookArg, Link: LinkProp, hrefFor, className }) {
2651
+ const { book, Link, href } = useNav(bookArg, LinkProp, hrefFor);
2652
+ const hydrated = useHydrated();
2653
+ const progress = useProgress(book.id);
2654
+ const highlights = useHighlights(book.id);
2655
+ const bookmarks = useBookmarks(book.id);
2656
+ const first = book.chapters[0];
2657
+ const lastId = progress.last?.chapterId;
2658
+ const last = book.chapters.find((c) => c.id === lastId);
2659
+ const done = book.chapters.filter((c) => progress.chapters[c.id]?.completed).length;
2660
+ const overall = book.chapters.length ? done / book.chapters.length : 0;
2661
+ const lastPct = last ? Math.round((progress.chapters[last.id]?.percent ?? 0) * 100) : 0;
2662
+ if (!first) return null;
2663
+ return /* @__PURE__ */ jsxs(Card, { className: cn("gap-4", className), children: [
2664
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
2665
+ /* @__PURE__ */ jsx(CardTitle, { children: last ? "Continue reading" : "Start reading" }),
2666
+ /* @__PURE__ */ jsx(CardDescription, { children: !hydrated ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-40" }) : last ? `${last.title} \xB7 ${lastPct}%` : `${book.chapters.length} chapters` })
2667
+ ] }),
2668
+ hydrated && (done > 0 || highlights.length > 0 || bookmarks.length > 0) && /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-3", children: [
2669
+ /* @__PURE__ */ jsx(Progress, { value: Math.round(overall * 100), "aria-label": "Book progress" }),
2670
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2 text-sm text-muted-foreground", children: [
2671
+ /* @__PURE__ */ jsxs(Badge, { variant: "secondary", children: [
2672
+ done,
2673
+ "/",
2674
+ book.chapters.length,
2675
+ " chapters read"
2676
+ ] }),
2677
+ highlights.length > 0 && /* @__PURE__ */ jsxs(Badge, { variant: "secondary", children: [
2678
+ highlights.length,
2679
+ " highlights"
2680
+ ] }),
2681
+ bookmarks.length > 0 && /* @__PURE__ */ jsxs(Badge, { variant: "secondary", children: [
2682
+ bookmarks.length,
2683
+ " bookmarks"
2684
+ ] })
2685
+ ] })
2686
+ ] }),
2687
+ /* @__PURE__ */ jsx(CardFooter, { children: /* @__PURE__ */ jsx(Button, { asChild: true, className: "w-full sm:w-auto", children: /* @__PURE__ */ jsxs(Link, { href: last ? href(last.id, progress.last?.blockId ? `b-${progress.last.blockId}` : void 0) : href(first.id), children: [
2688
+ /* @__PURE__ */ jsx(BookOpenIcon, {}),
2689
+ " ",
2690
+ last ? "Continue" : "Start"
2691
+ ] }) }) })
2692
+ ] });
2693
+ }
2694
+ function ChapterList({ book: bookArg, Link: LinkProp, hrefFor, className }) {
2695
+ const { book, Link, href } = useNav(bookArg, LinkProp, hrefFor);
2696
+ const hydrated = useHydrated();
2697
+ const progress = useProgress(book.id);
2698
+ return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-6", className), children: groupByPart(book.chapters).map((group) => /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-2", children: [
2699
+ group.part && /* @__PURE__ */ jsx("h3", { className: "text-sm font-medium text-muted-foreground", children: group.part }),
2700
+ /* @__PURE__ */ jsx("ol", { className: "flex flex-col divide-y overflow-hidden rounded-xl border", children: group.items.map(({ chapter: c, index: i }) => {
2701
+ const pr = hydrated ? progress.chapters[c.id] : void 0;
2702
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Button, { asChild: true, variant: "ghost", className: "h-auto w-full justify-start gap-4 rounded-none px-4 py-3 text-left whitespace-normal", children: /* @__PURE__ */ jsxs(Link, { href: href(c.id), children: [
2703
+ /* @__PURE__ */ jsx("span", { className: "w-6 shrink-0 text-sm text-muted-foreground tabular-nums", children: chapterMarker(c, i) }),
2704
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-1 flex-col gap-0.5", children: [
2705
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: c.title }),
2706
+ c.description && /* @__PURE__ */ jsx("span", { className: "text-sm font-normal text-muted-foreground", children: c.description })
2707
+ ] }),
2708
+ pr?.completed ? /* @__PURE__ */ jsx(CheckCircle2Icon, { className: "text-primary", "aria-label": "Read" }) : pr ? /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground tabular-nums", children: [
2709
+ Math.round(pr.percent * 100),
2710
+ "%"
2711
+ ] }) : /* @__PURE__ */ jsx(CircleIcon, { className: "text-muted-foreground/40", "aria-hidden": true })
2712
+ ] }) }) }, c.id);
2713
+ }) })
2714
+ ] }, group.part ?? "_")) });
2715
+ }
2716
+ function BookShelf({ books: booksProp, Link: LinkProp, hrefFor, title = "Books", description, embedded, className }) {
2717
+ const library = useLibrary();
2718
+ const books = booksProp ?? library.books;
2719
+ const Link = LinkProp ?? library.Link;
2720
+ const build = hrefFor ?? library.href;
2721
+ const hydrated = useHydrated();
2722
+ const grid = books.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "No books published yet." }) : /* @__PURE__ */ jsx("ul", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: books.map((book) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(BookCard, { book, Link, href: build(book.id), hydrated }) }, book.id)) });
2723
+ if (embedded) {
2724
+ return /* @__PURE__ */ jsx("div", { "data-zr-reader": "", className: cn("text-foreground", className), children: grid });
2725
+ }
2726
+ return /* @__PURE__ */ jsx("div", { "data-zr-reader": "", className: cn("min-h-svh bg-background text-foreground", className), children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex max-w-5xl flex-col gap-8 px-5 py-10 sm:px-8 sm:py-16", children: [
2727
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-2", children: [
2728
+ /* @__PURE__ */ jsx("h1", { className: "font-serif text-3xl font-semibold tracking-tight sm:text-4xl", children: title }),
2729
+ description && /* @__PURE__ */ jsx("p", { className: "max-w-prose text-muted-foreground", children: description })
2730
+ ] }),
2731
+ grid
2732
+ ] }) });
2733
+ }
2734
+ function BookCard({ book, Link, href, hydrated }) {
2735
+ const progress = useProgress(book.id);
2736
+ const done = hydrated ? book.chapters.filter((c) => progress.chapters[c.id]?.completed).length : 0;
2737
+ return /* @__PURE__ */ jsxs(Card, { className: "relative h-full gap-4 transition-shadow hover:shadow-md", children: [
2738
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
2739
+ /* @__PURE__ */ jsx(CardTitle, { className: "font-serif text-xl", children: /* @__PURE__ */ jsx(Link, { href, className: "after:absolute after:inset-0 focus-visible:outline-none", children: book.title }) }),
2740
+ book.authors.length > 0 && /* @__PURE__ */ jsx(CardDescription, { children: book.authors.join(", ") })
2741
+ ] }),
2742
+ book.description && /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "line-clamp-3 text-sm text-muted-foreground", children: book.description }) }),
2743
+ /* @__PURE__ */ jsxs(CardFooter, { className: "mt-auto flex items-center justify-between gap-2 text-sm text-muted-foreground", children: [
2744
+ /* @__PURE__ */ jsxs("span", { children: [
2745
+ book.chapters.length,
2746
+ " chapters"
2747
+ ] }),
2748
+ done > 0 && /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: done === book.chapters.length ? "Finished" : `${done} read` })
2749
+ ] })
2750
+ ] });
2751
+ }
2752
+ function BookHome({ book: bookArg, Link: LinkProp, hrefFor, backHref, backLabel = "All books", embedded, className }) {
2753
+ const { book, Link } = useNav(bookArg, LinkProp, hrefFor);
2754
+ if (embedded) {
2755
+ return /* @__PURE__ */ jsxs("div", { "data-zr-reader": "", className: cn("flex flex-col gap-8 text-foreground", className), children: [
2756
+ /* @__PURE__ */ jsx(ContinueReading, { book, Link, hrefFor }),
2757
+ /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-3", "aria-labelledby": "zr-chapters", children: [
2758
+ /* @__PURE__ */ jsx("h2", { id: "zr-chapters", className: "text-sm font-medium tracking-wide text-muted-foreground uppercase", children: "Chapters" }),
2759
+ /* @__PURE__ */ jsx(ChapterList, { book, Link, hrefFor })
2760
+ ] })
2761
+ ] });
2762
+ }
2763
+ return /* @__PURE__ */ jsx("div", { "data-zr-reader": "", className: cn("min-h-svh bg-background text-foreground", className), children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex max-w-3xl flex-col gap-8 px-5 py-8 sm:px-8 sm:py-14", children: [
2764
+ backHref && /* @__PURE__ */ jsx(Button, { asChild: true, variant: "ghost", size: "sm", className: "self-start", children: /* @__PURE__ */ jsxs(Link, { href: backHref, children: [
2765
+ /* @__PURE__ */ jsx(ChevronLeftIcon, {}),
2766
+ " ",
2767
+ backLabel
2768
+ ] }) }),
2769
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3", children: [
2770
+ /* @__PURE__ */ jsx("h1", { className: "font-serif text-3xl font-semibold tracking-tight text-balance sm:text-5xl", children: book.title }),
2771
+ book.subtitle && /* @__PURE__ */ jsx("p", { className: "font-serif text-xl text-balance text-foreground/80 sm:text-2xl", children: book.subtitle }),
2772
+ book.authors.length > 0 && /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: book.authors.join(", ") }),
2773
+ book.description && /* @__PURE__ */ jsx("p", { className: "max-w-prose text-lg text-foreground/80", children: book.description })
2774
+ ] }),
2775
+ /* @__PURE__ */ jsx(ContinueReading, { book, Link, hrefFor }),
2776
+ /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-3", "aria-labelledby": "zr-chapters", children: [
2777
+ /* @__PURE__ */ jsx("h2", { id: "zr-chapters", className: "text-sm font-medium tracking-wide text-muted-foreground uppercase", children: "Chapters" }),
2778
+ /* @__PURE__ */ jsx(ChapterList, { book, Link, hrefFor })
2779
+ ] })
2780
+ ] }) });
2781
+ }
2782
+ var BookContents = BookHome;
2783
+ function Callout({ title, children }) {
2784
+ return /* @__PURE__ */ jsxs(Alert, { className: "not-prose my-6", "data-zr-ignore": "", children: [
2785
+ /* @__PURE__ */ jsx(LightbulbIcon, {}),
2786
+ title && /* @__PURE__ */ jsx(AlertTitle, { children: title }),
2787
+ /* @__PURE__ */ jsx(AlertDescription, { children })
2788
+ ] });
2789
+ }
2790
+
2791
+ export { BookContents, BookHome, BookShelf, Callout, ChapterList, ContentsSheet, ContinueReading, DefaultLink, FONT_CLASS, HighlightPanel, LEADING_CLASS, MEASURE_CLASS, PROSE_CLASS, QUOTE_BORDER_CLASS, Reader, ReaderProvider, ReaderTitle, ResponsivePanel, SWATCH_CLASS, ScratchpadPanel, SelectionToolbar, SettingsPanel, TEXT_SIZE_CLASS, chapterMarker, copyText, defaultHref, downloadFile, groupByPart, mergeOptions, quoteBorderClass, scopeFor, scopeKey, swatchClass, useBook, useBookmarks, useChapterEngine, useHighlights, useHydrated, useIsDesktop, useLibrary, useMediaQuery, usePad, useProgress, useReader, useReaderChapter, useReaderStore, useSettings, useValueStore };
2792
+ //# sourceMappingURL=index.js.map
2793
+ //# sourceMappingURL=index.js.map