@tanishqkancharla/diffmap 0.1.3
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/LICENSE +21 -0
- package/README.md +212 -0
- package/bin.js +5 -0
- package/index.html +38 -0
- package/package.json +71 -0
- package/src/annotations.ts +82 -0
- package/src/cli.ts +94 -0
- package/src/codeFont.ts +2 -0
- package/src/contentPlugin.ts +47 -0
- package/src/definitions.ts +320 -0
- package/src/diagramAnnotations.ts +34 -0
- package/src/errors.ts +41 -0
- package/src/extractDocument.ts +20 -0
- package/src/gist/GistApp.tsx +292 -0
- package/src/gist/GistStatus.tsx +137 -0
- package/src/gist/fetchGist.ts +128 -0
- package/src/gist/main.tsx +25 -0
- package/src/gist/route.test.ts +90 -0
- package/src/gist/route.ts +134 -0
- package/src/index.ts +22 -0
- package/src/invokeCwd.ts +10 -0
- package/src/mermaid.ts +29 -0
- package/src/normalizeDiff.ts +39 -0
- package/src/parseFence.ts +91 -0
- package/src/parseViewer.ts +399 -0
- package/src/registry.ts +121 -0
- package/src/serve.ts +350 -0
- package/src/share.ts +94 -0
- package/src/viewer/CallStackDiff.tsx +156 -0
- package/src/viewer/Check.tsx +24 -0
- package/src/viewer/CodeDiff.tsx +26 -0
- package/src/viewer/ComarkView.tsx +209 -0
- package/src/viewer/DiffButton.tsx +24 -0
- package/src/viewer/DoneButton.tsx +32 -0
- package/src/viewer/Fence.tsx +51 -0
- package/src/viewer/FileExcerpt.tsx +65 -0
- package/src/viewer/HtmlBlock.tsx +19 -0
- package/src/viewer/HtmlPlaceholder.tsx +29 -0
- package/src/viewer/MermaidBlock.tsx +126 -0
- package/src/viewer/SourceDiffPanel.tsx +305 -0
- package/src/viewer/TableOfContents.tsx +211 -0
- package/src/viewer/ViewerApp.tsx +257 -0
- package/src/viewer/diagramLinks.ts +74 -0
- package/src/viewer/diffsTheme.ts +4 -0
- package/src/viewer/main.tsx +17 -0
- package/src/viewer/pierre.ts +47 -0
- package/src/viewer/sourceNavigation.ts +104 -0
- package/src/viewer/styles.css +9 -0
- package/src/viewer/viewerMode.ts +9 -0
- package/src/viewer/vite-env.d.ts +4 -0
- package/vite.config.ts +41 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { CodeView, type CodeViewHandle } from "@pierre/diffs/react";
|
|
3
|
+
import type {
|
|
4
|
+
CodeViewDiffItem,
|
|
5
|
+
CodeViewOptions,
|
|
6
|
+
CodeViewLineSelection,
|
|
7
|
+
} from "@pierre/diffs";
|
|
8
|
+
import {
|
|
9
|
+
backgroundColor,
|
|
10
|
+
border,
|
|
11
|
+
borderColor,
|
|
12
|
+
Button,
|
|
13
|
+
flex,
|
|
14
|
+
spacing,
|
|
15
|
+
text,
|
|
16
|
+
useTheme,
|
|
17
|
+
} from "maui";
|
|
18
|
+
import { style, useStyles } from "purse-styles";
|
|
19
|
+
import {
|
|
20
|
+
referenceLabel,
|
|
21
|
+
type SourceAnnotation,
|
|
22
|
+
type SourceReference,
|
|
23
|
+
} from "../annotations.js";
|
|
24
|
+
import type { SourceDefinition } from "../definitions.js";
|
|
25
|
+
import {
|
|
26
|
+
matchingSourceDiff,
|
|
27
|
+
requestSource,
|
|
28
|
+
useSourceReference,
|
|
29
|
+
} from "./sourceNavigation.js";
|
|
30
|
+
import { pierreDiffOptions, sourceSelectionCss } from "./pierre.js";
|
|
31
|
+
import { useViewerMode } from "./viewerMode.ts";
|
|
32
|
+
|
|
33
|
+
export type SourceSelection = {
|
|
34
|
+
annotation: SourceAnnotation;
|
|
35
|
+
reference: SourceReference;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type DefinitionNavigation = {
|
|
39
|
+
selection: SourceSelection | undefined;
|
|
40
|
+
history: SourceDefinition[];
|
|
41
|
+
status: string | undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function SourceDiffPanel(props: {
|
|
45
|
+
items: CodeViewDiffItem[];
|
|
46
|
+
selection: SourceSelection | undefined;
|
|
47
|
+
onSelect: (selection: SourceSelection) => void;
|
|
48
|
+
}) {
|
|
49
|
+
const { resolvedTheme } = useTheme();
|
|
50
|
+
const mode = useViewerMode();
|
|
51
|
+
const viewer = useRef<CodeViewHandle<undefined, undefined>>(null);
|
|
52
|
+
const [navigation, setNavigation] = useState<DefinitionNavigation>({
|
|
53
|
+
selection: props.selection,
|
|
54
|
+
history: [],
|
|
55
|
+
status: undefined,
|
|
56
|
+
});
|
|
57
|
+
if (navigation.selection !== props.selection) {
|
|
58
|
+
setNavigation({
|
|
59
|
+
selection: props.selection,
|
|
60
|
+
history: [],
|
|
61
|
+
status: undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const { history } = navigation;
|
|
65
|
+
const request = useRef(0);
|
|
66
|
+
const definition = history.at(-1);
|
|
67
|
+
const panel = useStyles(styles.panel);
|
|
68
|
+
const header = useStyles(styles.header);
|
|
69
|
+
const links = useStyles(styles.links);
|
|
70
|
+
const code = useStyles(styles.code);
|
|
71
|
+
const options = pierreDiffOptions({
|
|
72
|
+
themeType: resolvedTheme,
|
|
73
|
+
disableFileHeader: false,
|
|
74
|
+
});
|
|
75
|
+
const selection = props.selection;
|
|
76
|
+
const source = useSourceReference(selection?.reference);
|
|
77
|
+
const resolvedFile = source instanceof Error ? undefined : source;
|
|
78
|
+
const linkedDiff = useMemo(
|
|
79
|
+
() =>
|
|
80
|
+
resolvedFile === undefined
|
|
81
|
+
? undefined
|
|
82
|
+
: matchingSourceDiff(resolvedFile, props.items),
|
|
83
|
+
[resolvedFile, props.items],
|
|
84
|
+
);
|
|
85
|
+
const file =
|
|
86
|
+
definition === undefined
|
|
87
|
+
? linkedDiff === undefined
|
|
88
|
+
? resolvedFile
|
|
89
|
+
: undefined
|
|
90
|
+
: definition;
|
|
91
|
+
const status =
|
|
92
|
+
navigation.status !== undefined
|
|
93
|
+
? navigation.status
|
|
94
|
+
: source instanceof Error
|
|
95
|
+
? source.message
|
|
96
|
+
: selection?.reference.kind === "file" && source === undefined
|
|
97
|
+
? "Opening source…"
|
|
98
|
+
: undefined;
|
|
99
|
+
const selectedLines = useMemo<CodeViewLineSelection | null>(() => {
|
|
100
|
+
if (file !== undefined)
|
|
101
|
+
return { id: file.path, range: { start: file.start, end: file.end } };
|
|
102
|
+
if (linkedDiff !== undefined) return linkedDiff;
|
|
103
|
+
const ref = selection?.reference;
|
|
104
|
+
// oxlint-disable-next-line unicorn/no-null -- Pierre uses null for a controlled empty selection.
|
|
105
|
+
if (ref === undefined || ref.kind === "file") return null;
|
|
106
|
+
return {
|
|
107
|
+
id: ref.id,
|
|
108
|
+
range: {
|
|
109
|
+
start: ref.start,
|
|
110
|
+
end: ref.end,
|
|
111
|
+
side: ref.side === "old" ? "deletions" : "additions",
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}, [selection, file, linkedDiff]);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (selectedLines === null) return;
|
|
118
|
+
viewer.current?.scrollTo({
|
|
119
|
+
type: "range",
|
|
120
|
+
...selectedLines,
|
|
121
|
+
align: "center",
|
|
122
|
+
behavior: "instant",
|
|
123
|
+
});
|
|
124
|
+
}, [selectedLines]);
|
|
125
|
+
|
|
126
|
+
const onTokenClick: CodeViewOptions<undefined, undefined>["onTokenClick"] = (
|
|
127
|
+
token,
|
|
128
|
+
event,
|
|
129
|
+
context,
|
|
130
|
+
) => {
|
|
131
|
+
if (mode === "gist") return;
|
|
132
|
+
if (!event.metaKey && !event.ctrlKey) return;
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
const item = context.item;
|
|
135
|
+
let filePath: string;
|
|
136
|
+
let lineText: string;
|
|
137
|
+
if (item.type === "file") {
|
|
138
|
+
filePath = item.file.name;
|
|
139
|
+
lineText = item.file.contents.split(/\r?\n/)[token.lineNumber - 1]!;
|
|
140
|
+
} else {
|
|
141
|
+
const side =
|
|
142
|
+
"side" in token && token.side === "deletions" ? "deletion" : "addition";
|
|
143
|
+
const diff = item.fileDiff;
|
|
144
|
+
filePath = diff.name;
|
|
145
|
+
const hunk = diff.hunks.find(
|
|
146
|
+
(h) =>
|
|
147
|
+
token.lineNumber >= h[`${side}Start`] &&
|
|
148
|
+
token.lineNumber < h[`${side}Start`] + h[`${side}Count`],
|
|
149
|
+
)!;
|
|
150
|
+
const index = diff.isPartial
|
|
151
|
+
? hunk[`${side}LineIndex`] + token.lineNumber - hunk[`${side}Start`]
|
|
152
|
+
: token.lineNumber - 1;
|
|
153
|
+
lineText = diff[`${side}Lines`][index]!.replace(/\r?\n$/, "");
|
|
154
|
+
}
|
|
155
|
+
const params = new URLSearchParams({
|
|
156
|
+
path: filePath,
|
|
157
|
+
line: String(token.lineNumber),
|
|
158
|
+
column: String(token.lineCharStart),
|
|
159
|
+
text: lineText,
|
|
160
|
+
});
|
|
161
|
+
const currentRequest = ++request.current;
|
|
162
|
+
setNavigation({ selection, history, status: "Finding definition…" });
|
|
163
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- Token callbacks cannot await; the request owns its status update.
|
|
164
|
+
void requestSource("definition", params).then((result) => {
|
|
165
|
+
if (currentRequest !== request.current) return;
|
|
166
|
+
setNavigation((current) => {
|
|
167
|
+
if (current.selection !== selection) return current;
|
|
168
|
+
if (result instanceof Error)
|
|
169
|
+
return { ...current, status: result.message };
|
|
170
|
+
return {
|
|
171
|
+
selection,
|
|
172
|
+
history: [...history, result],
|
|
173
|
+
status: undefined,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<aside id="source-diff-panel" aria-label="Source changes" className={panel}>
|
|
181
|
+
<div className={header}>
|
|
182
|
+
<strong>
|
|
183
|
+
{definition !== undefined
|
|
184
|
+
? "Symbol definition"
|
|
185
|
+
: file !== undefined
|
|
186
|
+
? "Source file"
|
|
187
|
+
: "Source changes"}
|
|
188
|
+
</strong>
|
|
189
|
+
{definition !== undefined && (
|
|
190
|
+
<Button
|
|
191
|
+
variant="quiet"
|
|
192
|
+
onClick={() => {
|
|
193
|
+
request.current++;
|
|
194
|
+
setNavigation({
|
|
195
|
+
selection,
|
|
196
|
+
history: history.slice(0, -1),
|
|
197
|
+
status: undefined,
|
|
198
|
+
});
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{history.length === 1 ? "Back to reference" : "Back"}
|
|
202
|
+
</Button>
|
|
203
|
+
)}
|
|
204
|
+
{mode === "local" && (
|
|
205
|
+
<span>⌘-click or Ctrl-click a symbol to go to its definition.</span>
|
|
206
|
+
)}
|
|
207
|
+
{status !== undefined && <span role="status">{status}</span>}
|
|
208
|
+
{selection === undefined ? (
|
|
209
|
+
<span>
|
|
210
|
+
Select a linked stack row or diagram element to open its source.
|
|
211
|
+
</span>
|
|
212
|
+
) : (
|
|
213
|
+
<>
|
|
214
|
+
<span>
|
|
215
|
+
{selection.annotation.text.replace(/^[+ -]/, "").trim()}
|
|
216
|
+
</span>
|
|
217
|
+
{selection.annotation.references.length > 1 && (
|
|
218
|
+
<div className={links} aria-label="Linked changes">
|
|
219
|
+
{selection.annotation.references.map((reference, index) => (
|
|
220
|
+
<Button
|
|
221
|
+
key={index}
|
|
222
|
+
variantColor="blue"
|
|
223
|
+
variant={
|
|
224
|
+
reference === selection.reference ? "primary" : "quiet"
|
|
225
|
+
}
|
|
226
|
+
onClick={() =>
|
|
227
|
+
props.onSelect({
|
|
228
|
+
annotation: selection.annotation,
|
|
229
|
+
reference,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
>
|
|
233
|
+
{referenceLabel(reference)}
|
|
234
|
+
</Button>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
<CodeView
|
|
242
|
+
ref={viewer}
|
|
243
|
+
className={code}
|
|
244
|
+
items={
|
|
245
|
+
file === undefined
|
|
246
|
+
? props.items
|
|
247
|
+
: [
|
|
248
|
+
{
|
|
249
|
+
type: "file",
|
|
250
|
+
id: file.path,
|
|
251
|
+
file: {
|
|
252
|
+
name: file.path,
|
|
253
|
+
contents: file.contents,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
]
|
|
257
|
+
}
|
|
258
|
+
selectedLines={selectedLines}
|
|
259
|
+
disableWorkerPool
|
|
260
|
+
options={{
|
|
261
|
+
...options,
|
|
262
|
+
unsafeCSS: options.unsafeCSS + sourceSelectionCss,
|
|
263
|
+
stickyHeaders: true,
|
|
264
|
+
onTokenClick,
|
|
265
|
+
}}
|
|
266
|
+
/>
|
|
267
|
+
</aside>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const styles = {
|
|
272
|
+
panel: style(flex({ direction: "column" }), border(["left"], "border"), {
|
|
273
|
+
gridColumn: "3",
|
|
274
|
+
minWidth: 0,
|
|
275
|
+
minHeight: 0,
|
|
276
|
+
overflow: "hidden",
|
|
277
|
+
backgroundColor: backgroundColor.app,
|
|
278
|
+
"@media (max-width: 900px)": {
|
|
279
|
+
gridColumn: "1",
|
|
280
|
+
height: "45vh",
|
|
281
|
+
borderLeft: "none",
|
|
282
|
+
borderTop: `1px solid ${borderColor.border}`,
|
|
283
|
+
},
|
|
284
|
+
}),
|
|
285
|
+
header: style(
|
|
286
|
+
flex({ direction: "column" }),
|
|
287
|
+
spacing.padding({ all: 4 }),
|
|
288
|
+
border(["bottom"], "border"),
|
|
289
|
+
text({ size: "sm", color: "lowContrast" }),
|
|
290
|
+
{
|
|
291
|
+
gap: "6px",
|
|
292
|
+
overflowWrap: "anywhere",
|
|
293
|
+
maxHeight: "35%",
|
|
294
|
+
overflowY: "auto",
|
|
295
|
+
flexShrink: 0,
|
|
296
|
+
},
|
|
297
|
+
),
|
|
298
|
+
links: style(flex({ direction: "row" }), { gap: "6px", flexWrap: "wrap" }),
|
|
299
|
+
code: style({
|
|
300
|
+
flex: "1 1 auto",
|
|
301
|
+
minWidth: 0,
|
|
302
|
+
minHeight: 0,
|
|
303
|
+
overflow: "auto",
|
|
304
|
+
}),
|
|
305
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { useEffect, useState, type RefObject } from "react";
|
|
2
|
+
import { colors, focusRing, radius, spacing, text } from "maui";
|
|
3
|
+
import { style, useStyles } from "purse-styles";
|
|
4
|
+
import type { ViewerHeading } from "../parseViewer.js";
|
|
5
|
+
|
|
6
|
+
type TocItem = ViewerHeading & { children: TocItem[] };
|
|
7
|
+
|
|
8
|
+
export function TableOfContents(props: {
|
|
9
|
+
headings: ViewerHeading[];
|
|
10
|
+
articleRef: RefObject<HTMLElement | null>;
|
|
11
|
+
}) {
|
|
12
|
+
const items = nestHeadings(tocHeadings(props.headings));
|
|
13
|
+
const { activeId, setActiveId } = useActiveHeading(
|
|
14
|
+
props.headings,
|
|
15
|
+
props.articleRef,
|
|
16
|
+
);
|
|
17
|
+
const navClass = useStyles(styles.nav);
|
|
18
|
+
const listClass = useStyles(styles.list);
|
|
19
|
+
if (items.length === 0) return undefined;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<nav
|
|
23
|
+
className={navClass}
|
|
24
|
+
aria-label="Table of contents"
|
|
25
|
+
data-diffmap-kind="toc"
|
|
26
|
+
>
|
|
27
|
+
<TocList
|
|
28
|
+
items={items}
|
|
29
|
+
className={listClass}
|
|
30
|
+
activeId={activeId}
|
|
31
|
+
onSelect={setActiveId}
|
|
32
|
+
/>
|
|
33
|
+
</nav>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function TocList(props: {
|
|
38
|
+
items: TocItem[];
|
|
39
|
+
className: string;
|
|
40
|
+
activeId: string | undefined;
|
|
41
|
+
onSelect: (id: string) => void;
|
|
42
|
+
}) {
|
|
43
|
+
return (
|
|
44
|
+
<ol className={props.className}>
|
|
45
|
+
{props.items.map((item) => (
|
|
46
|
+
<TocEntry
|
|
47
|
+
key={item.id}
|
|
48
|
+
item={item}
|
|
49
|
+
listClass={props.className}
|
|
50
|
+
activeId={props.activeId}
|
|
51
|
+
onSelect={props.onSelect}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
</ol>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function TocEntry(props: {
|
|
59
|
+
item: TocItem;
|
|
60
|
+
listClass: string;
|
|
61
|
+
activeId: string | undefined;
|
|
62
|
+
onSelect: (id: string) => void;
|
|
63
|
+
}) {
|
|
64
|
+
const itemClass = useStyles(styles.item);
|
|
65
|
+
const linkClass = useStyles(styles.link);
|
|
66
|
+
const current = props.activeId === props.item.id;
|
|
67
|
+
return (
|
|
68
|
+
<li className={itemClass}>
|
|
69
|
+
<a
|
|
70
|
+
className={linkClass}
|
|
71
|
+
href={`#${props.item.id}`}
|
|
72
|
+
aria-current={current ? "location" : undefined}
|
|
73
|
+
onClick={(event) => {
|
|
74
|
+
const heading = document.getElementById(props.item.id);
|
|
75
|
+
if (!heading) return;
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
props.onSelect(props.item.id);
|
|
78
|
+
heading.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
79
|
+
history.pushState({}, "", `#${props.item.id}`);
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{props.item.text}
|
|
83
|
+
</a>
|
|
84
|
+
{props.item.children.length > 0 && (
|
|
85
|
+
<TocList
|
|
86
|
+
items={props.item.children}
|
|
87
|
+
className={props.listClass}
|
|
88
|
+
activeId={props.activeId}
|
|
89
|
+
onSelect={props.onSelect}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
</li>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function tocHeadings(headings: ViewerHeading[]) {
|
|
97
|
+
const first = headings[0];
|
|
98
|
+
if (first?.level === 1) return headings.slice(1);
|
|
99
|
+
return headings;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function nestHeadings(headings: ViewerHeading[]): TocItem[] {
|
|
103
|
+
const items: TocItem[] = [];
|
|
104
|
+
const stack: TocItem[] = [];
|
|
105
|
+
for (const heading of headings) {
|
|
106
|
+
const item: TocItem = { ...heading, children: [] };
|
|
107
|
+
while (stack.length > 0) {
|
|
108
|
+
const parent = stack[stack.length - 1];
|
|
109
|
+
if (parent === undefined || parent.level < heading.level) break;
|
|
110
|
+
stack.pop();
|
|
111
|
+
}
|
|
112
|
+
const parent = stack[stack.length - 1];
|
|
113
|
+
if (parent === undefined) items.push(item);
|
|
114
|
+
else parent.children.push(item);
|
|
115
|
+
stack.push(item);
|
|
116
|
+
}
|
|
117
|
+
return items;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function useActiveHeading(
|
|
121
|
+
headings: ViewerHeading[],
|
|
122
|
+
articleRef: RefObject<HTMLElement | null>,
|
|
123
|
+
) {
|
|
124
|
+
const [activeId, setActiveId] = useState<string>();
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const ids = tocHeadings(headings).map((heading) => heading.id);
|
|
128
|
+
const nodes = ids.flatMap((id) => {
|
|
129
|
+
const node = document.getElementById(id);
|
|
130
|
+
return node ? [node] : [];
|
|
131
|
+
});
|
|
132
|
+
if (nodes.length === 0) return;
|
|
133
|
+
const root = articleRef.current ?? undefined;
|
|
134
|
+
const visible = new Set<string>();
|
|
135
|
+
const observer = new IntersectionObserver(
|
|
136
|
+
(entries) => {
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.isIntersecting) visible.add(entry.target.id);
|
|
139
|
+
else visible.delete(entry.target.id);
|
|
140
|
+
}
|
|
141
|
+
const next = ids.find((id) => visible.has(id));
|
|
142
|
+
if (next !== undefined) setActiveId(next);
|
|
143
|
+
},
|
|
144
|
+
{ root, rootMargin: "0px 0px -70% 0px", threshold: 0 },
|
|
145
|
+
);
|
|
146
|
+
for (const node of nodes) observer.observe(node);
|
|
147
|
+
return () => observer.disconnect();
|
|
148
|
+
}, [articleRef, headings]);
|
|
149
|
+
|
|
150
|
+
return { activeId, setActiveId };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const styles = {
|
|
154
|
+
nav: style(spacing.padding({ left: 16, right: 6, top: 8, bottom: 8 }), {
|
|
155
|
+
boxSizing: "border-box",
|
|
156
|
+
gridColumn: "1",
|
|
157
|
+
alignSelf: "stretch",
|
|
158
|
+
minWidth: "240px",
|
|
159
|
+
width: "max-content",
|
|
160
|
+
maxWidth: "280px",
|
|
161
|
+
minHeight: 0,
|
|
162
|
+
height: "fit-content",
|
|
163
|
+
maxHeight: "100%",
|
|
164
|
+
marginTop: "auto",
|
|
165
|
+
marginBottom: "auto",
|
|
166
|
+
overflowY: "auto",
|
|
167
|
+
"@media (max-width: 1100px)": {
|
|
168
|
+
display: "none",
|
|
169
|
+
},
|
|
170
|
+
"&[data-diffmap-kind='toc'] ol": {
|
|
171
|
+
listStyle: "none",
|
|
172
|
+
counterReset: "none",
|
|
173
|
+
margin: 0,
|
|
174
|
+
padding: 0,
|
|
175
|
+
},
|
|
176
|
+
"&[data-diffmap-kind='toc'] ol ol": {
|
|
177
|
+
paddingInlineStart: spacing.value(6),
|
|
178
|
+
},
|
|
179
|
+
"&[data-diffmap-kind='toc'] ol > li::before": {
|
|
180
|
+
content: "none",
|
|
181
|
+
},
|
|
182
|
+
"&[data-diffmap-kind='toc'] a": {
|
|
183
|
+
fontWeight: 400,
|
|
184
|
+
textDecoration: "none",
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
list: style(text({ size: "sm", fontWeight: 400, color: "lowContrast" })),
|
|
188
|
+
item: style({
|
|
189
|
+
"&::before": {
|
|
190
|
+
content: "none",
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
link: style(
|
|
194
|
+
radius.sm,
|
|
195
|
+
spacing.padding({ x: 4, y: 2 }),
|
|
196
|
+
text({ size: "sm", fontWeight: 400, color: "lowContrast" }),
|
|
197
|
+
focusRing(),
|
|
198
|
+
{
|
|
199
|
+
display: "block",
|
|
200
|
+
overflowWrap: "anywhere",
|
|
201
|
+
cursor: "pointer",
|
|
202
|
+
"&:hover": {
|
|
203
|
+
backgroundColor: colors.gray[3],
|
|
204
|
+
},
|
|
205
|
+
"&[aria-current='location']": {
|
|
206
|
+
color: colors.accent[9],
|
|
207
|
+
fontWeight: 500,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
),
|
|
211
|
+
};
|