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