@tanishqkancharla/diffmap 0.1.3 → 0.1.5
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 +25 -14
- package/package.json +3 -3
- package/src/cli.test.ts +50 -0
- package/src/cli.ts +126 -51
- package/src/contentPlugin.ts +5 -3
- package/src/errors.ts +6 -1
- package/src/gist/GistApp.tsx +34 -21
- package/src/gist/GistStatus.tsx +8 -43
- package/src/gist/main.tsx +2 -2
- package/src/gist/pin.ts +14 -0
- package/src/github/GitHubApp.tsx +235 -0
- package/src/github/fetchRepo.test.ts +17 -0
- package/src/github/fetchRepo.ts +300 -0
- package/src/github/patch.test.ts +30 -0
- package/src/github/patch.ts +40 -0
- package/src/github/pin.ts +14 -0
- package/src/github/route.test.ts +68 -0
- package/src/github/route.ts +175 -0
- package/src/host/HostApp.tsx +22 -0
- package/src/host/HostHome.tsx +73 -0
- package/src/host/files.ts +10 -0
- package/src/host/home.md +194 -0
- package/src/host/home.test.ts +82 -0
- package/src/index.ts +1 -0
- package/src/mermaid.ts +29 -1
- package/src/packageVersion.ts +7 -0
- package/src/parseViewer.test.ts +185 -0
- package/src/parseViewer.ts +27 -3
- package/src/serve.test.ts +306 -0
- package/src/serve.ts +23 -5
- package/src/share.ts +5 -1
- package/src/update.test.ts +19 -0
- package/src/update.ts +285 -0
- package/src/viewer/CallStackDiff.tsx +26 -6
- package/src/viewer/CloseServerButton.tsx +16 -0
- package/src/viewer/FileExcerpt.tsx +52 -3
- package/src/viewer/HtmlPlaceholder.tsx +6 -1
- package/src/viewer/SourceDiffPanel.tsx +155 -59
- package/src/viewer/TableOfContents.tsx +230 -39
- package/src/viewer/TocButton.tsx +27 -0
- package/src/viewer/ViewerApp.tsx +182 -47
- package/src/viewer/main.tsx +10 -3
- package/src/viewer/sourceHeader.test.ts +131 -0
- package/src/viewer/sourceHeader.ts +103 -0
- package/src/viewer/sourceNavigation.ts +81 -3
- package/src/viewer/viewerMode.ts +1 -1
- package/src/viewer/vite-env.d.ts +2 -1
- package/vite.config.ts +1 -1
- package/src/viewer/Check.tsx +0 -24
- package/src/viewer/DoneButton.tsx +0 -32
|
@@ -22,12 +22,20 @@ import {
|
|
|
22
22
|
type SourceReference,
|
|
23
23
|
} from "../annotations.js";
|
|
24
24
|
import type { SourceDefinition } from "../definitions.js";
|
|
25
|
+
import { useGistPin } from "../gist/pin.ts";
|
|
26
|
+
import { useGitHubPin } from "../github/pin.ts";
|
|
27
|
+
import { useHostFiles } from "../host/files.ts";
|
|
25
28
|
import {
|
|
26
29
|
matchingSourceDiff,
|
|
27
30
|
requestSource,
|
|
28
31
|
useSourceReference,
|
|
29
32
|
} from "./sourceNavigation.js";
|
|
30
33
|
import { pierreDiffOptions, sourceSelectionCss } from "./pierre.js";
|
|
34
|
+
import {
|
|
35
|
+
sourcePanelOrigin,
|
|
36
|
+
sourcePanelPath,
|
|
37
|
+
sourcePanelPin,
|
|
38
|
+
} from "./sourceHeader.ts";
|
|
31
39
|
import { useViewerMode } from "./viewerMode.ts";
|
|
32
40
|
|
|
33
41
|
export type SourceSelection = {
|
|
@@ -66,12 +74,14 @@ export function SourceDiffPanel(props: {
|
|
|
66
74
|
const definition = history.at(-1);
|
|
67
75
|
const panel = useStyles(styles.panel);
|
|
68
76
|
const header = useStyles(styles.header);
|
|
77
|
+
const titleRow = useStyles(styles.titleRow);
|
|
78
|
+
const titleClass = useStyles(styles.title);
|
|
79
|
+
const pinClass = useStyles(styles.pin);
|
|
69
80
|
const links = useStyles(styles.links);
|
|
70
81
|
const code = useStyles(styles.code);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
82
|
+
const github = useGitHubPin();
|
|
83
|
+
const gist = useGistPin();
|
|
84
|
+
const hostFiles = useHostFiles();
|
|
75
85
|
const selection = props.selection;
|
|
76
86
|
const source = useSourceReference(selection?.reference);
|
|
77
87
|
const resolvedFile = source instanceof Error ? undefined : source;
|
|
@@ -93,7 +103,9 @@ export function SourceDiffPanel(props: {
|
|
|
93
103
|
? navigation.status
|
|
94
104
|
: source instanceof Error
|
|
95
105
|
? source.message
|
|
96
|
-
: selection?.reference.kind === "file" &&
|
|
106
|
+
: selection?.reference.kind === "file" &&
|
|
107
|
+
source === undefined &&
|
|
108
|
+
(mode === "local" || mode === "github" || hostFiles !== undefined)
|
|
97
109
|
? "Opening source…"
|
|
98
110
|
: undefined;
|
|
99
111
|
const selectedLines = useMemo<CodeViewLineSelection | null>(() => {
|
|
@@ -112,23 +124,67 @@ export function SourceDiffPanel(props: {
|
|
|
112
124
|
},
|
|
113
125
|
};
|
|
114
126
|
}, [selection, file, linkedDiff]);
|
|
127
|
+
const rangeId = selectedLines?.id;
|
|
128
|
+
const rangeStart = selectedLines?.range.start;
|
|
129
|
+
const rangeEnd = selectedLines?.range.end;
|
|
130
|
+
const rangeSide =
|
|
131
|
+
selectedLines !== null && "side" in selectedLines.range
|
|
132
|
+
? selectedLines.range.side
|
|
133
|
+
: undefined;
|
|
134
|
+
|
|
135
|
+
const openPath = sourcePanelPath({
|
|
136
|
+
filePath: file?.path,
|
|
137
|
+
reference: selection?.reference,
|
|
138
|
+
items: props.items,
|
|
139
|
+
linkedDiffId: linkedDiff?.id,
|
|
140
|
+
});
|
|
141
|
+
const origin = sourcePanelOrigin({
|
|
142
|
+
showingFile: file !== undefined,
|
|
143
|
+
reference: selection?.reference,
|
|
144
|
+
items: props.items,
|
|
145
|
+
linkedDiffId: linkedDiff?.id,
|
|
146
|
+
});
|
|
147
|
+
const pin =
|
|
148
|
+
openPath === undefined
|
|
149
|
+
? { kind: "none" as const }
|
|
150
|
+
: sourcePanelPin({
|
|
151
|
+
origin,
|
|
152
|
+
path: openPath,
|
|
153
|
+
github,
|
|
154
|
+
gist,
|
|
155
|
+
});
|
|
156
|
+
const options = pierreDiffOptions({
|
|
157
|
+
themeType: resolvedTheme,
|
|
158
|
+
disableFileHeader: openPath !== undefined,
|
|
159
|
+
});
|
|
115
160
|
|
|
116
161
|
useEffect(() => {
|
|
117
|
-
if (
|
|
162
|
+
if (
|
|
163
|
+
rangeId === undefined ||
|
|
164
|
+
rangeStart === undefined ||
|
|
165
|
+
rangeEnd === undefined
|
|
166
|
+
) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
118
169
|
viewer.current?.scrollTo({
|
|
119
170
|
type: "range",
|
|
120
|
-
|
|
171
|
+
id: rangeId,
|
|
172
|
+
range: {
|
|
173
|
+
start: rangeStart,
|
|
174
|
+
end: rangeEnd,
|
|
175
|
+
...(rangeSide === undefined ? {} : { side: rangeSide }),
|
|
176
|
+
},
|
|
121
177
|
align: "center",
|
|
122
178
|
behavior: "instant",
|
|
123
179
|
});
|
|
124
|
-
}, [
|
|
180
|
+
}, [rangeId, rangeStart, rangeEnd, rangeSide]);
|
|
125
181
|
|
|
126
182
|
const onTokenClick: CodeViewOptions<undefined, undefined>["onTokenClick"] = (
|
|
127
183
|
token,
|
|
128
184
|
event,
|
|
129
185
|
context,
|
|
130
186
|
) => {
|
|
131
|
-
if (mode
|
|
187
|
+
if (mode !== "local") return;
|
|
132
188
|
if (!event.metaKey && !event.ctrlKey) return;
|
|
133
189
|
event.preventDefault();
|
|
134
190
|
const item = context.item;
|
|
@@ -178,28 +234,53 @@ export function SourceDiffPanel(props: {
|
|
|
178
234
|
|
|
179
235
|
return (
|
|
180
236
|
<aside id="source-diff-panel" aria-label="Source changes" className={panel}>
|
|
181
|
-
<div className={header}>
|
|
182
|
-
|
|
183
|
-
{
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
237
|
+
<div className={header} data-diffmap-kind="source-header">
|
|
238
|
+
{openPath !== undefined && (
|
|
239
|
+
<div className={titleRow}>
|
|
240
|
+
<div className={titleClass} title={openPath}>
|
|
241
|
+
{openPath}
|
|
242
|
+
</div>
|
|
243
|
+
{definition !== undefined && (
|
|
244
|
+
<Button
|
|
245
|
+
variant="quiet"
|
|
246
|
+
onClick={() => {
|
|
247
|
+
request.current++;
|
|
248
|
+
setNavigation({
|
|
249
|
+
selection,
|
|
250
|
+
history: history.slice(0, -1),
|
|
251
|
+
status: undefined,
|
|
252
|
+
});
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
{history.length === 1 ? "Back to reference" : "Back"}
|
|
256
|
+
</Button>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
{pin.kind === "github" && (
|
|
261
|
+
<a
|
|
262
|
+
className={pinClass}
|
|
263
|
+
href={pin.href}
|
|
264
|
+
data-diffmap-pin="github"
|
|
265
|
+
title={pin.label}
|
|
200
266
|
>
|
|
201
|
-
{
|
|
202
|
-
</
|
|
267
|
+
{pin.label}
|
|
268
|
+
</a>
|
|
269
|
+
)}
|
|
270
|
+
{pin.kind === "gist" && (
|
|
271
|
+
<a
|
|
272
|
+
className={pinClass}
|
|
273
|
+
href={pin.href}
|
|
274
|
+
data-diffmap-pin="gist"
|
|
275
|
+
title={pin.label}
|
|
276
|
+
>
|
|
277
|
+
{pin.label}
|
|
278
|
+
</a>
|
|
279
|
+
)}
|
|
280
|
+
{pin.kind === "spec" && (
|
|
281
|
+
<span className={pinClass} data-diffmap-pin="spec">
|
|
282
|
+
{pin.label}
|
|
283
|
+
</span>
|
|
203
284
|
)}
|
|
204
285
|
{mode === "local" && (
|
|
205
286
|
<span>⌘-click or Ctrl-click a symbol to go to its definition.</span>
|
|
@@ -210,32 +291,27 @@ export function SourceDiffPanel(props: {
|
|
|
210
291
|
Select a linked stack row or diagram element to open its source.
|
|
211
292
|
</span>
|
|
212
293
|
) : (
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
{selection.annotation.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</Button>
|
|
235
|
-
))}
|
|
236
|
-
</div>
|
|
237
|
-
)}
|
|
238
|
-
</>
|
|
294
|
+
selection.annotation.references.length > 1 && (
|
|
295
|
+
<div className={links} aria-label="Linked changes">
|
|
296
|
+
{selection.annotation.references.map((reference, index) => (
|
|
297
|
+
<Button
|
|
298
|
+
key={index}
|
|
299
|
+
variantColor="blue"
|
|
300
|
+
variant={
|
|
301
|
+
reference === selection.reference ? "primary" : "quiet"
|
|
302
|
+
}
|
|
303
|
+
onClick={() =>
|
|
304
|
+
props.onSelect({
|
|
305
|
+
annotation: selection.annotation,
|
|
306
|
+
reference,
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
>
|
|
310
|
+
{referenceLabel(reference)}
|
|
311
|
+
</Button>
|
|
312
|
+
))}
|
|
313
|
+
</div>
|
|
314
|
+
)
|
|
239
315
|
)}
|
|
240
316
|
</div>
|
|
241
317
|
<CodeView
|
|
@@ -270,13 +346,12 @@ export function SourceDiffPanel(props: {
|
|
|
270
346
|
|
|
271
347
|
const styles = {
|
|
272
348
|
panel: style(flex({ direction: "column" }), border(["left"], "border"), {
|
|
273
|
-
|
|
349
|
+
gridArea: "diff",
|
|
274
350
|
minWidth: 0,
|
|
275
351
|
minHeight: 0,
|
|
276
352
|
overflow: "hidden",
|
|
277
353
|
backgroundColor: backgroundColor.app,
|
|
278
354
|
"@media (max-width: 900px)": {
|
|
279
|
-
gridColumn: "1",
|
|
280
355
|
height: "45vh",
|
|
281
356
|
borderLeft: "none",
|
|
282
357
|
borderTop: `1px solid ${borderColor.border}`,
|
|
@@ -289,12 +364,33 @@ const styles = {
|
|
|
289
364
|
text({ size: "sm", color: "lowContrast" }),
|
|
290
365
|
{
|
|
291
366
|
gap: "6px",
|
|
292
|
-
|
|
367
|
+
minWidth: 0,
|
|
293
368
|
maxHeight: "35%",
|
|
294
369
|
overflowY: "auto",
|
|
295
370
|
flexShrink: 0,
|
|
296
371
|
},
|
|
297
372
|
),
|
|
373
|
+
titleRow: style(flex({ direction: "row", alignItems: "center" }), {
|
|
374
|
+
gap: "6px",
|
|
375
|
+
minWidth: 0,
|
|
376
|
+
}),
|
|
377
|
+
title: style(text({ size: "sm", fontWeight: 600, color: "highContrast" }), {
|
|
378
|
+
minWidth: 0,
|
|
379
|
+
flex: "1 1 auto",
|
|
380
|
+
overflow: "hidden",
|
|
381
|
+
textOverflow: "ellipsis",
|
|
382
|
+
whiteSpace: "nowrap",
|
|
383
|
+
}),
|
|
384
|
+
pin: style(text({ size: "sm", color: "lowContrast" }), {
|
|
385
|
+
minWidth: 0,
|
|
386
|
+
overflow: "hidden",
|
|
387
|
+
textOverflow: "ellipsis",
|
|
388
|
+
whiteSpace: "nowrap",
|
|
389
|
+
textDecoration: "none",
|
|
390
|
+
"&:is(a):hover": {
|
|
391
|
+
textDecoration: "underline",
|
|
392
|
+
},
|
|
393
|
+
}),
|
|
298
394
|
links: style(flex({ direction: "row" }), { gap: "6px", flexWrap: "wrap" }),
|
|
299
395
|
code: style({
|
|
300
396
|
flex: "1 1 auto",
|
|
@@ -1,35 +1,157 @@
|
|
|
1
|
-
import { useEffect, useState, type RefObject } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useRef, useState, type RefObject } from "react";
|
|
2
|
+
import {
|
|
3
|
+
background,
|
|
4
|
+
colors,
|
|
5
|
+
focusRing,
|
|
6
|
+
motionEasing,
|
|
7
|
+
radius,
|
|
8
|
+
shadow,
|
|
9
|
+
spacing,
|
|
10
|
+
text,
|
|
11
|
+
} from "maui";
|
|
3
12
|
import { style, useStyles } from "purse-styles";
|
|
4
13
|
import type { ViewerHeading } from "../parseViewer.js";
|
|
5
14
|
|
|
6
15
|
type TocItem = ViewerHeading & { children: TocItem[] };
|
|
7
16
|
|
|
17
|
+
const TOC_LINE_MAX_PX = 24;
|
|
18
|
+
const TOC_LINE_MIN_PX = 8;
|
|
19
|
+
const TOC_LINE_STEP_PX = 6;
|
|
20
|
+
const DWELL_MS = 50;
|
|
21
|
+
const LEAVE_MS = 100;
|
|
22
|
+
const TOC_TRANSITION_MS = 180;
|
|
23
|
+
const TOC_MOTION = `opacity ${String(TOC_TRANSITION_MS)}ms ${motionEasing}, transform ${String(TOC_TRANSITION_MS)}ms ${motionEasing}`;
|
|
24
|
+
|
|
25
|
+
/** Equal side gutters so the line rail fits and the article column stays centered. */
|
|
26
|
+
export const TOC_SIDE_MARGIN_PX = 48;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Same cutoff that used to open the wide overlay (1101px). Applied to the
|
|
30
|
+
* article column's remaining width, so a resize or an open Diff panel can
|
|
31
|
+
* drop below it even when the viewport used to qualify.
|
|
32
|
+
*/
|
|
33
|
+
export const TOC_OPEN_MIN_WIDTH_PX = 1101;
|
|
34
|
+
|
|
35
|
+
export function hasTableOfContents(headings: ViewerHeading[]) {
|
|
36
|
+
return tocHeadings(headings).length > 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function tocLineWidth(level: number, shallowestLevel: number) {
|
|
40
|
+
const depth = Math.max(0, level - shallowestLevel);
|
|
41
|
+
return Math.max(TOC_LINE_MIN_PX, TOC_LINE_MAX_PX - depth * TOC_LINE_STEP_PX);
|
|
42
|
+
}
|
|
43
|
+
|
|
8
44
|
export function TableOfContents(props: {
|
|
9
45
|
headings: ViewerHeading[];
|
|
10
46
|
articleRef: RefObject<HTMLElement | null>;
|
|
47
|
+
open: boolean;
|
|
48
|
+
onDwellOpen: () => void;
|
|
49
|
+
onDwellClose: () => void;
|
|
11
50
|
}) {
|
|
12
|
-
const
|
|
51
|
+
const headings = tocHeadings(props.headings);
|
|
52
|
+
const items = nestHeadings(headings);
|
|
13
53
|
const { activeId, setActiveId } = useActiveHeading(
|
|
14
54
|
props.headings,
|
|
15
55
|
props.articleRef,
|
|
16
56
|
);
|
|
17
|
-
const
|
|
57
|
+
const railClass = useStyles(styles.rail);
|
|
58
|
+
const clusterClass = useStyles(styles.cluster);
|
|
59
|
+
const lineClass = useStyles(styles.line);
|
|
60
|
+
const cardClass = useStyles(styles.card);
|
|
18
61
|
const listClass = useStyles(styles.list);
|
|
62
|
+
const hoveringRef = useRef(false);
|
|
63
|
+
const dwellTimer = useRef<number | undefined>(undefined);
|
|
64
|
+
const closeTimer = useRef<number | undefined>(undefined);
|
|
65
|
+
const onDwellOpen = props.onDwellOpen;
|
|
66
|
+
const onDwellClose = props.onDwellClose;
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const onPageLeave = () => {
|
|
70
|
+
// Leaving the window must close a movement-opened nav, never open one.
|
|
71
|
+
// A left-edge exit used to land in the dwell zone and spring the drawer.
|
|
72
|
+
hoveringRef.current = false;
|
|
73
|
+
window.clearTimeout(dwellTimer.current);
|
|
74
|
+
window.clearTimeout(closeTimer.current);
|
|
75
|
+
dwellTimer.current = undefined;
|
|
76
|
+
closeTimer.current = undefined;
|
|
77
|
+
onDwellClose();
|
|
78
|
+
};
|
|
79
|
+
document.documentElement.addEventListener("mouseleave", onPageLeave);
|
|
80
|
+
return () => {
|
|
81
|
+
document.documentElement.removeEventListener("mouseleave", onPageLeave);
|
|
82
|
+
window.clearTimeout(dwellTimer.current);
|
|
83
|
+
window.clearTimeout(closeTimer.current);
|
|
84
|
+
};
|
|
85
|
+
}, [onDwellClose]);
|
|
86
|
+
|
|
19
87
|
if (items.length === 0) return undefined;
|
|
20
88
|
|
|
89
|
+
const shallowest = headings.reduce(
|
|
90
|
+
(min, heading) => Math.min(min, heading.level),
|
|
91
|
+
6,
|
|
92
|
+
);
|
|
93
|
+
|
|
21
94
|
return (
|
|
22
95
|
<nav
|
|
23
|
-
|
|
96
|
+
id="diffmap-toc"
|
|
97
|
+
className={railClass}
|
|
24
98
|
aria-label="Table of contents"
|
|
25
99
|
data-diffmap-kind="toc"
|
|
100
|
+
data-open={props.open ? "true" : "false"}
|
|
26
101
|
>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
102
|
+
<div
|
|
103
|
+
className={clusterClass}
|
|
104
|
+
data-open={props.open ? "true" : "false"}
|
|
105
|
+
onPointerEnter={() => {
|
|
106
|
+
hoveringRef.current = true;
|
|
107
|
+
window.clearTimeout(closeTimer.current);
|
|
108
|
+
closeTimer.current = undefined;
|
|
109
|
+
if (props.open || dwellTimer.current !== undefined) return;
|
|
110
|
+
dwellTimer.current = window.setTimeout(() => {
|
|
111
|
+
dwellTimer.current = undefined;
|
|
112
|
+
if (!hoveringRef.current) return;
|
|
113
|
+
onDwellOpen();
|
|
114
|
+
}, DWELL_MS);
|
|
115
|
+
}}
|
|
116
|
+
onPointerLeave={() => {
|
|
117
|
+
hoveringRef.current = false;
|
|
118
|
+
window.clearTimeout(dwellTimer.current);
|
|
119
|
+
dwellTimer.current = undefined;
|
|
120
|
+
window.clearTimeout(closeTimer.current);
|
|
121
|
+
closeTimer.current = window.setTimeout(() => {
|
|
122
|
+
closeTimer.current = undefined;
|
|
123
|
+
if (hoveringRef.current) return;
|
|
124
|
+
onDwellClose();
|
|
125
|
+
}, LEAVE_MS);
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{headings.map((heading) => (
|
|
129
|
+
<span
|
|
130
|
+
key={heading.id}
|
|
131
|
+
className={lineClass}
|
|
132
|
+
data-diffmap-kind="toc-line"
|
|
133
|
+
data-level={heading.level}
|
|
134
|
+
data-current={activeId === heading.id ? "true" : "false"}
|
|
135
|
+
style={{ width: tocLineWidth(heading.level, shallowest) }}
|
|
136
|
+
/>
|
|
137
|
+
))}
|
|
138
|
+
<div
|
|
139
|
+
className={cardClass}
|
|
140
|
+
data-diffmap-kind="toc-card"
|
|
141
|
+
data-open={props.open ? "true" : "false"}
|
|
142
|
+
aria-hidden={!props.open}
|
|
143
|
+
inert={!props.open}
|
|
144
|
+
>
|
|
145
|
+
<TocList
|
|
146
|
+
items={items}
|
|
147
|
+
className={listClass}
|
|
148
|
+
activeId={activeId}
|
|
149
|
+
onSelect={(id) => {
|
|
150
|
+
setActiveId(id);
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
33
155
|
</nav>
|
|
34
156
|
);
|
|
35
157
|
}
|
|
@@ -150,40 +272,105 @@ function useActiveHeading(
|
|
|
150
272
|
return { activeId, setActiveId };
|
|
151
273
|
}
|
|
152
274
|
|
|
275
|
+
const tocListRules = {
|
|
276
|
+
"&[data-diffmap-kind='toc-card'] ol": {
|
|
277
|
+
listStyle: "none",
|
|
278
|
+
counterReset: "none",
|
|
279
|
+
margin: 0,
|
|
280
|
+
padding: 0,
|
|
281
|
+
},
|
|
282
|
+
"&[data-diffmap-kind='toc-card'] ol ol": {
|
|
283
|
+
paddingInlineStart: spacing.value(6),
|
|
284
|
+
},
|
|
285
|
+
"&[data-diffmap-kind='toc-card'] ol > li::before": {
|
|
286
|
+
content: "none",
|
|
287
|
+
},
|
|
288
|
+
"&[data-diffmap-kind='toc-card'] a": {
|
|
289
|
+
fontWeight: 400,
|
|
290
|
+
textDecoration: "none",
|
|
291
|
+
},
|
|
292
|
+
} as const;
|
|
293
|
+
|
|
153
294
|
const styles = {
|
|
154
|
-
|
|
155
|
-
|
|
295
|
+
rail: style({
|
|
296
|
+
// Sit in the prose grid's left gutter (column 1). Lines share that outer
|
|
297
|
+
// edge and grow inward. Sticky height keeps them in view, off the
|
|
298
|
+
// scrollbar, and on the article when Diff is open.
|
|
156
299
|
gridColumn: "1",
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
300
|
+
gridRow: "1",
|
|
301
|
+
position: "sticky",
|
|
302
|
+
top: 0,
|
|
303
|
+
alignSelf: "start",
|
|
304
|
+
justifySelf: "stretch",
|
|
305
|
+
boxSizing: "border-box",
|
|
306
|
+
width: "100%",
|
|
307
|
+
minWidth: 0,
|
|
308
|
+
height: "100cqh",
|
|
309
|
+
zIndex: 3,
|
|
310
|
+
display: "flex",
|
|
311
|
+
flexDirection: "column",
|
|
312
|
+
justifyContent: "center",
|
|
313
|
+
alignItems: "flex-start",
|
|
314
|
+
pointerEvents: "none",
|
|
315
|
+
}),
|
|
316
|
+
cluster: style(spacing.padding({ top: 4, bottom: 4, left: 2, right: 6 }), {
|
|
317
|
+
position: "relative",
|
|
318
|
+
display: "flex",
|
|
319
|
+
flexDirection: "column",
|
|
320
|
+
alignItems: "flex-start",
|
|
321
|
+
gap: spacing.value(6),
|
|
322
|
+
pointerEvents: "auto",
|
|
323
|
+
"&[data-open='true'] [data-diffmap-kind='toc-line']": {
|
|
324
|
+
opacity: 0,
|
|
178
325
|
},
|
|
179
|
-
|
|
180
|
-
|
|
326
|
+
}),
|
|
327
|
+
line: style({
|
|
328
|
+
display: "block",
|
|
329
|
+
height: "2px",
|
|
330
|
+
flexShrink: 0,
|
|
331
|
+
borderRadius: "999px",
|
|
332
|
+
backgroundColor: colors.gray[8],
|
|
333
|
+
opacity: 1,
|
|
334
|
+
transition: `opacity ${String(TOC_TRANSITION_MS)}ms ${motionEasing}`,
|
|
335
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
336
|
+
transition: "none",
|
|
181
337
|
},
|
|
182
|
-
"&[data-
|
|
183
|
-
|
|
184
|
-
textDecoration: "none",
|
|
338
|
+
"&[data-current='true']": {
|
|
339
|
+
backgroundColor: colors.gray[12],
|
|
185
340
|
},
|
|
186
341
|
}),
|
|
342
|
+
card: style(
|
|
343
|
+
radius.lg,
|
|
344
|
+
shadow.medium,
|
|
345
|
+
background.element,
|
|
346
|
+
spacing.padding({ x: 3, y: 3 }),
|
|
347
|
+
{
|
|
348
|
+
position: "absolute",
|
|
349
|
+
insetInlineStart: 0,
|
|
350
|
+
top: "50%",
|
|
351
|
+
zIndex: 1,
|
|
352
|
+
boxSizing: "border-box",
|
|
353
|
+
width: "min(240px, calc(100cqw - 24px))",
|
|
354
|
+
maxHeight: "min(32rem, calc(100cqh - 24px))",
|
|
355
|
+
overflowX: "hidden",
|
|
356
|
+
overflowY: "auto",
|
|
357
|
+
overscrollBehavior: "contain",
|
|
358
|
+
transformOrigin: "left center",
|
|
359
|
+
opacity: 0,
|
|
360
|
+
pointerEvents: "none",
|
|
361
|
+
transform: "translateY(-50%) translateX(-6px) scale(0.98)",
|
|
362
|
+
transition: TOC_MOTION,
|
|
363
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
364
|
+
transition: "none",
|
|
365
|
+
},
|
|
366
|
+
"&[data-open='true']": {
|
|
367
|
+
opacity: 1,
|
|
368
|
+
pointerEvents: "auto",
|
|
369
|
+
transform: "translateY(-50%) translateX(0px) scale(1)",
|
|
370
|
+
},
|
|
371
|
+
...tocListRules,
|
|
372
|
+
},
|
|
373
|
+
),
|
|
187
374
|
list: style(text({ size: "sm", fontWeight: 400, color: "lowContrast" })),
|
|
188
375
|
item: style({
|
|
189
376
|
"&::before": {
|
|
@@ -205,6 +392,10 @@ const styles = {
|
|
|
205
392
|
"&[aria-current='location']": {
|
|
206
393
|
color: colors.accent[9],
|
|
207
394
|
fontWeight: 500,
|
|
395
|
+
backgroundColor: colors.accent[3],
|
|
396
|
+
},
|
|
397
|
+
"&[aria-current='location']:hover": {
|
|
398
|
+
backgroundColor: colors.accent[4],
|
|
208
399
|
},
|
|
209
400
|
},
|
|
210
401
|
),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Button, UnorderedList, backgroundColor } from "maui";
|
|
2
|
+
import { style, useStyles } from "purse-styles";
|
|
3
|
+
|
|
4
|
+
export function TocButton(props: { expanded: boolean; onClick?: () => void }) {
|
|
5
|
+
const className = useStyles(pressedClass);
|
|
6
|
+
return (
|
|
7
|
+
<Button
|
|
8
|
+
id="diffmap-toc-button"
|
|
9
|
+
className={className}
|
|
10
|
+
variant="quiet"
|
|
11
|
+
aria-label="Table of contents"
|
|
12
|
+
aria-pressed={props.expanded}
|
|
13
|
+
aria-expanded={props.expanded}
|
|
14
|
+
aria-controls="diffmap-toc"
|
|
15
|
+
onClick={props.onClick}
|
|
16
|
+
>
|
|
17
|
+
<UnorderedList size="sm" />
|
|
18
|
+
</Button>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pressedClass = style({
|
|
23
|
+
flexShrink: 0,
|
|
24
|
+
"&[aria-pressed='true']": {
|
|
25
|
+
backgroundColor: backgroundColor.elementActive,
|
|
26
|
+
},
|
|
27
|
+
});
|