seemore 1.8.7 → 1.9.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.
@@ -0,0 +1,83 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import { ChevronDown, Download, Printer } from 'lucide-react';
4
+ import { config } from 'virtual:seemore/config';
5
+ import type { ActionId } from '../../shared/types.js';
6
+ import { exportPageAsHtml, printPageAsPdf } from '../export/exportPage.js';
7
+
8
+ /**
9
+ * The actions the button can hold, keyed by the id a config's `pageActions` array names.
10
+ * A new action is a new id in `ACTION_IDS`, one entry here, and the menu picks it up —
11
+ * no component changes.
12
+ */
13
+ const ACTIONS: Record<ActionId, { label: string; icon: ReactNode; run: () => Promise<void> }> = {
14
+ 'export-html': { label: 'Export as HTML', icon: <Download aria-hidden="true" />, run: exportPageAsHtml },
15
+ 'export-pdf': { label: 'Export as PDF', icon: <Printer aria-hidden="true" />, run: printPageAsPdf },
16
+ };
17
+
18
+ /**
19
+ * The page-actions button: one dropdown above the article, holding the actions the site
20
+ * enables, in the order the config lists them. Chrome, not content — print hides it, and
21
+ * the export never touches it because the export takes the article only.
22
+ */
23
+ export function PageActions() {
24
+ const [open, setOpen] = useState(false);
25
+ const root = useRef<HTMLDivElement>(null);
26
+
27
+ useEffect(() => {
28
+ if (!open) return;
29
+ const onPointerDown = (event: PointerEvent) => {
30
+ if (root.current !== null && !root.current.contains(event.target as Node)) setOpen(false);
31
+ };
32
+ const onKeyDown = (event: KeyboardEvent) => {
33
+ if (event.key === 'Escape') setOpen(false);
34
+ };
35
+ document.addEventListener('pointerdown', onPointerDown);
36
+ document.addEventListener('keydown', onKeyDown);
37
+ return () => {
38
+ document.removeEventListener('pointerdown', onPointerDown);
39
+ document.removeEventListener('keydown', onKeyDown);
40
+ };
41
+ }, [open]);
42
+
43
+ const ids = config.pageActions.filter((id) => id in ACTIONS);
44
+ if (ids.length === 0) return undefined;
45
+
46
+ return (
47
+ <div className="seemore-page-actions" ref={root}>
48
+ <button
49
+ type="button"
50
+ className="seemore-page-actions-trigger"
51
+ aria-haspopup="menu"
52
+ aria-expanded={open}
53
+ onClick={() => setOpen((value) => !value)}
54
+ >
55
+ Actions
56
+ <ChevronDown aria-hidden="true" />
57
+ </button>
58
+
59
+ {open ? (
60
+ <div className="seemore-page-actions-menu" role="menu">
61
+ {ids.map((id) => (
62
+ <button
63
+ key={id}
64
+ type="button"
65
+ role="menuitem"
66
+ className="seemore-page-actions-item"
67
+ onClick={() => {
68
+ setOpen(false);
69
+ void ACTIONS[id].run().catch((cause: unknown) => {
70
+ const message = cause instanceof Error ? cause.message : String(cause);
71
+ window.alert(`Could not run this action: ${message}`);
72
+ });
73
+ }}
74
+ >
75
+ {ACTIONS[id].icon}
76
+ {ACTIONS[id].label}
77
+ </button>
78
+ ))}
79
+ </div>
80
+ ) : undefined}
81
+ </div>
82
+ );
83
+ }
@@ -467,6 +467,41 @@
467
467
  .seemore-pdf-unsupported .seemore-pdf-fallback-icon {
468
468
  @apply size-6;
469
469
  }
470
+
471
+ /* Page actions: the row above the article holding the export dropdown. Right-aligned so
472
+ it reads as chrome, not content, and stays clear of the heading it hangs over. */
473
+ .seemore-page-actions {
474
+ @apply relative mb-4 flex justify-end;
475
+ }
476
+
477
+ .seemore-page-actions-trigger {
478
+ @apply inline-flex items-center gap-1.5 rounded-lg border border-fd-border px-3 py-1.5 text-sm text-fd-muted-foreground transition-colors;
479
+ }
480
+
481
+ .seemore-page-actions-trigger:hover,
482
+ .seemore-page-actions-trigger[aria-expanded='true'] {
483
+ @apply bg-fd-accent text-fd-accent-foreground;
484
+ }
485
+
486
+ .seemore-page-actions-trigger svg {
487
+ @apply size-4;
488
+ }
489
+
490
+ .seemore-page-actions-menu {
491
+ @apply absolute end-0 z-50 mt-1.5 min-w-44 rounded-lg border border-fd-border bg-fd-background p-1 shadow-lg;
492
+ }
493
+
494
+ .seemore-page-actions-item {
495
+ @apply flex w-full cursor-pointer items-center gap-2 rounded-md border-none bg-transparent px-2.5 py-1.5 text-sm text-fd-muted-foreground transition-colors;
496
+ }
497
+
498
+ .seemore-page-actions-item:hover {
499
+ @apply bg-fd-accent text-fd-accent-foreground;
500
+ }
501
+
502
+ .seemore-page-actions-item svg {
503
+ @apply size-4 shrink-0;
504
+ }
470
505
  }
471
506
 
472
507
  /* Unlayered, deliberately: fumadocs' `max-h-[460px]` utility lives in Tailwind's utilities
@@ -488,5 +523,205 @@
488
523
  text-decoration-line: none;
489
524
  }
490
525
 
526
+ /* The single-page export reuses this same stylesheet — the exported file inlines it — so
527
+ the classes below only ever appear in the exported document, and cost the live site a
528
+ few hundred unused bytes. They are unlayered so nothing from a theme preset can beat
529
+ them inside a file that has no other seemore rules to lean on. */
530
+
531
+ /* The exported page's body column: the article alone, at reading width, centred — the
532
+ centring is what keeps slack space symmetric at any viewport width instead of piling
533
+ up on one side. `width: 100%` is load-bearing: the theme's body is a column flex
534
+ container, and auto margins alone would switch the block from stretch to
535
+ shrink-to-fit, letting wide content stretch it past the viewport. */
536
+ .seemore-export-main {
537
+ margin-inline: auto;
538
+ max-width: 56rem;
539
+ overflow-x: clip;
540
+ padding: 2rem 1rem 4rem;
541
+ width: 100%;
542
+ }
543
+
544
+
545
+ /* Zoom affordance in the export, where the site's zoom runtime is the fallback overlay. */
546
+ .seemore-export-main img {
547
+ cursor: zoom-in;
548
+ }
549
+
550
+ .seemore-export-main a img {
551
+ cursor: pointer;
552
+ }
553
+
554
+ /* The "On this page" block generated for the export: the live site's TOC transplanted to
555
+ a page with no layout columns.
556
+
557
+ Two shapes, one markup. On narrow screens it sits in flow under the title as a
558
+ collapsible — fumadocs' own inline-TOC pattern. From 1280px up, where the viewport has
559
+ room, it becomes a floating rail on the right edge and the runtime opens it.
560
+
561
+ Styled after fumadocs' rail — small muted label, text-sm links, the indicator line per
562
+ nesting level — using the theme's real tokens. The rail itself is a React component
563
+ that draws its connector as SVG from live layout measurements; a static file gets the
564
+ same look from plain borders instead. */
565
+ .seemore-export-toc {
566
+ font-size: 0.875rem;
567
+ margin-block: 1.5rem;
568
+ }
569
+
570
+ .seemore-export-toc summary {
571
+ color: var(--color-fd-muted-foreground, #8b8b8b);
572
+ cursor: pointer;
573
+ font-weight: 500;
574
+ list-style: none;
575
+ margin: 0 0 0.5rem;
576
+ }
577
+
578
+ .seemore-export-toc summary::-webkit-details-marker {
579
+ display: none;
580
+ }
581
+
582
+ .seemore-export-toc summary::after {
583
+ border-block-end: 1px solid currentColor;
584
+ border-inline-end: 1px solid currentColor;
585
+ content: '';
586
+ display: inline-block;
587
+ height: 0.3rem;
588
+ margin-inline-start: 0.5rem;
589
+ transform: rotate(-45deg);
590
+ transition: transform 0.15s;
591
+ vertical-align: middle;
592
+ width: 0.3rem;
593
+ }
594
+
595
+ .seemore-export-toc details[open] > summary::after {
596
+ transform: rotate(45deg);
597
+ }
598
+
599
+ .seemore-export-toc ul {
600
+ border-inline-start: 1px solid var(--color-fd-border, #e5e7eb);
601
+ list-style: none;
602
+ margin: 0;
603
+ padding-inline-start: 1rem;
604
+ }
605
+
606
+ .seemore-export-toc li {
607
+ margin-block: 0.375rem;
608
+ }
609
+
610
+ .seemore-export-toc a {
611
+ color: var(--color-fd-muted-foreground, #8b8b8b);
612
+ display: block;
613
+ text-decoration: none;
614
+ }
615
+
616
+ .seemore-export-toc a:hover,
617
+ .seemore-export-toc a[data-active='true'] {
618
+ color: var(--color-fd-foreground, #111827);
619
+ }
620
+
621
+ @media (min-width: 1280px) {
622
+ .seemore-export-toc {
623
+ inset-inline-end: 2rem;
624
+ margin-block: 0;
625
+ max-height: calc(100vh - 8rem);
626
+ overflow-y: auto;
627
+ position: fixed;
628
+ top: 5rem;
629
+ width: 14rem;
630
+ z-index: 40;
631
+ }
632
+
633
+ /* The rail is always a rail: no disclosure chevron in it. */
634
+ .seemore-export-toc summary {
635
+ cursor: default;
636
+ margin-bottom: 0.75rem;
637
+ }
638
+
639
+ .seemore-export-toc summary::after {
640
+ display: none;
641
+ }
642
+ }
643
+
644
+ /* The export's only chrome: a floating theme toggle, since the header is gone. */
645
+ .seemore-export-theme-toggle {
646
+ align-items: center;
647
+ background: var(--color-fd-background, #fff);
648
+ border: 1px solid var(--color-fd-border, #e5e7eb);
649
+ border-radius: 0.5rem;
650
+ cursor: pointer;
651
+ display: flex;
652
+ padding: 0.5rem;
653
+ position: fixed;
654
+ top: 1rem;
655
+ z-index: 50;
656
+ inset-inline-end: 1rem;
657
+ }
658
+
659
+ .seemore-export-theme-toggle svg {
660
+ height: 1rem;
661
+ width: 1rem;
662
+ }
663
+
664
+ /* Click-to-zoom overlay for the exported file, where the site's zoom runtime is absent. */
665
+ .seemore-export-overlay {
666
+ align-items: center;
667
+ background: rgb(0 0 0 / 80%);
668
+ cursor: zoom-out;
669
+ display: grid;
670
+ inset: 0;
671
+ justify-items: center;
672
+ position: fixed;
673
+ z-index: 100;
674
+ }
675
+
676
+ .seemore-export-overlay img {
677
+ max-height: 92vh;
678
+ max-width: 92vw;
679
+ object-fit: contain;
680
+ }
681
+
682
+ /* Printing — the PDF path. Every pixel of chrome goes, the article gets the whole page,
683
+ and nothing reader-consumed is ever split across a page break. */
684
+ @media print {
685
+ .seemore-header,
686
+ .seemore-sidebar,
687
+ .seemore-toc,
688
+ .seemore-export-toc,
689
+ .seemore-breadcrumb,
690
+ .seemore-edit-link,
691
+ .seemore-page-footer,
692
+ .seemore-site-footer,
693
+ .seemore-back-to-top,
694
+ .seemore-page-actions,
695
+ .seemore-selection-copy,
696
+ .seemore-preview,
697
+ .seemore-export-theme-toggle,
698
+ .seemore-export-overlay {
699
+ display: none !important;
700
+ }
701
+
702
+ .seemore-body,
703
+ .seemore-main,
704
+ .seemore-article {
705
+ display: block;
706
+ margin: 0;
707
+ max-width: none;
708
+ padding: 0;
709
+ }
710
+
711
+ body {
712
+ background: #fff;
713
+ }
714
+
715
+ pre,
716
+ .seemore-mermaid,
717
+ .seemore-d2,
718
+ .seemore-pdf,
719
+ img,
720
+ table,
721
+ blockquote {
722
+ break-inside: avoid;
723
+ }
724
+ }
725
+
491
726
  /* seemore:user-css — the stylesheet named by `css` in seemore.config.ts is inlined here, at
492
727
  the very end, so that it wins against everything above it. */
@@ -44,6 +44,14 @@ export type ClientSearchConfig =
44
44
  | { provider: 'algolia'; appId: string; apiKey: string; indexName: string };
45
45
 
46
46
  /** The payload of `virtual:seemore/config`. */
47
+ /**
48
+ * The actions a page-actions button can hold, by id. Presence in the `actions` array is
49
+ * what enables an action; the array order is the menu order.
50
+ */
51
+ export const ACTION_IDS = ['export-html', 'export-pdf'] as const;
52
+
53
+ export type ActionId = (typeof ACTION_IDS)[number];
54
+
47
55
  export interface ClientConfig {
48
56
  title: string;
49
57
  description?: string;
@@ -54,6 +62,7 @@ export interface ClientConfig {
54
62
  footer?: { text?: string; links?: { text: string; link: string }[] };
55
63
  editLink?: { base: string; text: string };
56
64
  favicon?: string;
65
+ pageActions: ActionId[];
57
66
  search: ClientSearchConfig;
58
67
  contentRoot: string;
59
68
  }