@gmickel/gno 0.34.1 → 0.36.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.
- package/README.md +37 -26
- package/assets/screenshots/webui-collections.jpg +0 -0
- package/assets/screenshots/webui-graph.jpg +0 -0
- package/package.json +1 -1
- package/src/core/file-ops.ts +12 -1
- package/src/core/file-refactors.ts +180 -0
- package/src/core/note-creation.ts +137 -0
- package/src/core/note-presets.ts +183 -0
- package/src/core/sections.ts +62 -0
- package/src/core/validation.ts +4 -3
- package/src/mcp/tools/capture.ts +71 -12
- package/src/mcp/tools/index.ts +82 -1
- package/src/mcp/tools/workspace-write.ts +321 -0
- package/src/sdk/client.ts +341 -0
- package/src/sdk/types.ts +67 -0
- package/src/serve/CLAUDE.md +3 -1
- package/src/serve/browse-tree.ts +60 -12
- package/src/serve/public/app.tsx +8 -3
- package/src/serve/public/components/BrowseDetailPane.tsx +18 -1
- package/src/serve/public/components/CaptureModal.tsx +135 -13
- package/src/serve/public/components/QuickSwitcher.tsx +408 -46
- package/src/serve/public/components/ShortcutHelpModal.tsx +54 -1
- package/src/serve/public/components/editor/MarkdownPreview.tsx +58 -26
- package/src/serve/public/components/ui/command.tsx +19 -9
- package/src/serve/public/components/ui/dialog.tsx +1 -1
- package/src/serve/public/globals.built.css +2 -2
- package/src/serve/public/globals.css +47 -0
- package/src/serve/public/hooks/useCaptureModal.tsx +31 -5
- package/src/serve/public/lib/browse.ts +1 -0
- package/src/serve/public/lib/workspace-actions.ts +226 -0
- package/src/serve/public/lib/workspace-events.ts +39 -0
- package/src/serve/public/pages/Browse.tsx +154 -3
- package/src/serve/public/pages/DocView.tsx +472 -10
- package/src/serve/public/pages/DocumentEditor.tsx +52 -0
- package/src/serve/routes/api.ts +712 -13
- package/src/serve/server.ts +74 -0
- package/src/store/sqlite/adapter.ts +19 -19
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CheckIcon,
|
|
6
6
|
ChevronRightIcon,
|
|
7
7
|
CodeIcon,
|
|
8
|
+
CopyIcon,
|
|
8
9
|
FileText,
|
|
9
10
|
FolderOpen,
|
|
10
11
|
HardDrive,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
} from "lucide-react";
|
|
19
20
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
20
21
|
|
|
22
|
+
import { extractSections } from "../../../core/sections";
|
|
21
23
|
import {
|
|
22
24
|
CodeBlock,
|
|
23
25
|
CodeBlockCopyButton,
|
|
@@ -46,6 +48,7 @@ import {
|
|
|
46
48
|
DialogHeader,
|
|
47
49
|
DialogTitle,
|
|
48
50
|
} from "../components/ui/dialog";
|
|
51
|
+
import { Input } from "../components/ui/input";
|
|
49
52
|
import { Separator } from "../components/ui/separator";
|
|
50
53
|
import { apiFetch } from "../hooks/use-api";
|
|
51
54
|
import { useDocEvents } from "../hooks/use-doc-events";
|
|
@@ -55,6 +58,7 @@ import {
|
|
|
55
58
|
parseDocumentDeepLink,
|
|
56
59
|
} from "../lib/deep-links";
|
|
57
60
|
import { waitForDocumentAvailability } from "../lib/document-availability";
|
|
61
|
+
import { subscribeWorkspaceActionRequest } from "../lib/workspace-events";
|
|
58
62
|
|
|
59
63
|
interface PageProps {
|
|
60
64
|
navigate: (to: string | number) => void;
|
|
@@ -99,6 +103,29 @@ interface RenameDocResponse {
|
|
|
99
103
|
uri: string;
|
|
100
104
|
path: string;
|
|
101
105
|
relPath: string;
|
|
106
|
+
refactorWarnings?: {
|
|
107
|
+
warnings: string[];
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface MoveDocResponse {
|
|
112
|
+
success: boolean;
|
|
113
|
+
uri: string;
|
|
114
|
+
path: string;
|
|
115
|
+
relPath: string;
|
|
116
|
+
refactorWarnings?: {
|
|
117
|
+
warnings: string[];
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface DuplicateDocResponse {
|
|
122
|
+
success: boolean;
|
|
123
|
+
uri: string;
|
|
124
|
+
path: string;
|
|
125
|
+
relPath: string;
|
|
126
|
+
refactorWarnings?: {
|
|
127
|
+
warnings: string[];
|
|
128
|
+
};
|
|
102
129
|
}
|
|
103
130
|
|
|
104
131
|
interface UpdateDocResponse {
|
|
@@ -219,6 +246,12 @@ function parseBreadcrumbs(
|
|
|
219
246
|
return segments;
|
|
220
247
|
}
|
|
221
248
|
|
|
249
|
+
function getParentPath(relPath: string): string {
|
|
250
|
+
const parts = relPath.split("/").filter(Boolean);
|
|
251
|
+
parts.pop();
|
|
252
|
+
return parts.join("/");
|
|
253
|
+
}
|
|
254
|
+
|
|
222
255
|
export default function DocView({ navigate }: PageProps) {
|
|
223
256
|
const [doc, setDoc] = useState<DocData | null>(null);
|
|
224
257
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -230,6 +263,19 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
230
263
|
const [renaming, setRenaming] = useState(false);
|
|
231
264
|
const [renameError, setRenameError] = useState<string | null>(null);
|
|
232
265
|
const [renameValue, setRenameValue] = useState("");
|
|
266
|
+
const [renameWarnings, setRenameWarnings] = useState<string[]>([]);
|
|
267
|
+
const [moveDialogOpen, setMoveDialogOpen] = useState(false);
|
|
268
|
+
const [moving, setMoving] = useState(false);
|
|
269
|
+
const [moveError, setMoveError] = useState<string | null>(null);
|
|
270
|
+
const [moveFolderPath, setMoveFolderPath] = useState("");
|
|
271
|
+
const [moveName, setMoveName] = useState("");
|
|
272
|
+
const [moveWarnings, setMoveWarnings] = useState<string[]>([]);
|
|
273
|
+
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
|
274
|
+
const [duplicating, setDuplicating] = useState(false);
|
|
275
|
+
const [duplicateError, setDuplicateError] = useState<string | null>(null);
|
|
276
|
+
const [duplicateFolderPath, setDuplicateFolderPath] = useState("");
|
|
277
|
+
const [duplicateName, setDuplicateName] = useState("");
|
|
278
|
+
const [duplicateWarnings, setDuplicateWarnings] = useState<string[]>([]);
|
|
233
279
|
const [showRawView, setShowRawView] = useState(false);
|
|
234
280
|
const [creatingCopy, setCreatingCopy] = useState(false);
|
|
235
281
|
const [copyError, setCopyError] = useState<string | null>(null);
|
|
@@ -246,6 +292,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
246
292
|
const [resolvedWikiLinks, setResolvedWikiLinks] = useState<OutgoingLink[]>(
|
|
247
293
|
[]
|
|
248
294
|
);
|
|
295
|
+
const [activeSectionAnchor, setActiveSectionAnchor] = useState<string | null>(
|
|
296
|
+
null
|
|
297
|
+
);
|
|
249
298
|
|
|
250
299
|
// Request sequencing - ignore stale responses on rapid navigation
|
|
251
300
|
const requestIdRef = useRef(0);
|
|
@@ -257,6 +306,10 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
257
306
|
[]
|
|
258
307
|
);
|
|
259
308
|
const currentUri = currentTarget.uri;
|
|
309
|
+
const currentHash = useMemo(
|
|
310
|
+
() => window.location.hash.replace(/^#/u, ""),
|
|
311
|
+
[]
|
|
312
|
+
);
|
|
260
313
|
const highlightedLines = useMemo(() => {
|
|
261
314
|
if (!currentTarget.lineStart) return [];
|
|
262
315
|
const end = currentTarget.lineEnd ?? currentTarget.lineStart;
|
|
@@ -371,7 +424,51 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
371
424
|
}
|
|
372
425
|
}, [currentTarget.lineStart, currentTarget.view]);
|
|
373
426
|
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
if (!currentHash || showRawView || loading) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
requestAnimationFrame(() => {
|
|
433
|
+
document
|
|
434
|
+
.getElementById(currentHash)
|
|
435
|
+
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
436
|
+
setActiveSectionAnchor(currentHash);
|
|
437
|
+
});
|
|
438
|
+
}, [currentHash, loading, showRawView]);
|
|
439
|
+
|
|
374
440
|
const breadcrumbs = doc ? parseBreadcrumbs(doc.collection, doc.relPath) : [];
|
|
441
|
+
const sections = useMemo(
|
|
442
|
+
() => extractSections(parsedContent.body),
|
|
443
|
+
[parsedContent.body]
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (showRawView || sections.length === 0) {
|
|
448
|
+
setActiveSectionAnchor(sections[0]?.anchor ?? null);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const updateActiveSection = () => {
|
|
453
|
+
let current = sections[0]?.anchor ?? null;
|
|
454
|
+
for (const section of sections) {
|
|
455
|
+
const element = document.getElementById(section.anchor);
|
|
456
|
+
if (!element) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (element.getBoundingClientRect().top <= 160) {
|
|
460
|
+
current = section.anchor;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
setActiveSectionAnchor(current);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
updateActiveSection();
|
|
467
|
+
window.addEventListener("scroll", updateActiveSection, { passive: true });
|
|
468
|
+
return () => {
|
|
469
|
+
window.removeEventListener("scroll", updateActiveSection);
|
|
470
|
+
};
|
|
471
|
+
}, [sections, showRawView]);
|
|
375
472
|
|
|
376
473
|
const handleEdit = () => {
|
|
377
474
|
if (doc?.capabilities.editable) {
|
|
@@ -445,9 +542,29 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
445
542
|
const filename = doc.relPath.split("/").pop() ?? doc.relPath;
|
|
446
543
|
setRenameValue(filename);
|
|
447
544
|
setRenameError(null);
|
|
545
|
+
setRenameWarnings([]);
|
|
448
546
|
setRenameDialogOpen(true);
|
|
449
547
|
}, [doc]);
|
|
450
548
|
|
|
549
|
+
useEffect(() => {
|
|
550
|
+
if (!renameDialogOpen || !doc || !renameValue.trim()) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
void apiFetch<{ refactorWarnings?: { warnings: string[] } }>(
|
|
554
|
+
`/api/docs/${encodeURIComponent(doc.docid)}/refactor-plan`,
|
|
555
|
+
{
|
|
556
|
+
method: "POST",
|
|
557
|
+
body: JSON.stringify({
|
|
558
|
+
operation: "rename",
|
|
559
|
+
name: renameValue,
|
|
560
|
+
uri: doc.uri,
|
|
561
|
+
}),
|
|
562
|
+
}
|
|
563
|
+
).then(({ data }) => {
|
|
564
|
+
setRenameWarnings(data?.refactorWarnings?.warnings ?? []);
|
|
565
|
+
});
|
|
566
|
+
}, [doc, renameDialogOpen, renameValue]);
|
|
567
|
+
|
|
451
568
|
const handleRename = useCallback(async () => {
|
|
452
569
|
if (!doc) {
|
|
453
570
|
return;
|
|
@@ -474,6 +591,159 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
474
591
|
}
|
|
475
592
|
}, [doc, navigate, renameValue]);
|
|
476
593
|
|
|
594
|
+
const handleStartMove = useCallback(() => {
|
|
595
|
+
if (!doc) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
setMoveFolderPath(getParentPath(doc.relPath));
|
|
599
|
+
setMoveName(doc.relPath.split("/").pop() ?? doc.relPath);
|
|
600
|
+
setMoveError(null);
|
|
601
|
+
setMoveWarnings([]);
|
|
602
|
+
setMoveDialogOpen(true);
|
|
603
|
+
}, [doc]);
|
|
604
|
+
|
|
605
|
+
useEffect(() => {
|
|
606
|
+
if (!moveDialogOpen || !doc || !moveFolderPath.trim()) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
void apiFetch<{ refactorWarnings?: { warnings: string[] } }>(
|
|
610
|
+
`/api/docs/${encodeURIComponent(doc.docid)}/refactor-plan`,
|
|
611
|
+
{
|
|
612
|
+
method: "POST",
|
|
613
|
+
body: JSON.stringify({
|
|
614
|
+
operation: "move",
|
|
615
|
+
folderPath: moveFolderPath,
|
|
616
|
+
name: moveName,
|
|
617
|
+
uri: doc.uri,
|
|
618
|
+
}),
|
|
619
|
+
}
|
|
620
|
+
).then(({ data }) => {
|
|
621
|
+
setMoveWarnings(data?.refactorWarnings?.warnings ?? []);
|
|
622
|
+
});
|
|
623
|
+
}, [doc, moveDialogOpen, moveFolderPath, moveName]);
|
|
624
|
+
|
|
625
|
+
const handleMove = useCallback(async () => {
|
|
626
|
+
if (!doc) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
setMoving(true);
|
|
630
|
+
setMoveError(null);
|
|
631
|
+
const { data, error: err } = await apiFetch<MoveDocResponse>(
|
|
632
|
+
`/api/docs/${encodeURIComponent(doc.docid)}/move`,
|
|
633
|
+
{
|
|
634
|
+
method: "POST",
|
|
635
|
+
body: JSON.stringify({
|
|
636
|
+
folderPath: moveFolderPath,
|
|
637
|
+
name: moveName,
|
|
638
|
+
uri: doc.uri,
|
|
639
|
+
}),
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
setMoving(false);
|
|
643
|
+
|
|
644
|
+
if (err) {
|
|
645
|
+
setMoveError(err);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
setMoveDialogOpen(false);
|
|
650
|
+
if (data?.uri) {
|
|
651
|
+
navigate(`/doc?uri=${encodeURIComponent(data.uri)}`);
|
|
652
|
+
}
|
|
653
|
+
}, [doc, moveFolderPath, moveName, navigate]);
|
|
654
|
+
|
|
655
|
+
const handleStartDuplicate = useCallback(() => {
|
|
656
|
+
if (!doc) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
setDuplicateFolderPath(getParentPath(doc.relPath));
|
|
660
|
+
setDuplicateName(doc.relPath.split("/").pop() ?? doc.relPath);
|
|
661
|
+
setDuplicateError(null);
|
|
662
|
+
setDuplicateWarnings([]);
|
|
663
|
+
setDuplicateDialogOpen(true);
|
|
664
|
+
}, [doc]);
|
|
665
|
+
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
if (!duplicateDialogOpen || !doc) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
void apiFetch<{ refactorWarnings?: { warnings: string[] } }>(
|
|
671
|
+
`/api/docs/${encodeURIComponent(doc.docid)}/refactor-plan`,
|
|
672
|
+
{
|
|
673
|
+
method: "POST",
|
|
674
|
+
body: JSON.stringify({
|
|
675
|
+
operation: "duplicate",
|
|
676
|
+
folderPath: duplicateFolderPath || undefined,
|
|
677
|
+
name: duplicateName || undefined,
|
|
678
|
+
uri: doc.uri,
|
|
679
|
+
}),
|
|
680
|
+
}
|
|
681
|
+
).then(({ data }) => {
|
|
682
|
+
setDuplicateWarnings(data?.refactorWarnings?.warnings ?? []);
|
|
683
|
+
});
|
|
684
|
+
}, [doc, duplicateDialogOpen, duplicateFolderPath, duplicateName]);
|
|
685
|
+
|
|
686
|
+
const handleDuplicate = useCallback(async () => {
|
|
687
|
+
if (!doc) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
setDuplicating(true);
|
|
691
|
+
setDuplicateError(null);
|
|
692
|
+
const { data, error: err } = await apiFetch<DuplicateDocResponse>(
|
|
693
|
+
`/api/docs/${encodeURIComponent(doc.docid)}/duplicate`,
|
|
694
|
+
{
|
|
695
|
+
method: "POST",
|
|
696
|
+
body: JSON.stringify({
|
|
697
|
+
folderPath: duplicateFolderPath || undefined,
|
|
698
|
+
name: duplicateName || undefined,
|
|
699
|
+
uri: doc.uri,
|
|
700
|
+
}),
|
|
701
|
+
}
|
|
702
|
+
);
|
|
703
|
+
setDuplicating(false);
|
|
704
|
+
|
|
705
|
+
if (err) {
|
|
706
|
+
setDuplicateError(err);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
setDuplicateDialogOpen(false);
|
|
711
|
+
if (data?.uri) {
|
|
712
|
+
navigate(`/doc?uri=${encodeURIComponent(data.uri)}`);
|
|
713
|
+
}
|
|
714
|
+
}, [doc, duplicateFolderPath, duplicateName, navigate]);
|
|
715
|
+
|
|
716
|
+
useEffect(() => {
|
|
717
|
+
const unsubscribers = [
|
|
718
|
+
subscribeWorkspaceActionRequest("rename-current-note", () => {
|
|
719
|
+
if (doc?.capabilities.editable) {
|
|
720
|
+
handleStartRename();
|
|
721
|
+
}
|
|
722
|
+
}),
|
|
723
|
+
subscribeWorkspaceActionRequest("move-current-note", () => {
|
|
724
|
+
if (doc?.capabilities.editable) {
|
|
725
|
+
handleStartMove();
|
|
726
|
+
}
|
|
727
|
+
}),
|
|
728
|
+
subscribeWorkspaceActionRequest("duplicate-current-note", () => {
|
|
729
|
+
if (doc?.capabilities.editable) {
|
|
730
|
+
handleStartDuplicate();
|
|
731
|
+
}
|
|
732
|
+
}),
|
|
733
|
+
];
|
|
734
|
+
|
|
735
|
+
return () => {
|
|
736
|
+
for (const unsubscribe of unsubscribers) {
|
|
737
|
+
unsubscribe();
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}, [
|
|
741
|
+
doc?.capabilities.editable,
|
|
742
|
+
handleStartDuplicate,
|
|
743
|
+
handleStartMove,
|
|
744
|
+
handleStartRename,
|
|
745
|
+
]);
|
|
746
|
+
|
|
477
747
|
const handleReveal = useCallback(async () => {
|
|
478
748
|
if (!doc) {
|
|
479
749
|
return;
|
|
@@ -549,9 +819,8 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
549
819
|
setTimeout(() => setTagSaveSuccess(false), 2000);
|
|
550
820
|
}, [doc, editedTags]);
|
|
551
821
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
<nav aria-label="Document facts" className="space-y-0">
|
|
822
|
+
const renderPropertiesPathRail = () => (
|
|
823
|
+
<nav aria-label="Document properties" className="space-y-0">
|
|
555
824
|
{/* Section: Properties */}
|
|
556
825
|
<div className="px-3 pb-3">
|
|
557
826
|
<div className="mb-2.5 font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
@@ -630,11 +899,15 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
630
899
|
</button>
|
|
631
900
|
</div>
|
|
632
901
|
</div>
|
|
902
|
+
</nav>
|
|
903
|
+
);
|
|
633
904
|
|
|
634
|
-
|
|
905
|
+
/** Left rail — metadata + outline */
|
|
906
|
+
const renderDocumentFactsRail = () => (
|
|
907
|
+
<nav aria-label="Document facts" className="space-y-0">
|
|
908
|
+
{/* Frontmatter + tags */}
|
|
635
909
|
{(hasFrontmatter || showStandaloneTags) && (
|
|
636
910
|
<>
|
|
637
|
-
<div className="mx-3 border-border/20 border-t" />
|
|
638
911
|
<div className="px-3 py-3">
|
|
639
912
|
<div className="mb-2 flex items-center justify-between">
|
|
640
913
|
<span className="font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
@@ -724,6 +997,71 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
724
997
|
</div>
|
|
725
998
|
</>
|
|
726
999
|
)}
|
|
1000
|
+
|
|
1001
|
+
{sections.length > 0 && (
|
|
1002
|
+
<>
|
|
1003
|
+
<div className="mx-3 border-border/20 border-t" />
|
|
1004
|
+
<div className="px-3 py-3">
|
|
1005
|
+
<div className="mb-2 font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
1006
|
+
Outline
|
|
1007
|
+
</div>
|
|
1008
|
+
<div className="space-y-0.5">
|
|
1009
|
+
{sections.map((section) => (
|
|
1010
|
+
<div
|
|
1011
|
+
className={`flex items-center gap-1 rounded px-1 py-0.5 ${
|
|
1012
|
+
activeSectionAnchor === section.anchor
|
|
1013
|
+
? "bg-primary/10 text-primary"
|
|
1014
|
+
: "text-muted-foreground"
|
|
1015
|
+
}`}
|
|
1016
|
+
key={section.anchor}
|
|
1017
|
+
style={{ paddingLeft: `${section.level * 10}px` }}
|
|
1018
|
+
>
|
|
1019
|
+
<button
|
|
1020
|
+
className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 rounded px-1 py-0.5 text-left text-xs transition-colors hover:bg-muted/20 hover:text-foreground"
|
|
1021
|
+
onClick={() => {
|
|
1022
|
+
setShowRawView(false);
|
|
1023
|
+
requestAnimationFrame(() => {
|
|
1024
|
+
document
|
|
1025
|
+
.getElementById(section.anchor)
|
|
1026
|
+
?.scrollIntoView({
|
|
1027
|
+
behavior: "smooth",
|
|
1028
|
+
block: "start",
|
|
1029
|
+
});
|
|
1030
|
+
window.history.replaceState(
|
|
1031
|
+
{},
|
|
1032
|
+
"",
|
|
1033
|
+
`${buildDocDeepLink({
|
|
1034
|
+
uri: doc?.uri ?? "",
|
|
1035
|
+
view: "rendered",
|
|
1036
|
+
})}#${section.anchor}`
|
|
1037
|
+
);
|
|
1038
|
+
});
|
|
1039
|
+
}}
|
|
1040
|
+
type="button"
|
|
1041
|
+
>
|
|
1042
|
+
<ChevronRightIcon className="size-3 shrink-0" />
|
|
1043
|
+
<span className="truncate">{section.title}</span>
|
|
1044
|
+
</button>
|
|
1045
|
+
<button
|
|
1046
|
+
className="cursor-pointer rounded p-1 transition-colors hover:bg-muted/20 hover:text-foreground"
|
|
1047
|
+
onClick={() => {
|
|
1048
|
+
void navigator.clipboard.writeText(
|
|
1049
|
+
`${window.location.origin}${buildDocDeepLink({
|
|
1050
|
+
uri: doc?.uri ?? "",
|
|
1051
|
+
view: "rendered",
|
|
1052
|
+
})}#${section.anchor}`
|
|
1053
|
+
);
|
|
1054
|
+
}}
|
|
1055
|
+
type="button"
|
|
1056
|
+
>
|
|
1057
|
+
<CopyIcon className="size-3" />
|
|
1058
|
+
</button>
|
|
1059
|
+
</div>
|
|
1060
|
+
))}
|
|
1061
|
+
</div>
|
|
1062
|
+
</div>
|
|
1063
|
+
</>
|
|
1064
|
+
)}
|
|
727
1065
|
</nav>
|
|
728
1066
|
);
|
|
729
1067
|
|
|
@@ -961,6 +1299,24 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
961
1299
|
<TextIcon className="size-4" />
|
|
962
1300
|
Rename
|
|
963
1301
|
</Button>
|
|
1302
|
+
<Button
|
|
1303
|
+
className="gap-1.5"
|
|
1304
|
+
onClick={handleStartMove}
|
|
1305
|
+
size="sm"
|
|
1306
|
+
variant="outline"
|
|
1307
|
+
>
|
|
1308
|
+
<FolderOpen className="size-4" />
|
|
1309
|
+
Move
|
|
1310
|
+
</Button>
|
|
1311
|
+
<Button
|
|
1312
|
+
className="gap-1.5"
|
|
1313
|
+
onClick={handleStartDuplicate}
|
|
1314
|
+
size="sm"
|
|
1315
|
+
variant="outline"
|
|
1316
|
+
>
|
|
1317
|
+
<CopyIcon className="size-4" />
|
|
1318
|
+
Duplicate
|
|
1319
|
+
</Button>
|
|
964
1320
|
</>
|
|
965
1321
|
) : (
|
|
966
1322
|
<>
|
|
@@ -1036,10 +1392,12 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1036
1392
|
</header>
|
|
1037
1393
|
|
|
1038
1394
|
<div className="mx-auto flex max-w-[1800px] gap-5 px-6 xl:px-8">
|
|
1039
|
-
{/* Left rail —
|
|
1395
|
+
{/* Left rail — metadata + outline */}
|
|
1040
1396
|
{doc && (
|
|
1041
1397
|
<aside className="hidden w-[200px] shrink-0 border-border/15 border-r pr-2 py-6 lg:block">
|
|
1042
|
-
<div className="sticky top-24
|
|
1398
|
+
<div className="sticky top-24 max-h-[calc(100vh-7rem)] overflow-y-auto pr-1">
|
|
1399
|
+
{renderDocumentFactsRail()}
|
|
1400
|
+
</div>
|
|
1043
1401
|
</aside>
|
|
1044
1402
|
)}
|
|
1045
1403
|
|
|
@@ -1202,10 +1560,11 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1202
1560
|
)}
|
|
1203
1561
|
</main>
|
|
1204
1562
|
|
|
1205
|
-
{/* Right rail — relationships */}
|
|
1563
|
+
{/* Right rail — properties/path + relationships */}
|
|
1206
1564
|
{doc && (
|
|
1207
|
-
<aside className="hidden w-[
|
|
1208
|
-
<div className="sticky top-
|
|
1565
|
+
<aside className="hidden w-[250px] min-w-0 shrink-0 overflow-hidden border-border/15 border-l pl-2 pt-2 pb-6 lg:block">
|
|
1566
|
+
<div className="sticky top-18 min-w-0 max-h-[calc(100vh-5.5rem)] space-y-1 overflow-y-auto overflow-x-hidden pr-1">
|
|
1567
|
+
{renderPropertiesPathRail()}
|
|
1209
1568
|
<BacklinksPanel
|
|
1210
1569
|
docId={doc.docid}
|
|
1211
1570
|
onNavigate={(uri) =>
|
|
@@ -1322,6 +1681,13 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1322
1681
|
{renameError}
|
|
1323
1682
|
</div>
|
|
1324
1683
|
)}
|
|
1684
|
+
{renameWarnings.length > 0 && (
|
|
1685
|
+
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500 text-sm">
|
|
1686
|
+
{renameWarnings.map((warning) => (
|
|
1687
|
+
<div key={warning}>{warning}</div>
|
|
1688
|
+
))}
|
|
1689
|
+
</div>
|
|
1690
|
+
)}
|
|
1325
1691
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
1326
1692
|
<Button
|
|
1327
1693
|
onClick={() => setRenameDialogOpen(false)}
|
|
@@ -1338,6 +1704,102 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1338
1704
|
</DialogFooter>
|
|
1339
1705
|
</DialogContent>
|
|
1340
1706
|
</Dialog>
|
|
1707
|
+
|
|
1708
|
+
<Dialog onOpenChange={setMoveDialogOpen} open={moveDialogOpen}>
|
|
1709
|
+
<DialogContent>
|
|
1710
|
+
<DialogHeader>
|
|
1711
|
+
<DialogTitle>Move document</DialogTitle>
|
|
1712
|
+
<DialogDescription>
|
|
1713
|
+
Move the current note to another folder inside this collection.
|
|
1714
|
+
</DialogDescription>
|
|
1715
|
+
</DialogHeader>
|
|
1716
|
+
<div className="space-y-3">
|
|
1717
|
+
<Input
|
|
1718
|
+
onChange={(event) => setMoveFolderPath(event.target.value)}
|
|
1719
|
+
placeholder="projects/research"
|
|
1720
|
+
value={moveFolderPath}
|
|
1721
|
+
/>
|
|
1722
|
+
<Input
|
|
1723
|
+
onChange={(event) => setMoveName(event.target.value)}
|
|
1724
|
+
placeholder="note.md"
|
|
1725
|
+
value={moveName}
|
|
1726
|
+
/>
|
|
1727
|
+
{moveError && (
|
|
1728
|
+
<div className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm">
|
|
1729
|
+
{moveError}
|
|
1730
|
+
</div>
|
|
1731
|
+
)}
|
|
1732
|
+
{moveWarnings.length > 0 && (
|
|
1733
|
+
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500 text-sm">
|
|
1734
|
+
{moveWarnings.map((warning) => (
|
|
1735
|
+
<div key={warning}>{warning}</div>
|
|
1736
|
+
))}
|
|
1737
|
+
</div>
|
|
1738
|
+
)}
|
|
1739
|
+
</div>
|
|
1740
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
1741
|
+
<Button onClick={() => setMoveDialogOpen(false)} variant="outline">
|
|
1742
|
+
Cancel
|
|
1743
|
+
</Button>
|
|
1744
|
+
<Button disabled={moving} onClick={() => void handleMove()}>
|
|
1745
|
+
{moving && <Loader2Icon className="mr-1.5 size-4 animate-spin" />}
|
|
1746
|
+
Move
|
|
1747
|
+
</Button>
|
|
1748
|
+
</DialogFooter>
|
|
1749
|
+
</DialogContent>
|
|
1750
|
+
</Dialog>
|
|
1751
|
+
|
|
1752
|
+
<Dialog onOpenChange={setDuplicateDialogOpen} open={duplicateDialogOpen}>
|
|
1753
|
+
<DialogContent>
|
|
1754
|
+
<DialogHeader>
|
|
1755
|
+
<DialogTitle>Duplicate document</DialogTitle>
|
|
1756
|
+
<DialogDescription>
|
|
1757
|
+
Create a copy of this note in the current or another folder.
|
|
1758
|
+
</DialogDescription>
|
|
1759
|
+
</DialogHeader>
|
|
1760
|
+
<div className="space-y-3">
|
|
1761
|
+
<Input
|
|
1762
|
+
onChange={(event) => setDuplicateFolderPath(event.target.value)}
|
|
1763
|
+
placeholder="projects/research"
|
|
1764
|
+
value={duplicateFolderPath}
|
|
1765
|
+
/>
|
|
1766
|
+
<Input
|
|
1767
|
+
onChange={(event) => setDuplicateName(event.target.value)}
|
|
1768
|
+
placeholder="note-copy.md"
|
|
1769
|
+
value={duplicateName}
|
|
1770
|
+
/>
|
|
1771
|
+
{duplicateError && (
|
|
1772
|
+
<div className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm">
|
|
1773
|
+
{duplicateError}
|
|
1774
|
+
</div>
|
|
1775
|
+
)}
|
|
1776
|
+
{duplicateWarnings.length > 0 && (
|
|
1777
|
+
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500 text-sm">
|
|
1778
|
+
{duplicateWarnings.map((warning) => (
|
|
1779
|
+
<div key={warning}>{warning}</div>
|
|
1780
|
+
))}
|
|
1781
|
+
</div>
|
|
1782
|
+
)}
|
|
1783
|
+
</div>
|
|
1784
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
1785
|
+
<Button
|
|
1786
|
+
onClick={() => setDuplicateDialogOpen(false)}
|
|
1787
|
+
variant="outline"
|
|
1788
|
+
>
|
|
1789
|
+
Cancel
|
|
1790
|
+
</Button>
|
|
1791
|
+
<Button
|
|
1792
|
+
disabled={duplicating}
|
|
1793
|
+
onClick={() => void handleDuplicate()}
|
|
1794
|
+
>
|
|
1795
|
+
{duplicating && (
|
|
1796
|
+
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
|
1797
|
+
)}
|
|
1798
|
+
Duplicate
|
|
1799
|
+
</Button>
|
|
1800
|
+
</DialogFooter>
|
|
1801
|
+
</DialogContent>
|
|
1802
|
+
</Dialog>
|
|
1341
1803
|
</div>
|
|
1342
1804
|
);
|
|
1343
1805
|
}
|