@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.
@@ -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 { OutgoingLinksPanel } from "../components/OutgoingLinksPanel";
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-7xl gap-8 px-8">
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-8">
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-6 opacity-0">
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
- {/* Metadata */}
748
- <Card>
749
- <CardContent className="py-4">
750
- <div className="grid gap-4 sm:grid-cols-3">
751
- <div className="flex items-center gap-3">
752
- <FolderOpen className="size-4 text-muted-foreground" />
753
- <div>
754
- <div className="text-muted-foreground text-xs">
755
- Collection
756
- </div>
757
- <div className="font-medium">
758
- {doc.collection || "Unknown"}
759
- </div>
760
- </div>
761
- </div>
762
- {doc.source.sizeBytes !== undefined && (
763
- <div className="flex items-center gap-3">
764
- <HardDrive className="size-4 text-muted-foreground" />
765
- <div>
766
- <div className="text-muted-foreground text-xs">
767
- Size
768
- </div>
769
- <div className="font-medium">
770
- {formatBytes(doc.source.sizeBytes)}
771
- </div>
772
- </div>
773
- </div>
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
- <div className="mt-4 border-border/50 border-t pt-4">
790
- <div className="mb-1 text-muted-foreground text-xs">
791
- Path
792
- </div>
793
- <code className="break-all font-mono text-muted-foreground text-sm">
794
- {doc.uri}
795
- </code>
796
- {doc.capabilities.reason && (
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
- </CardContent>
835
- </Card>
836
-
837
- {/* Tags */}
838
- <Card>
839
- <CardContent className="py-4">
840
- <div className="flex items-start gap-3">
841
- <TagIcon className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
842
- <div className="min-w-0 flex-1">
843
- <div className="mb-2 flex items-center justify-between">
844
- <div className="text-muted-foreground text-xs">
845
- Tags
846
- </div>
847
- {!editingTags && (
848
- <Button
849
- className="gap-1 text-xs"
850
- onClick={handleStartEditTags}
851
- size="sm"
852
- variant="ghost"
853
- >
854
- <PencilIcon className="size-3" />
855
- Edit
856
- </Button>
857
- )}
858
- {tagSaveSuccess && (
859
- <span className="flex items-center gap-1 text-green-500 text-xs">
860
- <CheckIcon className="size-3" />
861
- Saved
862
- </span>
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
- </CardContent>
929
- </Card>
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 sidebar - Link panels */}
1205
+ {/* Right rail relationships */}
1020
1206
  {doc && (
1021
- <aside className="hidden w-72 shrink-0 space-y-4 py-8 lg:block">
1022
- <BacklinksPanel
1023
- docId={doc.docid}
1024
- onNavigate={(uri) =>
1025
- navigate(`/doc?uri=${encodeURIComponent(uri)}`)
1026
- }
1027
- />
1028
- <OutgoingLinksPanel
1029
- docId={doc.docid}
1030
- onNavigate={(uri) =>
1031
- navigate(`/doc?uri=${encodeURIComponent(uri)}`)
1032
- }
1033
- />
1034
- <RelatedNotesSidebar
1035
- docId={doc.docid}
1036
- onNavigate={(uri) =>
1037
- navigate(`/doc?uri=${encodeURIComponent(uri)}`)
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>