@gmickel/gno 1.30.6 → 1.31.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 +2 -2
- package/assets/skill/SKILL.md +2 -0
- package/assets/skill/mcp-reference.md +6 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.6.zip → gno-browser-clipper-v1.31.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.31.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/mcp.md +62 -0
- package/spec/output-schemas/section-target-create-result.schema.json +20 -0
- package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
- package/spec/output-schemas/section-target.schema.json +118 -0
- package/spec/output-schemas/section.schema.json +113 -0
- package/src/core/section-parse.ts +187 -0
- package/src/core/section-target-link.ts +154 -0
- package/src/core/section-target-resolve.ts +351 -0
- package/src/core/section-target-transport.ts +519 -0
- package/src/core/section-target.ts +263 -0
- package/src/core/sections.ts +60 -115
- package/src/mcp/AGENTS.md +1 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +19 -0
- package/src/mcp/tools/sections.ts +512 -0
- package/src/sdk/client.ts +71 -1
- package/src/sdk/index.ts +5 -0
- package/src/sdk/types.ts +29 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/section-links.ts +189 -0
- package/src/serve/public/pages/DocView.tsx +219 -36
- package/src/serve/routes/section-targets.ts +221 -0
- package/src/serve/server.ts +34 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.6.zip.sha256 +0 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readable section deep links and optional citation-safe selectors.
|
|
3
|
+
*
|
|
4
|
+
* Copy-link stays human-readable (`#anchor` only). Citation links add a
|
|
5
|
+
* bounded, versioned `st` query param for conservative recovery.
|
|
6
|
+
*
|
|
7
|
+
* @module src/serve/public/lib/section-links
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createSectionTarget,
|
|
12
|
+
decodeSectionTargetLinkParam,
|
|
13
|
+
encodeSectionTargetLinkParam,
|
|
14
|
+
isNavigableSectionResolution,
|
|
15
|
+
resolveSectionTarget,
|
|
16
|
+
SECTION_TARGET_LINK_PARAM,
|
|
17
|
+
type SectionResolutionStatus,
|
|
18
|
+
type SectionTargetV1,
|
|
19
|
+
} from "../../../core/sections";
|
|
20
|
+
import { buildDocDeepLink, type DocumentDeepLinkTarget } from "./deep-links";
|
|
21
|
+
|
|
22
|
+
export interface SectionLinkTarget extends DocumentDeepLinkTarget {
|
|
23
|
+
/** Readable heading anchor (HTML id). */
|
|
24
|
+
anchor: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SectionLinkNoticeKind =
|
|
28
|
+
| "copied_link"
|
|
29
|
+
| "copied_citation"
|
|
30
|
+
| "citation_unavailable"
|
|
31
|
+
| "clipboard_unavailable"
|
|
32
|
+
| SectionResolutionStatus
|
|
33
|
+
| "invalid_citation";
|
|
34
|
+
|
|
35
|
+
/** Ephemeral, content-free status copy for outline/rail feedback. */
|
|
36
|
+
export const SECTION_LINK_NOTICE_COPY: Record<SectionLinkNoticeKind, string> = {
|
|
37
|
+
copied_link: "Copied section link",
|
|
38
|
+
copied_citation: "Copied citation link",
|
|
39
|
+
citation_unavailable: "Citation unavailable for this section",
|
|
40
|
+
clipboard_unavailable: "Could not copy link",
|
|
41
|
+
exact: "Exact section match",
|
|
42
|
+
recovered: "Section recovered",
|
|
43
|
+
ambiguous: "Ambiguous section link — not navigating",
|
|
44
|
+
stale: "Stale section link — not navigating",
|
|
45
|
+
missing: "Section link not found — not navigating",
|
|
46
|
+
invalid_citation: "Invalid section citation — not navigating",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Absolute readable section URL for human sharing (no durable selector). */
|
|
50
|
+
export function buildReadableSectionUrl(
|
|
51
|
+
origin: string,
|
|
52
|
+
target: SectionLinkTarget
|
|
53
|
+
): string {
|
|
54
|
+
const path = `${buildDocDeepLink({
|
|
55
|
+
uri: target.uri,
|
|
56
|
+
view: target.view ?? "rendered",
|
|
57
|
+
})}#${target.anchor}`;
|
|
58
|
+
return `${origin}${path}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Absolute citation URL: readable `#anchor` plus bounded `st` selector.
|
|
63
|
+
* Returns null when a faithful bounded encoding cannot be produced.
|
|
64
|
+
*/
|
|
65
|
+
export function buildCitationSectionUrl(
|
|
66
|
+
origin: string,
|
|
67
|
+
target: SectionLinkTarget,
|
|
68
|
+
sectionTarget: SectionTargetV1
|
|
69
|
+
): string | null {
|
|
70
|
+
const encoded = encodeSectionTargetLinkParam(sectionTarget);
|
|
71
|
+
if (!encoded) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const params = new URLSearchParams({ uri: target.uri });
|
|
75
|
+
params.set("view", target.view ?? "rendered");
|
|
76
|
+
params.set(SECTION_TARGET_LINK_PARAM, encoded);
|
|
77
|
+
return `${origin}/doc?${params.toString()}#${target.anchor}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Read the raw `st` query value from a location search string. */
|
|
81
|
+
export function readSectionTargetLinkParam(search: string): string | null {
|
|
82
|
+
const value = new URLSearchParams(search).get(SECTION_TARGET_LINK_PARAM);
|
|
83
|
+
return value && value.length > 0 ? value : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Strip the additive `st` param while preserving other query fields. */
|
|
87
|
+
export function stripSectionTargetLinkParam(search: string): string {
|
|
88
|
+
const params = new URLSearchParams(
|
|
89
|
+
search.startsWith("?") ? search.slice(1) : search
|
|
90
|
+
);
|
|
91
|
+
params.delete(SECTION_TARGET_LINK_PARAM);
|
|
92
|
+
const serialized = params.toString();
|
|
93
|
+
return serialized.length > 0 ? `?${serialized}` : "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ResolveSectionLinkInput {
|
|
97
|
+
content: string;
|
|
98
|
+
uri: string;
|
|
99
|
+
/** Raw `st` query value. */
|
|
100
|
+
encodedTarget: string | null;
|
|
101
|
+
/** Readable hash without leading `#`. */
|
|
102
|
+
hashAnchor: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ResolveSectionLinkResult {
|
|
106
|
+
/** When true, hash-only scroll must not run. */
|
|
107
|
+
blockHashNavigation: boolean;
|
|
108
|
+
/** Anchor to scroll to when navigable. */
|
|
109
|
+
navigateAnchor: string | null;
|
|
110
|
+
notice: SectionLinkNoticeKind | null;
|
|
111
|
+
/** Drop `st` from the address bar after a successful recovery. */
|
|
112
|
+
cleanCitationParam: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve an optional durable selector against current document content.
|
|
117
|
+
* Readable `#anchor`-only links keep legacy behavior (no block).
|
|
118
|
+
*/
|
|
119
|
+
export async function resolveSectionLinkNavigation(
|
|
120
|
+
input: ResolveSectionLinkInput
|
|
121
|
+
): Promise<ResolveSectionLinkResult> {
|
|
122
|
+
if (!input.encodedTarget) {
|
|
123
|
+
return {
|
|
124
|
+
blockHashNavigation: false,
|
|
125
|
+
navigateAnchor: input.hashAnchor || null,
|
|
126
|
+
notice: null,
|
|
127
|
+
cleanCitationParam: false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const target = decodeSectionTargetLinkParam(input.encodedTarget);
|
|
132
|
+
if (!target) {
|
|
133
|
+
return {
|
|
134
|
+
blockHashNavigation: true,
|
|
135
|
+
navigateAnchor: null,
|
|
136
|
+
notice: "invalid_citation",
|
|
137
|
+
cleanCitationParam: false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const resolution = await resolveSectionTarget({
|
|
142
|
+
content: input.content,
|
|
143
|
+
target,
|
|
144
|
+
uri: input.uri,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (isNavigableSectionResolution(resolution)) {
|
|
148
|
+
return {
|
|
149
|
+
blockHashNavigation: false,
|
|
150
|
+
navigateAnchor: resolution.section.anchor,
|
|
151
|
+
notice: resolution.status,
|
|
152
|
+
cleanCitationParam: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
blockHashNavigation: true,
|
|
158
|
+
navigateAnchor: null,
|
|
159
|
+
notice: resolution.status,
|
|
160
|
+
cleanCitationParam: false,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Create a citation URL for an outline section using shared core create. */
|
|
165
|
+
export async function createCitationSectionUrl(input: {
|
|
166
|
+
origin: string;
|
|
167
|
+
uri: string;
|
|
168
|
+
content: string;
|
|
169
|
+
anchor: string;
|
|
170
|
+
view?: "rendered" | "source";
|
|
171
|
+
}): Promise<string | null> {
|
|
172
|
+
const target = await createSectionTarget({
|
|
173
|
+
content: input.content,
|
|
174
|
+
uri: input.uri,
|
|
175
|
+
anchor: input.anchor,
|
|
176
|
+
});
|
|
177
|
+
if (!target) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return buildCitationSectionUrl(
|
|
181
|
+
input.origin,
|
|
182
|
+
{
|
|
183
|
+
uri: input.uri,
|
|
184
|
+
view: input.view ?? "rendered",
|
|
185
|
+
anchor: input.anchor,
|
|
186
|
+
},
|
|
187
|
+
target
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
LinkIcon,
|
|
14
14
|
Loader2Icon,
|
|
15
15
|
PencilIcon,
|
|
16
|
+
QuoteIcon,
|
|
16
17
|
Share2Icon,
|
|
17
18
|
SquareArrowOutUpRightIcon,
|
|
18
19
|
TextIcon,
|
|
@@ -83,6 +84,15 @@ import {
|
|
|
83
84
|
downloadPublishArtifactFile,
|
|
84
85
|
type PublishExportResponse,
|
|
85
86
|
} from "../lib/publish-export";
|
|
87
|
+
import {
|
|
88
|
+
buildReadableSectionUrl,
|
|
89
|
+
createCitationSectionUrl,
|
|
90
|
+
readSectionTargetLinkParam,
|
|
91
|
+
resolveSectionLinkNavigation,
|
|
92
|
+
SECTION_LINK_NOTICE_COPY,
|
|
93
|
+
stripSectionTargetLinkParam,
|
|
94
|
+
type SectionLinkNoticeKind,
|
|
95
|
+
} from "../lib/section-links";
|
|
86
96
|
import { subscribeWorkspaceActionRequest } from "../lib/workspace-events";
|
|
87
97
|
|
|
88
98
|
/** Lazy so pdfjs is never pulled for non-PDF documents. */
|
|
@@ -389,9 +399,13 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
389
399
|
const [activeSectionAnchor, setActiveSectionAnchor] = useState<string | null>(
|
|
390
400
|
null
|
|
391
401
|
);
|
|
402
|
+
const [sectionLinkNotice, setSectionLinkNotice] =
|
|
403
|
+
useState<SectionLinkNoticeKind | null>(null);
|
|
404
|
+
const [blockHashNavigation, setBlockHashNavigation] = useState(false);
|
|
392
405
|
|
|
393
406
|
// Request sequencing - ignore stale responses on rapid navigation
|
|
394
407
|
const requestIdRef = useRef(0);
|
|
408
|
+
const sectionResolveRequestRef = useRef(0);
|
|
395
409
|
const latestDocEvent = useDocEvents();
|
|
396
410
|
|
|
397
411
|
// App remounts page on route/query changes, so URI is stable per render.
|
|
@@ -404,6 +418,10 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
404
418
|
() => window.location.hash.replace(/^#/u, ""),
|
|
405
419
|
[]
|
|
406
420
|
);
|
|
421
|
+
const encodedSectionTarget = useMemo(
|
|
422
|
+
() => readSectionTargetLinkParam(window.location.search),
|
|
423
|
+
[]
|
|
424
|
+
);
|
|
407
425
|
const highlightedLines = useMemo(() => {
|
|
408
426
|
if (!currentTarget.lineStart) return [];
|
|
409
427
|
const end = currentTarget.lineEnd ?? currentTarget.lineStart;
|
|
@@ -564,7 +582,78 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
564
582
|
}, []);
|
|
565
583
|
|
|
566
584
|
useEffect(() => {
|
|
567
|
-
if (!
|
|
585
|
+
if (!sectionLinkNotice) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const timer = window.setTimeout(() => setSectionLinkNotice(null), 3200);
|
|
589
|
+
return () => {
|
|
590
|
+
window.clearTimeout(timer);
|
|
591
|
+
};
|
|
592
|
+
}, [sectionLinkNotice]);
|
|
593
|
+
|
|
594
|
+
useEffect(() => {
|
|
595
|
+
if (!doc?.content || loading || !encodedSectionTarget) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const requestId = ++sectionResolveRequestRef.current;
|
|
600
|
+
const content = doc.content;
|
|
601
|
+
void resolveSectionLinkNavigation({
|
|
602
|
+
content,
|
|
603
|
+
uri: doc.uri,
|
|
604
|
+
encodedTarget: encodedSectionTarget,
|
|
605
|
+
hashAnchor: currentHash,
|
|
606
|
+
}).then((result) => {
|
|
607
|
+
if (requestId !== sectionResolveRequestRef.current) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
setBlockHashNavigation(result.blockHashNavigation);
|
|
611
|
+
if (result.notice) {
|
|
612
|
+
setSectionLinkNotice(result.notice);
|
|
613
|
+
}
|
|
614
|
+
if (result.cleanCitationParam) {
|
|
615
|
+
const cleanedSearch = stripSectionTargetLinkParam(
|
|
616
|
+
window.location.search
|
|
617
|
+
);
|
|
618
|
+
const nextHash = result.navigateAnchor
|
|
619
|
+
? `#${result.navigateAnchor}`
|
|
620
|
+
: window.location.hash;
|
|
621
|
+
window.history.replaceState(
|
|
622
|
+
{},
|
|
623
|
+
"",
|
|
624
|
+
`${window.location.pathname}${cleanedSearch}${nextHash}`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (result.blockHashNavigation || !result.navigateAnchor) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (showRawView) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
requestAnimationFrame(() => {
|
|
634
|
+
document
|
|
635
|
+
.getElementById(result.navigateAnchor ?? "")
|
|
636
|
+
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
637
|
+
setActiveSectionAnchor(result.navigateAnchor);
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
}, [
|
|
641
|
+
currentHash,
|
|
642
|
+
doc?.content,
|
|
643
|
+
doc?.uri,
|
|
644
|
+
encodedSectionTarget,
|
|
645
|
+
loading,
|
|
646
|
+
showRawView,
|
|
647
|
+
]);
|
|
648
|
+
|
|
649
|
+
useEffect(() => {
|
|
650
|
+
if (
|
|
651
|
+
blockHashNavigation ||
|
|
652
|
+
encodedSectionTarget ||
|
|
653
|
+
!currentHash ||
|
|
654
|
+
showRawView ||
|
|
655
|
+
loading
|
|
656
|
+
) {
|
|
568
657
|
return;
|
|
569
658
|
}
|
|
570
659
|
|
|
@@ -574,7 +663,13 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
574
663
|
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
575
664
|
setActiveSectionAnchor(currentHash);
|
|
576
665
|
});
|
|
577
|
-
}, [
|
|
666
|
+
}, [
|
|
667
|
+
blockHashNavigation,
|
|
668
|
+
currentHash,
|
|
669
|
+
encodedSectionTarget,
|
|
670
|
+
loading,
|
|
671
|
+
showRawView,
|
|
672
|
+
]);
|
|
578
673
|
|
|
579
674
|
const breadcrumbs = doc ? parseBreadcrumbs(doc.collection, doc.relPath) : [];
|
|
580
675
|
const sections = useMemo(
|
|
@@ -582,6 +677,79 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
582
677
|
[parsedContent.body]
|
|
583
678
|
);
|
|
584
679
|
|
|
680
|
+
const copyReadableSectionLink = useCallback(
|
|
681
|
+
(anchor: string) => {
|
|
682
|
+
if (!doc) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
void navigator.clipboard
|
|
686
|
+
.writeText(
|
|
687
|
+
buildReadableSectionUrl(window.location.origin, {
|
|
688
|
+
uri: doc.uri,
|
|
689
|
+
view: "rendered",
|
|
690
|
+
anchor,
|
|
691
|
+
})
|
|
692
|
+
)
|
|
693
|
+
.then(() => {
|
|
694
|
+
setSectionLinkNotice("copied_link");
|
|
695
|
+
})
|
|
696
|
+
.catch(() => {
|
|
697
|
+
setSectionLinkNotice("clipboard_unavailable");
|
|
698
|
+
});
|
|
699
|
+
},
|
|
700
|
+
[doc]
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const copyCitationSectionLink = useCallback(
|
|
704
|
+
async (anchor: string) => {
|
|
705
|
+
const content = doc?.content;
|
|
706
|
+
if (!doc || !content) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const citationUrl = await createCitationSectionUrl({
|
|
710
|
+
origin: window.location.origin,
|
|
711
|
+
uri: doc.uri,
|
|
712
|
+
content,
|
|
713
|
+
anchor,
|
|
714
|
+
view: "rendered",
|
|
715
|
+
});
|
|
716
|
+
if (!citationUrl) {
|
|
717
|
+
setSectionLinkNotice("citation_unavailable");
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
await navigator.clipboard.writeText(citationUrl);
|
|
722
|
+
setSectionLinkNotice("copied_citation");
|
|
723
|
+
} catch {
|
|
724
|
+
setSectionLinkNotice("clipboard_unavailable");
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
[doc]
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
const jumpToSection = useCallback(
|
|
731
|
+
(anchor: string) => {
|
|
732
|
+
setShowRawView(false);
|
|
733
|
+
setBlockHashNavigation(false);
|
|
734
|
+
requestAnimationFrame(() => {
|
|
735
|
+
document.getElementById(anchor)?.scrollIntoView({
|
|
736
|
+
behavior: "smooth",
|
|
737
|
+
block: "start",
|
|
738
|
+
});
|
|
739
|
+
window.history.replaceState(
|
|
740
|
+
{},
|
|
741
|
+
"",
|
|
742
|
+
`${buildDocDeepLink({
|
|
743
|
+
uri: doc?.uri ?? "",
|
|
744
|
+
view: "rendered",
|
|
745
|
+
})}#${anchor}`
|
|
746
|
+
);
|
|
747
|
+
setActiveSectionAnchor(anchor);
|
|
748
|
+
});
|
|
749
|
+
},
|
|
750
|
+
[doc?.uri]
|
|
751
|
+
);
|
|
752
|
+
|
|
585
753
|
useEffect(() => {
|
|
586
754
|
if (showRawView || sections.length === 0) {
|
|
587
755
|
setActiveSectionAnchor(sections[0]?.anchor ?? null);
|
|
@@ -1170,8 +1338,19 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1170
1338
|
<>
|
|
1171
1339
|
<div className="mx-3 border-border/20 border-t" />
|
|
1172
1340
|
<div className="px-3 py-3">
|
|
1173
|
-
<div className="mb-2
|
|
1174
|
-
|
|
1341
|
+
<div className="mb-2 flex items-center justify-between gap-2">
|
|
1342
|
+
<div className="font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
1343
|
+
Outline
|
|
1344
|
+
</div>
|
|
1345
|
+
{sectionLinkNotice && (
|
|
1346
|
+
<div
|
|
1347
|
+
aria-live="polite"
|
|
1348
|
+
className="min-w-0 truncate font-mono text-[10px] text-muted-foreground/70"
|
|
1349
|
+
role="status"
|
|
1350
|
+
>
|
|
1351
|
+
{SECTION_LINK_NOTICE_COPY[sectionLinkNotice]}
|
|
1352
|
+
</div>
|
|
1353
|
+
)}
|
|
1175
1354
|
</div>
|
|
1176
1355
|
<div className="w-full min-w-0 max-w-full space-y-0.5 overflow-x-hidden">
|
|
1177
1356
|
{sections.map((section) => (
|
|
@@ -1185,25 +1364,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1185
1364
|
style={{ paddingLeft: `${section.level * 7}px` }}
|
|
1186
1365
|
>
|
|
1187
1366
|
<button
|
|
1188
|
-
className="flex w-full min-w-0 max-w-full cursor-pointer items-start gap-2 overflow-hidden rounded px-1 py-0.5 pr-
|
|
1367
|
+
className="flex w-full min-w-0 max-w-full cursor-pointer items-start gap-2 overflow-hidden rounded px-1 py-0.5 pr-12 text-left text-xs transition-colors hover:bg-muted/20 hover:text-foreground"
|
|
1189
1368
|
onClick={() => {
|
|
1190
|
-
|
|
1191
|
-
requestAnimationFrame(() => {
|
|
1192
|
-
document
|
|
1193
|
-
.getElementById(section.anchor)
|
|
1194
|
-
?.scrollIntoView({
|
|
1195
|
-
behavior: "smooth",
|
|
1196
|
-
block: "start",
|
|
1197
|
-
});
|
|
1198
|
-
window.history.replaceState(
|
|
1199
|
-
{},
|
|
1200
|
-
"",
|
|
1201
|
-
`${buildDocDeepLink({
|
|
1202
|
-
uri: doc?.uri ?? "",
|
|
1203
|
-
view: "rendered",
|
|
1204
|
-
})}#${section.anchor}`
|
|
1205
|
-
);
|
|
1206
|
-
});
|
|
1369
|
+
jumpToSection(section.anchor);
|
|
1207
1370
|
}}
|
|
1208
1371
|
type="button"
|
|
1209
1372
|
>
|
|
@@ -1221,20 +1384,40 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1221
1384
|
</TooltipContent>
|
|
1222
1385
|
</Tooltip>
|
|
1223
1386
|
</button>
|
|
1224
|
-
<
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1387
|
+
<div className="absolute top-1 right-1 flex items-center gap-0.5 opacity-0 transition-all focus-within:opacity-100 group-hover:opacity-100">
|
|
1388
|
+
<Tooltip>
|
|
1389
|
+
<TooltipTrigger asChild>
|
|
1390
|
+
<button
|
|
1391
|
+
aria-label={`Copy link to ${section.title}`}
|
|
1392
|
+
className="cursor-pointer rounded p-1 hover:bg-muted/20 hover:text-foreground"
|
|
1393
|
+
onClick={() => {
|
|
1394
|
+
copyReadableSectionLink(section.anchor);
|
|
1395
|
+
}}
|
|
1396
|
+
type="button"
|
|
1397
|
+
>
|
|
1398
|
+
<CopyIcon className="size-3" />
|
|
1399
|
+
</button>
|
|
1400
|
+
</TooltipTrigger>
|
|
1401
|
+
<TooltipContent side="left">Copy link</TooltipContent>
|
|
1402
|
+
</Tooltip>
|
|
1403
|
+
<Tooltip>
|
|
1404
|
+
<TooltipTrigger asChild>
|
|
1405
|
+
<button
|
|
1406
|
+
aria-label={`Copy local citation link to ${section.title}`}
|
|
1407
|
+
className="cursor-pointer rounded p-1 hover:bg-muted/20 hover:text-foreground"
|
|
1408
|
+
onClick={() => {
|
|
1409
|
+
void copyCitationSectionLink(section.anchor);
|
|
1410
|
+
}}
|
|
1411
|
+
type="button"
|
|
1412
|
+
>
|
|
1413
|
+
<QuoteIcon className="size-3" />
|
|
1414
|
+
</button>
|
|
1415
|
+
</TooltipTrigger>
|
|
1416
|
+
<TooltipContent side="left">
|
|
1417
|
+
Copy local citation link
|
|
1418
|
+
</TooltipContent>
|
|
1419
|
+
</Tooltip>
|
|
1420
|
+
</div>
|
|
1238
1421
|
</div>
|
|
1239
1422
|
))}
|
|
1240
1423
|
</div>
|