@gmickel/gno 0.33.4 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/package.json +1 -1
- package/src/serve/browse-tree.ts +201 -0
- package/src/serve/public/app.tsx +38 -14
- package/src/serve/public/components/BacklinksPanel.tsx +10 -10
- package/src/serve/public/components/BrowseDetailPane.tsx +194 -0
- package/src/serve/public/components/BrowseOverview.tsx +81 -0
- package/src/serve/public/components/BrowseTreeSidebar.tsx +281 -0
- package/src/serve/public/components/BrowseWorkspaceCard.tsx +96 -0
- package/src/serve/public/components/FrontmatterDisplay.tsx +164 -65
- package/src/serve/public/components/OutgoingLinksPanel.tsx +8 -12
- package/src/serve/public/components/RelatedNotesSidebar.tsx +42 -76
- package/src/serve/public/components/editor/MarkdownPreview.tsx +61 -2
- package/src/serve/public/components/ui/button.tsx +1 -1
- package/src/serve/public/components/ui/dialog.tsx +1 -1
- package/src/serve/public/hooks/useWorkspace.tsx +38 -0
- package/src/serve/public/lib/browse.ts +110 -0
- package/src/serve/public/lib/workspace-tabs.ts +59 -1
- package/src/serve/public/pages/Browse.tsx +334 -344
- package/src/serve/public/pages/DocView.tsx +484 -296
- package/src/serve/public/pages/DocumentEditor.tsx +1 -11
- package/src/serve/public/pages/GraphView.tsx +5 -4
- package/src/serve/routes/api.ts +47 -0
- package/src/serve/server.ts +5 -0
- package/src/store/sqlite/adapter.ts +240 -241
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
Loader2Icon,
|
|
14
14
|
PencilIcon,
|
|
15
15
|
SquareArrowOutUpRightIcon,
|
|
16
|
-
TagIcon,
|
|
17
16
|
TextIcon,
|
|
18
17
|
TrashIcon,
|
|
19
18
|
} from "lucide-react";
|
|
@@ -30,17 +29,15 @@ import {
|
|
|
30
29
|
FrontmatterDisplay,
|
|
31
30
|
parseFrontmatter,
|
|
32
31
|
} from "../components/FrontmatterDisplay";
|
|
33
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
OutgoingLinksPanel,
|
|
34
|
+
type OutgoingLink,
|
|
35
|
+
} from "../components/OutgoingLinksPanel";
|
|
34
36
|
import { RelatedNotesSidebar } from "../components/RelatedNotesSidebar";
|
|
35
37
|
import { TagInput } from "../components/TagInput";
|
|
36
38
|
import { Badge } from "../components/ui/badge";
|
|
37
39
|
import { Button } from "../components/ui/button";
|
|
38
|
-
import {
|
|
39
|
-
Card,
|
|
40
|
-
CardContent,
|
|
41
|
-
CardHeader,
|
|
42
|
-
CardTitle,
|
|
43
|
-
} from "../components/ui/card";
|
|
40
|
+
import { Card, CardContent } from "../components/ui/card";
|
|
44
41
|
import {
|
|
45
42
|
Dialog,
|
|
46
43
|
DialogContent,
|
|
@@ -246,6 +243,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
246
243
|
const [savingTags, setSavingTags] = useState(false);
|
|
247
244
|
const [tagSaveError, setTagSaveError] = useState<string | null>(null);
|
|
248
245
|
const [tagSaveSuccess, setTagSaveSuccess] = useState(false);
|
|
246
|
+
const [resolvedWikiLinks, setResolvedWikiLinks] = useState<OutgoingLink[]>(
|
|
247
|
+
[]
|
|
248
|
+
);
|
|
249
249
|
|
|
250
250
|
// Request sequencing - ignore stale responses on rapid navigation
|
|
251
251
|
const requestIdRef = useRef(0);
|
|
@@ -302,6 +302,19 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
302
302
|
loadDocument();
|
|
303
303
|
}, [loadDocument]);
|
|
304
304
|
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (!doc?.docid) {
|
|
307
|
+
setResolvedWikiLinks([]);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
void apiFetch<{ links: OutgoingLink[] }>(
|
|
312
|
+
`/api/doc/${encodeURIComponent(doc.docid)}/links?type=wiki`
|
|
313
|
+
).then(({ data }) => {
|
|
314
|
+
setResolvedWikiLinks(data?.links ?? []);
|
|
315
|
+
});
|
|
316
|
+
}, [doc?.docid]);
|
|
317
|
+
|
|
305
318
|
useEffect(() => {
|
|
306
319
|
if (latestDocEvent?.uri !== currentUri) {
|
|
307
320
|
return;
|
|
@@ -350,6 +363,7 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
350
363
|
}, [doc?.content, isMarkdown]);
|
|
351
364
|
|
|
352
365
|
const hasFrontmatter = Object.keys(parsedContent.data).length > 0;
|
|
366
|
+
const showStandaloneTags = !hasFrontmatter || editingTags;
|
|
353
367
|
|
|
354
368
|
useEffect(() => {
|
|
355
369
|
if (currentTarget.view === "source" || currentTarget.lineStart) {
|
|
@@ -535,6 +549,360 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
535
549
|
setTimeout(() => setTagSaveSuccess(false), 2000);
|
|
536
550
|
}, [doc, editedTags]);
|
|
537
551
|
|
|
552
|
+
/** Slim left rail — archival catalogue style, no Card chrome */
|
|
553
|
+
const renderDocumentFactsRail = () => (
|
|
554
|
+
<nav aria-label="Document facts" className="space-y-0">
|
|
555
|
+
{/* Section: Properties */}
|
|
556
|
+
<div className="px-3 pb-3">
|
|
557
|
+
<div className="mb-2.5 font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
558
|
+
Properties
|
|
559
|
+
</div>
|
|
560
|
+
<dl className="space-y-2.5 text-[13px]">
|
|
561
|
+
<div className="flex items-center gap-2">
|
|
562
|
+
<FolderOpen className="size-3.5 shrink-0 text-muted-foreground/60" />
|
|
563
|
+
<dt className="sr-only">Collection</dt>
|
|
564
|
+
<dd className="truncate font-medium">
|
|
565
|
+
{doc?.collection || "Unknown"}
|
|
566
|
+
</dd>
|
|
567
|
+
</div>
|
|
568
|
+
{doc?.source.sizeBytes !== undefined && (
|
|
569
|
+
<div className="flex items-center gap-2">
|
|
570
|
+
<HardDrive className="size-3.5 shrink-0 text-muted-foreground/60" />
|
|
571
|
+
<dt className="sr-only">Size</dt>
|
|
572
|
+
<dd className="font-mono text-muted-foreground">
|
|
573
|
+
{formatBytes(doc.source.sizeBytes)}
|
|
574
|
+
</dd>
|
|
575
|
+
</div>
|
|
576
|
+
)}
|
|
577
|
+
{doc?.source.modifiedAt && (
|
|
578
|
+
<div className="flex items-center gap-2">
|
|
579
|
+
<Calendar className="size-3.5 shrink-0 text-muted-foreground/60" />
|
|
580
|
+
<dt className="sr-only">Modified</dt>
|
|
581
|
+
<dd className="text-muted-foreground">
|
|
582
|
+
{formatDate(doc.source.modifiedAt)}
|
|
583
|
+
</dd>
|
|
584
|
+
</div>
|
|
585
|
+
)}
|
|
586
|
+
</dl>
|
|
587
|
+
</div>
|
|
588
|
+
|
|
589
|
+
{/* Divider */}
|
|
590
|
+
<div className="mx-3 border-border/20 border-t" />
|
|
591
|
+
|
|
592
|
+
{/* Section: Path */}
|
|
593
|
+
<div className="px-3 py-3">
|
|
594
|
+
<div className="mb-1.5 font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
595
|
+
Path
|
|
596
|
+
</div>
|
|
597
|
+
<code className="block break-all font-mono text-[11px] leading-relaxed text-muted-foreground/70">
|
|
598
|
+
{doc?.uri}
|
|
599
|
+
</code>
|
|
600
|
+
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
|
601
|
+
{currentTarget.lineStart && (
|
|
602
|
+
<span className="rounded bg-muted/30 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
|
|
603
|
+
L{currentTarget.lineStart}
|
|
604
|
+
{currentTarget.lineEnd &&
|
|
605
|
+
currentTarget.lineEnd !== currentTarget.lineStart
|
|
606
|
+
? `-${currentTarget.lineEnd}`
|
|
607
|
+
: ""}
|
|
608
|
+
</span>
|
|
609
|
+
)}
|
|
610
|
+
<button
|
|
611
|
+
className="inline-flex cursor-pointer items-center gap-1 rounded px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground/60 transition-colors hover:bg-muted/20 hover:text-muted-foreground"
|
|
612
|
+
onClick={() => {
|
|
613
|
+
if (!doc) return;
|
|
614
|
+
void navigator.clipboard.writeText(
|
|
615
|
+
`${window.location.origin}${buildDocDeepLink({
|
|
616
|
+
uri: doc.uri,
|
|
617
|
+
view:
|
|
618
|
+
currentTarget.view === "source" || currentTarget.lineStart
|
|
619
|
+
? "source"
|
|
620
|
+
: "rendered",
|
|
621
|
+
lineStart: currentTarget.lineStart,
|
|
622
|
+
lineEnd: currentTarget.lineEnd,
|
|
623
|
+
})}`
|
|
624
|
+
);
|
|
625
|
+
}}
|
|
626
|
+
type="button"
|
|
627
|
+
>
|
|
628
|
+
<LinkIcon className="size-3" />
|
|
629
|
+
Copy link
|
|
630
|
+
</button>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
{/* Divider + Frontmatter/Tags */}
|
|
635
|
+
{(hasFrontmatter || showStandaloneTags) && (
|
|
636
|
+
<>
|
|
637
|
+
<div className="mx-3 border-border/20 border-t" />
|
|
638
|
+
<div className="px-3 py-3">
|
|
639
|
+
<div className="mb-2 flex items-center justify-between">
|
|
640
|
+
<span className="font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
641
|
+
{hasFrontmatter ? "Metadata" : "Tags"}
|
|
642
|
+
</span>
|
|
643
|
+
{!editingTags && (
|
|
644
|
+
<button
|
|
645
|
+
className="flex cursor-pointer items-center gap-1 rounded px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground/40 transition-colors hover:bg-muted/20 hover:text-muted-foreground"
|
|
646
|
+
onClick={handleStartEditTags}
|
|
647
|
+
type="button"
|
|
648
|
+
>
|
|
649
|
+
<PencilIcon className="size-2.5" />
|
|
650
|
+
Edit
|
|
651
|
+
</button>
|
|
652
|
+
)}
|
|
653
|
+
{tagSaveSuccess && (
|
|
654
|
+
<span className="flex items-center gap-1 text-[10px] text-green-500">
|
|
655
|
+
<CheckIcon className="size-2.5" />
|
|
656
|
+
</span>
|
|
657
|
+
)}
|
|
658
|
+
</div>
|
|
659
|
+
{hasFrontmatter && (
|
|
660
|
+
<FrontmatterDisplay
|
|
661
|
+
className="grid-cols-1 gap-2 sm:grid-cols-1 lg:grid-cols-1 [&>div]:rounded-none [&>div]:bg-transparent [&>div]:p-0 [&>div]:border-b [&>div]:border-border/10 [&>div]:pb-2 [&>div:last-child]:border-0 [&>div:last-child]:pb-0 [&_a]:text-[11px] [&_a]:leading-snug [&_.text-sm]:text-[12px]"
|
|
662
|
+
content={doc?.content ?? ""}
|
|
663
|
+
/>
|
|
664
|
+
)}
|
|
665
|
+
{editingTags ? (
|
|
666
|
+
<div
|
|
667
|
+
className={
|
|
668
|
+
hasFrontmatter
|
|
669
|
+
? "mt-3 space-y-2 border-border/20 border-t pt-3"
|
|
670
|
+
: "space-y-2"
|
|
671
|
+
}
|
|
672
|
+
>
|
|
673
|
+
<TagInput
|
|
674
|
+
aria-label="Edit document tags"
|
|
675
|
+
disabled={savingTags}
|
|
676
|
+
onChange={setEditedTags}
|
|
677
|
+
placeholder="Add tags..."
|
|
678
|
+
value={editedTags}
|
|
679
|
+
/>
|
|
680
|
+
{tagSaveError && (
|
|
681
|
+
<p className="text-destructive text-xs">{tagSaveError}</p>
|
|
682
|
+
)}
|
|
683
|
+
<div className="flex items-center gap-1.5">
|
|
684
|
+
<Button
|
|
685
|
+
disabled={savingTags}
|
|
686
|
+
onClick={handleSaveTags}
|
|
687
|
+
size="sm"
|
|
688
|
+
>
|
|
689
|
+
{savingTags && (
|
|
690
|
+
<Loader2Icon className="mr-1 size-3 animate-spin" />
|
|
691
|
+
)}
|
|
692
|
+
Save
|
|
693
|
+
</Button>
|
|
694
|
+
<Button
|
|
695
|
+
disabled={savingTags}
|
|
696
|
+
onClick={handleCancelEditTags}
|
|
697
|
+
size="sm"
|
|
698
|
+
variant="outline"
|
|
699
|
+
>
|
|
700
|
+
Cancel
|
|
701
|
+
</Button>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
) : (
|
|
705
|
+
!hasFrontmatter && (
|
|
706
|
+
<div className="flex flex-wrap gap-1">
|
|
707
|
+
{doc?.tags.length === 0 ? (
|
|
708
|
+
<span className="text-[11px] text-muted-foreground/40 italic">
|
|
709
|
+
No tags
|
|
710
|
+
</span>
|
|
711
|
+
) : (
|
|
712
|
+
doc?.tags.map((tag) => (
|
|
713
|
+
<span
|
|
714
|
+
className="rounded-full bg-primary/10 px-2 py-0.5 font-mono text-[10px] text-primary/80"
|
|
715
|
+
key={tag}
|
|
716
|
+
>
|
|
717
|
+
{tag}
|
|
718
|
+
</span>
|
|
719
|
+
))
|
|
720
|
+
)}
|
|
721
|
+
</div>
|
|
722
|
+
)
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
</>
|
|
726
|
+
)}
|
|
727
|
+
</nav>
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
const renderDocumentOverviewCard = () => (
|
|
731
|
+
<Card>
|
|
732
|
+
<CardContent className="space-y-3 py-3">
|
|
733
|
+
<div className="flex items-center justify-between">
|
|
734
|
+
<div className="font-semibold text-sm">Overview</div>
|
|
735
|
+
<div className="flex items-center gap-2">
|
|
736
|
+
{tagSaveSuccess && (
|
|
737
|
+
<span className="flex items-center gap-1 text-green-500 text-xs">
|
|
738
|
+
<CheckIcon className="size-3" />
|
|
739
|
+
Saved
|
|
740
|
+
</span>
|
|
741
|
+
)}
|
|
742
|
+
{(hasFrontmatter || doc?.tags.length) && !editingTags && (
|
|
743
|
+
<Button
|
|
744
|
+
className="gap-1 text-xs"
|
|
745
|
+
onClick={handleStartEditTags}
|
|
746
|
+
size="sm"
|
|
747
|
+
variant="ghost"
|
|
748
|
+
>
|
|
749
|
+
<PencilIcon className="size-3" />
|
|
750
|
+
Edit tags
|
|
751
|
+
</Button>
|
|
752
|
+
)}
|
|
753
|
+
</div>
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
<div className="grid gap-2 md:grid-cols-3">
|
|
757
|
+
<div className="rounded-lg bg-muted/15 px-3 py-2.5">
|
|
758
|
+
<div className="mb-1 flex items-center gap-2 text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
759
|
+
<FolderOpen className="size-3.5" />
|
|
760
|
+
Collection
|
|
761
|
+
</div>
|
|
762
|
+
<div className="font-medium text-sm">
|
|
763
|
+
{doc?.collection || "Unknown"}
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
{doc?.source.sizeBytes !== undefined && (
|
|
768
|
+
<div className="rounded-lg bg-muted/15 px-3 py-2.5">
|
|
769
|
+
<div className="mb-1 flex items-center gap-2 text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
770
|
+
<HardDrive className="size-3.5" />
|
|
771
|
+
Size
|
|
772
|
+
</div>
|
|
773
|
+
<div className="font-medium text-sm">
|
|
774
|
+
{formatBytes(doc.source.sizeBytes)}
|
|
775
|
+
</div>
|
|
776
|
+
</div>
|
|
777
|
+
)}
|
|
778
|
+
|
|
779
|
+
{doc?.source.modifiedAt && (
|
|
780
|
+
<div className="rounded-lg bg-muted/15 px-3 py-2.5">
|
|
781
|
+
<div className="mb-1 flex items-center gap-2 text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
782
|
+
<Calendar className="size-3.5" />
|
|
783
|
+
Modified
|
|
784
|
+
</div>
|
|
785
|
+
<div className="font-medium text-sm">
|
|
786
|
+
{formatDate(doc.source.modifiedAt)}
|
|
787
|
+
</div>
|
|
788
|
+
</div>
|
|
789
|
+
)}
|
|
790
|
+
</div>
|
|
791
|
+
|
|
792
|
+
<div className="space-y-1.5">
|
|
793
|
+
<div className="text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
794
|
+
Path
|
|
795
|
+
</div>
|
|
796
|
+
<code className="block break-all font-mono text-[11px] text-muted-foreground">
|
|
797
|
+
{doc?.uri}
|
|
798
|
+
</code>
|
|
799
|
+
<div className="flex flex-wrap gap-2 pt-1">
|
|
800
|
+
{currentTarget.lineStart && (
|
|
801
|
+
<Badge className="font-mono" variant="outline">
|
|
802
|
+
L{currentTarget.lineStart}
|
|
803
|
+
{currentTarget.lineEnd &&
|
|
804
|
+
currentTarget.lineEnd !== currentTarget.lineStart
|
|
805
|
+
? `-${currentTarget.lineEnd}`
|
|
806
|
+
: ""}
|
|
807
|
+
</Badge>
|
|
808
|
+
)}
|
|
809
|
+
<Button
|
|
810
|
+
onClick={() => {
|
|
811
|
+
if (!doc) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
void navigator.clipboard.writeText(
|
|
815
|
+
`${window.location.origin}${buildDocDeepLink({
|
|
816
|
+
uri: doc.uri,
|
|
817
|
+
view:
|
|
818
|
+
currentTarget.view === "source" || currentTarget.lineStart
|
|
819
|
+
? "source"
|
|
820
|
+
: "rendered",
|
|
821
|
+
lineStart: currentTarget.lineStart,
|
|
822
|
+
lineEnd: currentTarget.lineEnd,
|
|
823
|
+
})}`
|
|
824
|
+
);
|
|
825
|
+
}}
|
|
826
|
+
size="sm"
|
|
827
|
+
variant="outline"
|
|
828
|
+
>
|
|
829
|
+
<LinkIcon className="mr-1.5 size-4" />
|
|
830
|
+
Copy link
|
|
831
|
+
</Button>
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
|
|
835
|
+
{(hasFrontmatter || showStandaloneTags) && (
|
|
836
|
+
<div className="rounded-lg border border-border/40 bg-muted/10 p-2.5">
|
|
837
|
+
{hasFrontmatter && (
|
|
838
|
+
<FrontmatterDisplay content={doc?.content ?? ""} />
|
|
839
|
+
)}
|
|
840
|
+
{editingTags ? (
|
|
841
|
+
<div
|
|
842
|
+
className={
|
|
843
|
+
hasFrontmatter
|
|
844
|
+
? "mt-3 space-y-3 border-border/30 border-t pt-3"
|
|
845
|
+
: "space-y-3"
|
|
846
|
+
}
|
|
847
|
+
>
|
|
848
|
+
<TagInput
|
|
849
|
+
aria-label="Edit document tags"
|
|
850
|
+
disabled={savingTags}
|
|
851
|
+
onChange={setEditedTags}
|
|
852
|
+
placeholder="Add tags..."
|
|
853
|
+
value={editedTags}
|
|
854
|
+
/>
|
|
855
|
+
{tagSaveError && (
|
|
856
|
+
<p className="text-destructive text-xs">{tagSaveError}</p>
|
|
857
|
+
)}
|
|
858
|
+
<div className="flex items-center gap-2">
|
|
859
|
+
<Button
|
|
860
|
+
disabled={savingTags}
|
|
861
|
+
onClick={handleSaveTags}
|
|
862
|
+
size="sm"
|
|
863
|
+
>
|
|
864
|
+
{savingTags && (
|
|
865
|
+
<Loader2Icon className="mr-1.5 size-3 animate-spin" />
|
|
866
|
+
)}
|
|
867
|
+
Save
|
|
868
|
+
</Button>
|
|
869
|
+
<Button
|
|
870
|
+
disabled={savingTags}
|
|
871
|
+
onClick={handleCancelEditTags}
|
|
872
|
+
size="sm"
|
|
873
|
+
variant="outline"
|
|
874
|
+
>
|
|
875
|
+
Cancel
|
|
876
|
+
</Button>
|
|
877
|
+
</div>
|
|
878
|
+
</div>
|
|
879
|
+
) : (
|
|
880
|
+
!hasFrontmatter && (
|
|
881
|
+
<div className="flex flex-wrap gap-1.5">
|
|
882
|
+
{doc?.tags.length === 0 ? (
|
|
883
|
+
<span className="text-muted-foreground/60 text-sm italic">
|
|
884
|
+
No tags
|
|
885
|
+
</span>
|
|
886
|
+
) : (
|
|
887
|
+
doc?.tags.map((tag) => (
|
|
888
|
+
<Badge
|
|
889
|
+
className="font-mono text-xs"
|
|
890
|
+
key={tag}
|
|
891
|
+
variant="outline"
|
|
892
|
+
>
|
|
893
|
+
{tag}
|
|
894
|
+
</Badge>
|
|
895
|
+
))
|
|
896
|
+
)}
|
|
897
|
+
</div>
|
|
898
|
+
)
|
|
899
|
+
)}
|
|
900
|
+
</div>
|
|
901
|
+
)}
|
|
902
|
+
</CardContent>
|
|
903
|
+
</Card>
|
|
904
|
+
);
|
|
905
|
+
|
|
538
906
|
return (
|
|
539
907
|
<div className="min-h-screen">
|
|
540
908
|
{/* Header */}
|
|
@@ -667,9 +1035,16 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
667
1035
|
</div>
|
|
668
1036
|
</header>
|
|
669
1037
|
|
|
670
|
-
<div className="mx-auto flex max-w-
|
|
1038
|
+
<div className="mx-auto flex max-w-[1800px] gap-5 px-6 xl:px-8">
|
|
1039
|
+
{/* Left rail — document facts, only on ultra-wide */}
|
|
1040
|
+
{doc && (
|
|
1041
|
+
<aside className="hidden w-[200px] shrink-0 border-border/15 border-r pr-2 py-6 lg:block">
|
|
1042
|
+
<div className="sticky top-24">{renderDocumentFactsRail()}</div>
|
|
1043
|
+
</aside>
|
|
1044
|
+
)}
|
|
1045
|
+
|
|
671
1046
|
{/* Main content */}
|
|
672
|
-
<main className="min-w-0 flex-1 py-
|
|
1047
|
+
<main className="min-w-0 flex-1 px-4 py-6">
|
|
673
1048
|
{/* Loading */}
|
|
674
1049
|
{loading && (
|
|
675
1050
|
<div className="flex flex-col items-center justify-center gap-4 py-20">
|
|
@@ -693,7 +1068,7 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
693
1068
|
|
|
694
1069
|
{/* Document */}
|
|
695
1070
|
{doc && (
|
|
696
|
-
<div className="animate-fade-in space-y-
|
|
1071
|
+
<div className="animate-fade-in space-y-4 opacity-0">
|
|
697
1072
|
{externalChangeNotice && (
|
|
698
1073
|
<Card className="border-amber-500/40 bg-amber-500/10">
|
|
699
1074
|
<CardContent className="flex flex-wrap items-center justify-between gap-3 py-3">
|
|
@@ -728,7 +1103,7 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
728
1103
|
)}
|
|
729
1104
|
{crumb.path ? (
|
|
730
1105
|
<button
|
|
731
|
-
className="text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
|
1106
|
+
className="cursor-pointer text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
|
732
1107
|
onClick={() => navigate(crumb.path)}
|
|
733
1108
|
type="button"
|
|
734
1109
|
>
|
|
@@ -744,299 +1119,112 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
744
1119
|
</nav>
|
|
745
1120
|
)}
|
|
746
1121
|
|
|
747
|
-
{
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
)}
|
|
775
|
-
{doc.source.modifiedAt && (
|
|
776
|
-
<div className="flex items-center gap-3">
|
|
777
|
-
<Calendar className="size-4 text-muted-foreground" />
|
|
778
|
-
<div>
|
|
779
|
-
<div className="text-muted-foreground text-xs">
|
|
780
|
-
Modified
|
|
781
|
-
</div>
|
|
782
|
-
<div className="font-medium">
|
|
783
|
-
{formatDate(doc.source.modifiedAt)}
|
|
784
|
-
</div>
|
|
785
|
-
</div>
|
|
786
|
-
</div>
|
|
1122
|
+
<div className="lg:hidden">{renderDocumentOverviewCard()}</div>
|
|
1123
|
+
|
|
1124
|
+
{/* Content */}
|
|
1125
|
+
<div className="relative">
|
|
1126
|
+
{/* Source/Rendered toggle pill */}
|
|
1127
|
+
{isMarkdown && doc.contentAvailable && (
|
|
1128
|
+
<button
|
|
1129
|
+
className="z-10 flex cursor-pointer items-center gap-1.5 rounded-full border border-border/30 bg-background/80 px-3 py-1 font-mono text-[11px] text-muted-foreground backdrop-blur-sm transition-colors hover:border-primary/30 hover:text-primary"
|
|
1130
|
+
onClick={() => setShowRawView(!showRawView)}
|
|
1131
|
+
style={{
|
|
1132
|
+
position: "absolute",
|
|
1133
|
+
top: "0.75rem",
|
|
1134
|
+
right: "0.75rem",
|
|
1135
|
+
left: "auto",
|
|
1136
|
+
}}
|
|
1137
|
+
type="button"
|
|
1138
|
+
>
|
|
1139
|
+
{showRawView ? (
|
|
1140
|
+
<>
|
|
1141
|
+
<TextIcon className="size-3" />
|
|
1142
|
+
Rendered
|
|
1143
|
+
</>
|
|
1144
|
+
) : (
|
|
1145
|
+
<>
|
|
1146
|
+
<CodeIcon className="size-3" />
|
|
1147
|
+
Source
|
|
1148
|
+
</>
|
|
787
1149
|
)}
|
|
1150
|
+
</button>
|
|
1151
|
+
)}
|
|
1152
|
+
|
|
1153
|
+
{!doc.contentAvailable && (
|
|
1154
|
+
<div className="rounded-lg border border-border/50 bg-muted/30 p-6 text-center">
|
|
1155
|
+
<p className="text-muted-foreground">
|
|
1156
|
+
Content not available (document may need re-indexing)
|
|
1157
|
+
</p>
|
|
788
1158
|
</div>
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
{
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
<p className="mt-2 text-amber-500 text-xs">
|
|
798
|
-
{doc.capabilities.reason}
|
|
799
|
-
</p>
|
|
800
|
-
)}
|
|
801
|
-
<div className="mt-2 flex flex-wrap gap-2">
|
|
802
|
-
{currentTarget.lineStart && (
|
|
803
|
-
<Badge className="font-mono" variant="outline">
|
|
804
|
-
L{currentTarget.lineStart}
|
|
805
|
-
{currentTarget.lineEnd &&
|
|
806
|
-
currentTarget.lineEnd !== currentTarget.lineStart
|
|
807
|
-
? `-${currentTarget.lineEnd}`
|
|
808
|
-
: ""}
|
|
809
|
-
</Badge>
|
|
810
|
-
)}
|
|
811
|
-
<Button
|
|
812
|
-
onClick={() => {
|
|
813
|
-
void navigator.clipboard.writeText(
|
|
814
|
-
`${window.location.origin}${buildDocDeepLink({
|
|
815
|
-
uri: doc.uri,
|
|
816
|
-
view:
|
|
817
|
-
currentTarget.view === "source" ||
|
|
818
|
-
currentTarget.lineStart
|
|
819
|
-
? "source"
|
|
820
|
-
: "rendered",
|
|
821
|
-
lineStart: currentTarget.lineStart,
|
|
822
|
-
lineEnd: currentTarget.lineEnd,
|
|
823
|
-
})}`
|
|
824
|
-
);
|
|
825
|
-
}}
|
|
826
|
-
size="sm"
|
|
827
|
-
variant="outline"
|
|
828
|
-
>
|
|
829
|
-
<LinkIcon className="mr-1.5 size-4" />
|
|
830
|
-
Copy link
|
|
831
|
-
</Button>
|
|
832
|
-
</div>
|
|
1159
|
+
)}
|
|
1160
|
+
{doc.contentAvailable && isMarkdown && !showRawView && (
|
|
1161
|
+
<div className="rounded-lg border border-border/40 bg-gradient-to-br from-background to-muted/10 p-4 shadow-inner">
|
|
1162
|
+
<MarkdownPreview
|
|
1163
|
+
collection={doc.collection}
|
|
1164
|
+
content={parsedContent.body}
|
|
1165
|
+
wikiLinks={resolvedWikiLinks}
|
|
1166
|
+
/>
|
|
833
1167
|
</div>
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
</div>
|
|
865
|
-
|
|
866
|
-
{/* Display mode */}
|
|
867
|
-
{!editingTags && (
|
|
868
|
-
<div className="flex flex-wrap gap-1.5">
|
|
869
|
-
{doc.tags.length === 0 ? (
|
|
870
|
-
<span className="text-muted-foreground/60 text-sm italic">
|
|
871
|
-
No tags
|
|
872
|
-
</span>
|
|
873
|
-
) : (
|
|
874
|
-
doc.tags.map((tag) => (
|
|
875
|
-
<Badge
|
|
876
|
-
className="font-mono text-xs"
|
|
877
|
-
key={tag}
|
|
878
|
-
variant="outline"
|
|
879
|
-
>
|
|
880
|
-
{tag}
|
|
881
|
-
</Badge>
|
|
882
|
-
))
|
|
883
|
-
)}
|
|
884
|
-
</div>
|
|
885
|
-
)}
|
|
886
|
-
|
|
887
|
-
{/* Edit mode */}
|
|
888
|
-
{editingTags && (
|
|
889
|
-
<div className="space-y-3">
|
|
890
|
-
<TagInput
|
|
891
|
-
aria-label="Edit document tags"
|
|
892
|
-
disabled={savingTags}
|
|
893
|
-
onChange={setEditedTags}
|
|
894
|
-
placeholder="Add tags..."
|
|
895
|
-
value={editedTags}
|
|
896
|
-
/>
|
|
897
|
-
|
|
898
|
-
{tagSaveError && (
|
|
899
|
-
<p className="text-destructive text-xs">
|
|
900
|
-
{tagSaveError}
|
|
901
|
-
</p>
|
|
902
|
-
)}
|
|
903
|
-
|
|
904
|
-
<div className="flex items-center gap-2">
|
|
905
|
-
<Button
|
|
906
|
-
disabled={savingTags}
|
|
907
|
-
onClick={handleSaveTags}
|
|
908
|
-
size="sm"
|
|
909
|
-
>
|
|
910
|
-
{savingTags && (
|
|
911
|
-
<Loader2Icon className="mr-1.5 size-3 animate-spin" />
|
|
912
|
-
)}
|
|
913
|
-
Save
|
|
914
|
-
</Button>
|
|
915
|
-
<Button
|
|
916
|
-
disabled={savingTags}
|
|
917
|
-
onClick={handleCancelEditTags}
|
|
918
|
-
size="sm"
|
|
919
|
-
variant="outline"
|
|
920
|
-
>
|
|
921
|
-
Cancel
|
|
922
|
-
</Button>
|
|
923
|
-
</div>
|
|
924
|
-
</div>
|
|
925
|
-
)}
|
|
926
|
-
</div>
|
|
1168
|
+
)}
|
|
1169
|
+
{doc.contentAvailable && isMarkdown && showRawView && (
|
|
1170
|
+
<CodeBlock
|
|
1171
|
+
code={doc.content ?? ""}
|
|
1172
|
+
highlightedLines={highlightedLines}
|
|
1173
|
+
language={"markdown" as BundledLanguage}
|
|
1174
|
+
scrollToLine={currentTarget.lineStart}
|
|
1175
|
+
showLineNumbers
|
|
1176
|
+
>
|
|
1177
|
+
<CodeBlockCopyButton />
|
|
1178
|
+
</CodeBlock>
|
|
1179
|
+
)}
|
|
1180
|
+
{doc.contentAvailable && isCodeFile && !isMarkdown && (
|
|
1181
|
+
<CodeBlock
|
|
1182
|
+
code={doc.content ?? ""}
|
|
1183
|
+
highlightedLines={highlightedLines}
|
|
1184
|
+
language={
|
|
1185
|
+
getLanguageFromExt(doc.source.ext) as BundledLanguage
|
|
1186
|
+
}
|
|
1187
|
+
scrollToLine={currentTarget.lineStart}
|
|
1188
|
+
showLineNumbers
|
|
1189
|
+
>
|
|
1190
|
+
<CodeBlockCopyButton />
|
|
1191
|
+
</CodeBlock>
|
|
1192
|
+
)}
|
|
1193
|
+
{doc.contentAvailable && !isCodeFile && (
|
|
1194
|
+
<div className="rounded-lg border border-border/50 bg-muted/30 p-6">
|
|
1195
|
+
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
|
|
1196
|
+
{doc.content}
|
|
1197
|
+
</pre>
|
|
927
1198
|
</div>
|
|
928
|
-
|
|
929
|
-
</
|
|
930
|
-
|
|
931
|
-
{/* Content */}
|
|
932
|
-
<Card>
|
|
933
|
-
<CardHeader className="pb-0">
|
|
934
|
-
<CardTitle className="flex items-center justify-between text-lg">
|
|
935
|
-
<span className="flex items-center gap-2">
|
|
936
|
-
<FileText className="size-4" />
|
|
937
|
-
Content
|
|
938
|
-
</span>
|
|
939
|
-
{isMarkdown && doc.contentAvailable && (
|
|
940
|
-
<Button
|
|
941
|
-
className="gap-1.5"
|
|
942
|
-
onClick={() => setShowRawView(!showRawView)}
|
|
943
|
-
size="sm"
|
|
944
|
-
variant="ghost"
|
|
945
|
-
>
|
|
946
|
-
{showRawView ? (
|
|
947
|
-
<>
|
|
948
|
-
<TextIcon className="size-4" />
|
|
949
|
-
<span className="hidden sm:inline">Rendered</span>
|
|
950
|
-
</>
|
|
951
|
-
) : (
|
|
952
|
-
<>
|
|
953
|
-
<CodeIcon className="size-4" />
|
|
954
|
-
<span className="hidden sm:inline">Source</span>
|
|
955
|
-
</>
|
|
956
|
-
)}
|
|
957
|
-
</Button>
|
|
958
|
-
)}
|
|
959
|
-
</CardTitle>
|
|
960
|
-
</CardHeader>
|
|
961
|
-
<CardContent className="pt-4">
|
|
962
|
-
{!doc.contentAvailable && (
|
|
963
|
-
<div className="rounded-lg border border-border/50 bg-muted/30 p-6 text-center">
|
|
964
|
-
<p className="text-muted-foreground">
|
|
965
|
-
Content not available (document may need re-indexing)
|
|
966
|
-
</p>
|
|
967
|
-
</div>
|
|
968
|
-
)}
|
|
969
|
-
{doc.contentAvailable && isMarkdown && !showRawView && (
|
|
970
|
-
<div className="space-y-4">
|
|
971
|
-
{hasFrontmatter && (
|
|
972
|
-
<FrontmatterDisplay
|
|
973
|
-
className="rounded-lg border border-border/40 bg-muted/10 p-4"
|
|
974
|
-
content={doc.content ?? ""}
|
|
975
|
-
/>
|
|
976
|
-
)}
|
|
977
|
-
<div className="rounded-lg border border-border/40 bg-gradient-to-br from-background to-muted/10 p-6 shadow-inner">
|
|
978
|
-
<MarkdownPreview content={parsedContent.body} />
|
|
979
|
-
</div>
|
|
980
|
-
</div>
|
|
981
|
-
)}
|
|
982
|
-
{doc.contentAvailable && isMarkdown && showRawView && (
|
|
983
|
-
<CodeBlock
|
|
984
|
-
code={doc.content ?? ""}
|
|
985
|
-
highlightedLines={highlightedLines}
|
|
986
|
-
language={"markdown" as BundledLanguage}
|
|
987
|
-
scrollToLine={currentTarget.lineStart}
|
|
988
|
-
showLineNumbers
|
|
989
|
-
>
|
|
990
|
-
<CodeBlockCopyButton />
|
|
991
|
-
</CodeBlock>
|
|
992
|
-
)}
|
|
993
|
-
{doc.contentAvailable && isCodeFile && !isMarkdown && (
|
|
994
|
-
<CodeBlock
|
|
995
|
-
code={doc.content ?? ""}
|
|
996
|
-
highlightedLines={highlightedLines}
|
|
997
|
-
language={
|
|
998
|
-
getLanguageFromExt(doc.source.ext) as BundledLanguage
|
|
999
|
-
}
|
|
1000
|
-
scrollToLine={currentTarget.lineStart}
|
|
1001
|
-
showLineNumbers
|
|
1002
|
-
>
|
|
1003
|
-
<CodeBlockCopyButton />
|
|
1004
|
-
</CodeBlock>
|
|
1005
|
-
)}
|
|
1006
|
-
{doc.contentAvailable && !isCodeFile && (
|
|
1007
|
-
<div className="rounded-lg border border-border/50 bg-muted/30 p-6">
|
|
1008
|
-
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
|
|
1009
|
-
{doc.content}
|
|
1010
|
-
</pre>
|
|
1011
|
-
</div>
|
|
1012
|
-
)}
|
|
1013
|
-
</CardContent>
|
|
1014
|
-
</Card>
|
|
1199
|
+
)}
|
|
1200
|
+
</div>
|
|
1015
1201
|
</div>
|
|
1016
1202
|
)}
|
|
1017
1203
|
</main>
|
|
1018
1204
|
|
|
1019
|
-
{/* Right
|
|
1205
|
+
{/* Right rail — relationships */}
|
|
1020
1206
|
{doc && (
|
|
1021
|
-
<aside className="hidden w-
|
|
1022
|
-
<
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1207
|
+
<aside className="hidden w-[240px] min-w-0 shrink-0 overflow-hidden border-border/15 border-l pl-2 py-6 lg:block">
|
|
1208
|
+
<div className="sticky top-24 min-w-0 space-y-1 overflow-hidden">
|
|
1209
|
+
<BacklinksPanel
|
|
1210
|
+
docId={doc.docid}
|
|
1211
|
+
onNavigate={(uri) =>
|
|
1212
|
+
navigate(`/doc?uri=${encodeURIComponent(uri)}`)
|
|
1213
|
+
}
|
|
1214
|
+
/>
|
|
1215
|
+
<OutgoingLinksPanel
|
|
1216
|
+
docId={doc.docid}
|
|
1217
|
+
onNavigate={(uri) =>
|
|
1218
|
+
navigate(`/doc?uri=${encodeURIComponent(uri)}`)
|
|
1219
|
+
}
|
|
1220
|
+
/>
|
|
1221
|
+
<RelatedNotesSidebar
|
|
1222
|
+
docId={doc.docid}
|
|
1223
|
+
onNavigate={(uri) =>
|
|
1224
|
+
navigate(`/doc?uri=${encodeURIComponent(uri)}`)
|
|
1225
|
+
}
|
|
1226
|
+
/>
|
|
1227
|
+
</div>
|
|
1040
1228
|
</aside>
|
|
1041
1229
|
)}
|
|
1042
1230
|
</div>
|