narrarium-astro-reader 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +52 -0
  2. package/astro.config.mjs +12 -0
  3. package/cli-dist/cli.d.ts +3 -0
  4. package/cli-dist/cli.d.ts.map +1 -0
  5. package/cli-dist/cli.js +78 -0
  6. package/cli-dist/cli.js.map +1 -0
  7. package/cli-dist/lib/assets.d.ts +8 -0
  8. package/cli-dist/lib/assets.d.ts.map +1 -0
  9. package/cli-dist/lib/assets.js +35 -0
  10. package/cli-dist/lib/assets.js.map +1 -0
  11. package/cli-dist/lib/book-config.d.ts +2 -0
  12. package/cli-dist/lib/book-config.d.ts.map +1 -0
  13. package/cli-dist/lib/book-config.js +2 -0
  14. package/cli-dist/lib/book-config.js.map +1 -0
  15. package/cli-dist/lib/book.d.ts +103 -0
  16. package/cli-dist/lib/book.d.ts.map +1 -0
  17. package/cli-dist/lib/book.js +89 -0
  18. package/cli-dist/lib/book.js.map +1 -0
  19. package/cli-dist/lib/canon.d.ts +16 -0
  20. package/cli-dist/lib/canon.d.ts.map +1 -0
  21. package/cli-dist/lib/canon.js +164 -0
  22. package/cli-dist/lib/canon.js.map +1 -0
  23. package/cli-dist/lib/glossary.d.ts +25 -0
  24. package/cli-dist/lib/glossary.d.ts.map +1 -0
  25. package/cli-dist/lib/glossary.js +195 -0
  26. package/cli-dist/lib/glossary.js.map +1 -0
  27. package/cli-dist/lib/search.d.ts +9 -0
  28. package/cli-dist/lib/search.d.ts.map +1 -0
  29. package/cli-dist/lib/search.js +55 -0
  30. package/cli-dist/lib/search.js.map +1 -0
  31. package/cli-dist/scaffold.d.ts +14 -0
  32. package/cli-dist/scaffold.d.ts.map +1 -0
  33. package/cli-dist/scaffold.js +190 -0
  34. package/cli-dist/scaffold.js.map +1 -0
  35. package/package.json +58 -0
  36. package/scripts/export-epub.mjs +13 -0
  37. package/src/cli.ts +96 -0
  38. package/src/components/AssetFigure.astro +17 -0
  39. package/src/components/ChapterPager.astro +40 -0
  40. package/src/components/LinkedValue.astro +18 -0
  41. package/src/components/MetadataSection.astro +22 -0
  42. package/src/components/ReaderRuntime.astro +310 -0
  43. package/src/components/RelatedLinks.astro +19 -0
  44. package/src/components/SiteSearch.astro +91 -0
  45. package/src/layouts/BaseLayout.astro +676 -0
  46. package/src/lib/assets.ts +44 -0
  47. package/src/lib/book-config.ts +1 -0
  48. package/src/lib/book.ts +116 -0
  49. package/src/lib/canon.ts +212 -0
  50. package/src/lib/glossary.ts +247 -0
  51. package/src/lib/search.ts +74 -0
  52. package/src/pages/chapters/[chapter].astro +102 -0
  53. package/src/pages/characters/[slug].astro +65 -0
  54. package/src/pages/characters/index.astro +45 -0
  55. package/src/pages/factions/[slug].astro +64 -0
  56. package/src/pages/factions/index.astro +45 -0
  57. package/src/pages/index.astro +129 -0
  58. package/src/pages/items/[slug].astro +63 -0
  59. package/src/pages/items/index.astro +45 -0
  60. package/src/pages/locations/[slug].astro +61 -0
  61. package/src/pages/locations/index.astro +45 -0
  62. package/src/pages/secrets/[slug].astro +67 -0
  63. package/src/pages/secrets/index.astro +46 -0
  64. package/src/pages/timeline/[slug].astro +58 -0
  65. package/src/pages/timeline/index.astro +52 -0
  66. package/src/scaffold.ts +232 -0
  67. package/tsconfig.json +6 -0
@@ -0,0 +1,676 @@
1
+ ---
2
+ import ReaderRuntime from "../components/ReaderRuntime.astro";
3
+ import SiteSearch from "../components/SiteSearch.astro";
4
+ import { loadCanonGlossary } from "../lib/glossary.js";
5
+ import { loadSearchIndex } from "../lib/search.js";
6
+
7
+ interface Props {
8
+ title: string;
9
+ description?: string;
10
+ activeSection?:
11
+ | "home"
12
+ | "chapters"
13
+ | "characters"
14
+ | "locations"
15
+ | "factions"
16
+ | "items"
17
+ | "secrets"
18
+ | "timeline";
19
+ }
20
+
21
+ const { title, description, activeSection = "home" } = Astro.props;
22
+ const canonGlossary = await loadCanonGlossary();
23
+ const searchIndex = await loadSearchIndex();
24
+ ---
25
+
26
+ <!doctype html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="utf-8" />
30
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
31
+ <title>{title}</title>
32
+ {description && <meta name="description" content={description} />}
33
+ <base href={import.meta.env.BASE_URL} />
34
+ <style>
35
+ :root {
36
+ --paper: #f3ecdd;
37
+ --paper-deep: #eadfcb;
38
+ --ink: #2c2018;
39
+ --muted: #705742;
40
+ --accent: #975f2d;
41
+ --line: rgba(44, 32, 24, 0.12);
42
+ --panel: rgba(255, 250, 240, 0.72);
43
+ --shadow: 0 24px 60px rgba(44, 32, 24, 0.12);
44
+ --surface-strong: rgba(255, 251, 243, 0.78);
45
+ }
46
+
47
+ html[data-theme="dark"] {
48
+ --paper: #171514;
49
+ --paper-deep: #0f0f10;
50
+ --ink: #f4ede1;
51
+ --muted: #c4b29b;
52
+ --accent: #d8a86b;
53
+ --line: rgba(255, 244, 230, 0.12);
54
+ --panel: rgba(36, 30, 28, 0.82);
55
+ --shadow: 0 28px 60px rgba(0, 0, 0, 0.32);
56
+ --surface-strong: rgba(33, 28, 27, 0.86);
57
+ }
58
+
59
+ * {
60
+ box-sizing: border-box;
61
+ }
62
+
63
+ html {
64
+ background:
65
+ radial-gradient(circle at top, rgba(255, 255, 255, 0.18), transparent 35%),
66
+ repeating-linear-gradient(
67
+ 180deg,
68
+ rgba(255, 255, 255, 0.08) 0,
69
+ rgba(255, 255, 255, 0.08) 2px,
70
+ transparent 2px,
71
+ transparent 6px
72
+ ),
73
+ linear-gradient(180deg, var(--paper), var(--paper-deep));
74
+ color: var(--ink);
75
+ font-family: Baskerville, "Palatino Linotype", "Book Antiqua", "Iowan Old Style", serif;
76
+ line-height: 1.65;
77
+ transition: background 180ms ease, color 180ms ease;
78
+ }
79
+
80
+ body {
81
+ margin: 0;
82
+ min-height: 100vh;
83
+ }
84
+
85
+ a {
86
+ color: inherit;
87
+ }
88
+
89
+ .shell {
90
+ width: min(1080px, calc(100vw - 2rem));
91
+ margin: 0 auto;
92
+ padding: 2rem 0 4rem;
93
+ }
94
+
95
+ .masthead {
96
+ display: flex;
97
+ align-items: center;
98
+ justify-content: space-between;
99
+ gap: 1rem;
100
+ margin-bottom: 1.25rem;
101
+ padding: 0.9rem 1rem;
102
+ border: 1px solid var(--line);
103
+ border-radius: 999px;
104
+ background: var(--surface-strong);
105
+ backdrop-filter: blur(8px);
106
+ box-shadow: var(--shadow);
107
+ }
108
+
109
+ .masthead-actions {
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: flex-end;
113
+ gap: 0.75rem;
114
+ flex-wrap: wrap;
115
+ }
116
+
117
+ .site-search {
118
+ position: relative;
119
+ min-width: min(360px, 84vw);
120
+ }
121
+
122
+ .site-search__field {
123
+ display: grid;
124
+ gap: 0.3rem;
125
+ }
126
+
127
+ .site-search__field input {
128
+ width: 100%;
129
+ padding: 0.78rem 0.95rem;
130
+ border-radius: 18px;
131
+ border: 1px solid var(--line);
132
+ background: rgba(255, 255, 255, 0.22);
133
+ color: var(--ink);
134
+ font: inherit;
135
+ }
136
+
137
+ .site-search__results {
138
+ position: absolute;
139
+ top: calc(100% + 0.45rem);
140
+ left: 0;
141
+ right: 0;
142
+ padding: 0.5rem;
143
+ border-radius: 18px;
144
+ border: 1px solid var(--line);
145
+ background: var(--surface-strong);
146
+ box-shadow: var(--shadow);
147
+ z-index: 25;
148
+ }
149
+
150
+ .site-search__result {
151
+ display: grid;
152
+ gap: 0.35rem;
153
+ padding: 0.75rem;
154
+ border-radius: 14px;
155
+ text-decoration: none;
156
+ }
157
+
158
+ .site-search__result:hover {
159
+ background: rgba(255, 255, 255, 0.12);
160
+ }
161
+
162
+ .brand {
163
+ font-size: 0.92rem;
164
+ letter-spacing: 0.14em;
165
+ text-transform: uppercase;
166
+ color: var(--muted);
167
+ text-decoration: none;
168
+ }
169
+
170
+ .site-nav {
171
+ display: flex;
172
+ flex-wrap: wrap;
173
+ gap: 0.5rem;
174
+ }
175
+
176
+ .theme-toggle {
177
+ padding: 0.45rem 0.8rem;
178
+ border-radius: 999px;
179
+ border: 1px solid var(--line);
180
+ background: rgba(255, 255, 255, 0.28);
181
+ color: var(--ink);
182
+ cursor: pointer;
183
+ font: inherit;
184
+ }
185
+
186
+ .nav-link {
187
+ padding: 0.45rem 0.8rem;
188
+ border-radius: 999px;
189
+ text-decoration: none;
190
+ color: var(--muted);
191
+ border: 1px solid transparent;
192
+ }
193
+
194
+ .nav-link.is-active {
195
+ color: var(--ink);
196
+ background: rgba(151, 95, 45, 0.12);
197
+ border-color: rgba(151, 95, 45, 0.18);
198
+ }
199
+
200
+ .hero {
201
+ padding: 3rem clamp(1.2rem, 2vw, 2rem);
202
+ border: 1px solid var(--line);
203
+ border-radius: 28px;
204
+ background: linear-gradient(135deg, rgba(255, 251, 243, 0.92), rgba(244, 233, 213, 0.8));
205
+ box-shadow: var(--shadow);
206
+ }
207
+
208
+ html[data-theme="dark"] .hero {
209
+ background: linear-gradient(135deg, rgba(42, 36, 34, 0.96), rgba(24, 22, 23, 0.88));
210
+ }
211
+
212
+ .eyebrow {
213
+ margin: 0 0 0.75rem;
214
+ text-transform: uppercase;
215
+ letter-spacing: 0.16em;
216
+ font-size: 0.78rem;
217
+ color: var(--muted);
218
+ }
219
+
220
+ h1,
221
+ h2,
222
+ h3 {
223
+ font-weight: 600;
224
+ line-height: 1.1;
225
+ }
226
+
227
+ h1 {
228
+ margin: 0;
229
+ font-size: clamp(2.4rem, 5vw, 4.8rem);
230
+ }
231
+
232
+ h2 {
233
+ font-size: clamp(1.5rem, 3vw, 2.25rem);
234
+ margin-top: 0;
235
+ }
236
+
237
+ .lede {
238
+ max-width: 62ch;
239
+ font-size: 1.06rem;
240
+ color: var(--muted);
241
+ }
242
+
243
+ .grid {
244
+ display: grid;
245
+ gap: 1rem;
246
+ }
247
+
248
+ .stats {
249
+ margin-top: 1.5rem;
250
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
251
+ }
252
+
253
+ .card,
254
+ .chapter-card,
255
+ .scene {
256
+ border: 1px solid var(--line);
257
+ border-radius: 22px;
258
+ background: var(--panel);
259
+ backdrop-filter: blur(8px);
260
+ box-shadow: var(--shadow);
261
+ }
262
+
263
+ .card a,
264
+ .chapter-card a,
265
+ .scene a {
266
+ color: inherit;
267
+ }
268
+
269
+ .card {
270
+ padding: 1rem 1.1rem;
271
+ }
272
+
273
+ .label {
274
+ font-size: 0.76rem;
275
+ text-transform: uppercase;
276
+ letter-spacing: 0.12em;
277
+ color: var(--muted);
278
+ }
279
+
280
+ .value {
281
+ margin-top: 0.35rem;
282
+ font-size: 1.15rem;
283
+ }
284
+
285
+ .chapter-list {
286
+ margin-top: 2rem;
287
+ display: grid;
288
+ gap: 1rem;
289
+ }
290
+
291
+ .catalog-grid {
292
+ display: grid;
293
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
294
+ gap: 1rem;
295
+ }
296
+
297
+ .catalog-card {
298
+ padding: 1.2rem 1.25rem;
299
+ }
300
+
301
+ .catalog-card a {
302
+ text-decoration: none;
303
+ }
304
+
305
+ .scene a,
306
+ .meta-value a {
307
+ color: var(--accent);
308
+ text-decoration-thickness: 1px;
309
+ text-underline-offset: 0.12em;
310
+ }
311
+
312
+ .chip-row {
313
+ display: flex;
314
+ flex-wrap: wrap;
315
+ gap: 0.45rem;
316
+ margin-top: 0.9rem;
317
+ }
318
+
319
+ .chip {
320
+ display: inline-flex;
321
+ align-items: center;
322
+ padding: 0.24rem 0.55rem;
323
+ border-radius: 999px;
324
+ border: 1px solid var(--line);
325
+ font-size: 0.82rem;
326
+ color: var(--muted);
327
+ background: rgba(255, 255, 255, 0.38);
328
+ text-decoration: none;
329
+ }
330
+
331
+ .canon-mention {
332
+ display: inline;
333
+ border: 0;
334
+ padding: 0;
335
+ margin: 0;
336
+ font: inherit;
337
+ color: var(--accent);
338
+ background: transparent;
339
+ cursor: pointer;
340
+ text-decoration: underline;
341
+ text-decoration-thickness: 1px;
342
+ text-underline-offset: 0.14em;
343
+ }
344
+
345
+ .chapter-pager {
346
+ display: grid;
347
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
348
+ gap: 0.8rem;
349
+ align-items: center;
350
+ margin-top: 1rem;
351
+ padding: 1rem;
352
+ border: 1px solid var(--line);
353
+ border-radius: 22px;
354
+ background: var(--panel);
355
+ box-shadow: var(--shadow);
356
+ }
357
+
358
+ .pager-link {
359
+ display: inline-flex;
360
+ align-items: center;
361
+ min-height: 100%;
362
+ padding: 0.8rem 0.95rem;
363
+ border-radius: 16px;
364
+ border: 1px solid var(--line);
365
+ text-decoration: none;
366
+ background: rgba(255, 255, 255, 0.22);
367
+ }
368
+
369
+ .pager-link.is-disabled {
370
+ opacity: 0.55;
371
+ pointer-events: none;
372
+ }
373
+
374
+ .pager-jump {
375
+ display: grid;
376
+ gap: 0.35rem;
377
+ }
378
+
379
+ .pager-jump select {
380
+ min-width: min(420px, 70vw);
381
+ padding: 0.7rem 0.9rem;
382
+ border-radius: 14px;
383
+ border: 1px solid var(--line);
384
+ background: rgba(255, 255, 255, 0.32);
385
+ color: var(--ink);
386
+ font: inherit;
387
+ }
388
+
389
+ .canon-overlay {
390
+ position: fixed;
391
+ inset: 0;
392
+ z-index: 50;
393
+ }
394
+
395
+ .canon-overlay__backdrop {
396
+ position: absolute;
397
+ inset: 0;
398
+ border: 0;
399
+ background: rgba(10, 10, 12, 0.58);
400
+ }
401
+
402
+ .canon-overlay__panel {
403
+ position: relative;
404
+ width: min(560px, calc(100vw - 1.5rem));
405
+ max-height: calc(100vh - 2rem);
406
+ overflow: auto;
407
+ margin: 1rem auto;
408
+ padding: 1.1rem 1.1rem 1.2rem;
409
+ border-radius: 24px;
410
+ border: 1px solid var(--line);
411
+ background: var(--surface-strong);
412
+ box-shadow: var(--shadow);
413
+ }
414
+
415
+ .canon-overlay__close {
416
+ position: sticky;
417
+ top: 0;
418
+ margin-left: auto;
419
+ display: flex;
420
+ align-items: center;
421
+ justify-content: center;
422
+ width: 2rem;
423
+ height: 2rem;
424
+ border-radius: 999px;
425
+ border: 1px solid var(--line);
426
+ background: rgba(255, 255, 255, 0.25);
427
+ color: var(--ink);
428
+ cursor: pointer;
429
+ }
430
+
431
+ .canon-overlay__media {
432
+ margin-bottom: 1rem;
433
+ }
434
+
435
+ .canon-overlay__media img {
436
+ display: block;
437
+ width: 100%;
438
+ height: auto;
439
+ border-radius: 20px;
440
+ border: 1px solid var(--line);
441
+ }
442
+
443
+ body.has-overlay {
444
+ overflow: hidden;
445
+ }
446
+
447
+ .chapter-card {
448
+ padding: 1.15rem 1.2rem;
449
+ transition: transform 140ms ease, box-shadow 140ms ease;
450
+ }
451
+
452
+ .chapter-card:hover {
453
+ transform: translateY(-2px);
454
+ box-shadow: 0 30px 70px rgba(44, 32, 24, 0.16);
455
+ }
456
+
457
+ .chapter-card a {
458
+ text-decoration: none;
459
+ }
460
+
461
+ .media-frame {
462
+ margin: 1.25rem 0 0;
463
+ }
464
+
465
+ .media-frame img {
466
+ display: block;
467
+ width: 100%;
468
+ height: auto;
469
+ border-radius: 24px;
470
+ border: 1px solid var(--line);
471
+ box-shadow: var(--shadow);
472
+ object-fit: cover;
473
+ background: rgba(255, 255, 255, 0.45);
474
+ }
475
+
476
+ .hero-media {
477
+ max-width: min(360px, 100%);
478
+ }
479
+
480
+ .scene-media {
481
+ max-width: min(420px, 100%);
482
+ }
483
+
484
+ .catalog-media {
485
+ margin: 0 0 0.95rem;
486
+ }
487
+
488
+ .catalog-media img {
489
+ aspect-ratio: 2 / 3;
490
+ max-height: 340px;
491
+ }
492
+
493
+ .canon-overlay__body {
494
+ margin: 1rem 0 1.25rem;
495
+ }
496
+
497
+ .canon-tabs {
498
+ display: flex;
499
+ flex-wrap: wrap;
500
+ gap: 0.45rem;
501
+ margin: 1rem 0;
502
+ }
503
+
504
+ .canon-tab {
505
+ padding: 0.45rem 0.75rem;
506
+ border-radius: 999px;
507
+ border: 1px solid var(--line);
508
+ background: rgba(255, 255, 255, 0.16);
509
+ color: var(--muted);
510
+ cursor: pointer;
511
+ font: inherit;
512
+ }
513
+
514
+ .canon-tab.is-active {
515
+ color: var(--ink);
516
+ background: rgba(151, 95, 45, 0.14);
517
+ }
518
+
519
+ .canon-panel {
520
+ display: none;
521
+ }
522
+
523
+ .canon-panel.is-active {
524
+ display: block;
525
+ }
526
+
527
+ .canon-overlay__image-panel {
528
+ min-height: 1px;
529
+ }
530
+
531
+ .canon-related {
532
+ margin-top: 1rem;
533
+ }
534
+
535
+ .canon-overlay__body h1,
536
+ .canon-overlay__body h2,
537
+ .canon-overlay__body h3 {
538
+ font-size: 1.15rem;
539
+ margin-top: 1rem;
540
+ }
541
+
542
+ .chapter-meta {
543
+ color: var(--muted);
544
+ font-size: 0.95rem;
545
+ }
546
+
547
+ .chapter-number {
548
+ font-size: 0.85rem;
549
+ text-transform: uppercase;
550
+ letter-spacing: 0.14em;
551
+ color: var(--accent);
552
+ }
553
+
554
+ .chapter-title {
555
+ font-size: 1.55rem;
556
+ margin: 0.4rem 0 0.5rem;
557
+ }
558
+
559
+ .section {
560
+ margin-top: 2rem;
561
+ }
562
+
563
+ .scene {
564
+ padding: 1.35rem 1.4rem;
565
+ margin-top: 1rem;
566
+ }
567
+
568
+ .scene h3 {
569
+ margin: 0 0 0.5rem;
570
+ }
571
+
572
+ .prose :global(p:first-child) {
573
+ margin-top: 0;
574
+ }
575
+
576
+ .meta-list {
577
+ display: grid;
578
+ gap: 0.65rem;
579
+ }
580
+
581
+ .meta-row {
582
+ display: grid;
583
+ grid-template-columns: minmax(120px, 180px) 1fr;
584
+ gap: 0.75rem;
585
+ padding-bottom: 0.65rem;
586
+ border-bottom: 1px solid var(--line);
587
+ }
588
+
589
+ .meta-key {
590
+ color: var(--muted);
591
+ text-transform: uppercase;
592
+ letter-spacing: 0.1em;
593
+ font-size: 0.78rem;
594
+ }
595
+
596
+ .meta-value {
597
+ min-width: 0;
598
+ }
599
+
600
+ .empty {
601
+ margin-top: 2rem;
602
+ padding: 1.2rem 1.3rem;
603
+ border-radius: 22px;
604
+ border: 1px dashed var(--line);
605
+ background: rgba(255, 255, 255, 0.34);
606
+ color: var(--muted);
607
+ }
608
+
609
+ @media (max-width: 720px) {
610
+ .shell {
611
+ width: min(100vw - 1rem, 1080px);
612
+ padding-top: 1rem;
613
+ }
614
+
615
+ .hero,
616
+ .scene,
617
+ .chapter-card,
618
+ .card {
619
+ border-radius: 18px;
620
+ }
621
+
622
+ .masthead {
623
+ border-radius: 24px;
624
+ align-items: flex-start;
625
+ flex-direction: column;
626
+ }
627
+
628
+ .masthead-actions {
629
+ width: 100%;
630
+ align-items: stretch;
631
+ }
632
+
633
+ .theme-toggle {
634
+ width: 100%;
635
+ }
636
+
637
+ .chapter-pager {
638
+ grid-template-columns: 1fr;
639
+ }
640
+
641
+ .pager-jump select {
642
+ min-width: 0;
643
+ width: 100%;
644
+ }
645
+
646
+ .meta-row {
647
+ grid-template-columns: 1fr;
648
+ gap: 0.3rem;
649
+ }
650
+ }
651
+ </style>
652
+ </head>
653
+ <body>
654
+ <main class="shell">
655
+ <header class="masthead">
656
+ <a class="brand" href="./">Narrarium Reader</a>
657
+ <div class="masthead-actions">
658
+ <SiteSearch entries={searchIndex} />
659
+ <nav class="site-nav">
660
+ <a class:list={["nav-link", activeSection === "home" && "is-active"]} href="./">Book</a>
661
+ <a class:list={["nav-link", activeSection === "chapters" && "is-active"]} href="./#chapters">Chapters</a>
662
+ <a class:list={["nav-link", activeSection === "characters" && "is-active"]} href="characters/">Characters</a>
663
+ <a class:list={["nav-link", activeSection === "locations" && "is-active"]} href="locations/">Locations</a>
664
+ <a class:list={["nav-link", activeSection === "factions" && "is-active"]} href="factions/">Factions</a>
665
+ <a class:list={["nav-link", activeSection === "items" && "is-active"]} href="items/">Items</a>
666
+ <a class:list={["nav-link", activeSection === "secrets" && "is-active"]} href="secrets/">Secrets</a>
667
+ <a class:list={["nav-link", activeSection === "timeline" && "is-active"]} href="timeline/">Timeline</a>
668
+ </nav>
669
+ <button type="button" class="theme-toggle" data-theme-toggle aria-label="Switch theme">Dark mode</button>
670
+ </div>
671
+ </header>
672
+ <slot />
673
+ </main>
674
+ <ReaderRuntime glossary={canonGlossary} />
675
+ </body>
676
+ </html>
@@ -0,0 +1,44 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { readAsset } from "narrarium";
4
+ import { getBookRoot } from "./book.js";
5
+
6
+ export type ReaderFigure = {
7
+ src: string;
8
+ alt: string;
9
+ aspectRatio: string;
10
+ orientation: "portrait" | "landscape" | "square";
11
+ };
12
+
13
+ export async function loadAssetFigure(subject: string, alt: string, assetKind?: string): Promise<ReaderFigure | null> {
14
+ const asset = await readAsset(getBookRoot(), subject, assetKind);
15
+ if (!asset || !asset.imageExists) {
16
+ return null;
17
+ }
18
+
19
+ const buffer = await readFile(asset.imagePath);
20
+ return {
21
+ src: `data:${mimeTypeForExtension(path.extname(asset.imagePath))};base64,${buffer.toString("base64")}`,
22
+ alt,
23
+ aspectRatio: asset.metadata.aspect_ratio,
24
+ orientation: asset.metadata.orientation,
25
+ };
26
+ }
27
+
28
+ function mimeTypeForExtension(extension: string): string {
29
+ switch (extension.toLowerCase()) {
30
+ case ".jpg":
31
+ case ".jpeg":
32
+ return "image/jpeg";
33
+ case ".webp":
34
+ return "image/webp";
35
+ case ".gif":
36
+ return "image/gif";
37
+ case ".avif":
38
+ return "image/avif";
39
+ case ".svg":
40
+ return "image/svg+xml";
41
+ default:
42
+ return "image/png";
43
+ }
44
+ }
@@ -0,0 +1 @@
1
+ export const defaultBookRoot = "../../example-book";