@tanishqkancharla/diffmap 0.1.4 → 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 +12 -4
- package/package.json +3 -3
- package/src/cli.test.ts +50 -0
- package/src/cli.ts +40 -5
- package/src/errors.ts +5 -0
- package/src/gist/GistApp.tsx +3 -2
- package/src/gist/GistStatus.tsx +2 -51
- 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/packageVersion.ts +7 -0
- package/src/serve.test.ts +306 -0
- package/src/serve.ts +14 -4
- package/src/update.test.ts +19 -0
- package/src/update.ts +285 -0
- package/src/viewer/CloseServerButton.tsx +16 -0
- package/src/viewer/FileExcerpt.tsx +25 -3
- package/src/viewer/HtmlPlaceholder.tsx +6 -1
- package/src/viewer/SourceDiffPanel.tsx +26 -7
- package/src/viewer/TableOfContents.tsx +230 -40
- package/src/viewer/TocButton.tsx +27 -0
- package/src/viewer/ViewerApp.tsx +182 -50
- package/src/viewer/sourceNavigation.ts +37 -3
- package/src/viewer/viewerMode.ts +1 -1
- package/vite.config.ts +1 -1
- package/src/viewer/Check.tsx +0 -24
- package/src/viewer/DoneButton.tsx +0 -32
|
@@ -5,6 +5,7 @@ import { useStyles } from "purse-styles";
|
|
|
5
5
|
import { pierreFileOptions, pierreShell } from "./pierre.ts";
|
|
6
6
|
import { useViewerMode } from "./viewerMode.ts";
|
|
7
7
|
import { useGitHubPin } from "../github/pin.ts";
|
|
8
|
+
import { useHostFiles } from "../host/files.ts";
|
|
8
9
|
import { fetchPinnedBlob } from "../github/fetchRepo.ts";
|
|
9
10
|
|
|
10
11
|
type FileExcerptPayload = {
|
|
@@ -43,8 +44,10 @@ function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
|
43
44
|
const [excerpt, setExcerpt] = useState<FileExcerptPayload>();
|
|
44
45
|
const mode = useViewerMode();
|
|
45
46
|
const pin = useGitHubPin();
|
|
47
|
+
const hostFiles = useHostFiles();
|
|
48
|
+
const hostExcerpt = hostFileExcerpt(hostFiles, input);
|
|
46
49
|
useEffect(() => {
|
|
47
|
-
if (
|
|
50
|
+
if (hostFiles !== undefined) return;
|
|
48
51
|
if (mode === "github") {
|
|
49
52
|
if (pin === undefined) return;
|
|
50
53
|
// oxlint-disable-next-line typescript/no-floating-promises -- React effects cannot await; this request owns the excerpt update.
|
|
@@ -69,6 +72,7 @@ function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
|
69
72
|
});
|
|
70
73
|
return;
|
|
71
74
|
}
|
|
75
|
+
if (mode !== "local") return;
|
|
72
76
|
const params = new URLSearchParams({
|
|
73
77
|
path: input.path,
|
|
74
78
|
start: String(input.start),
|
|
@@ -87,6 +91,24 @@ function useFileExcerpt(input: { path: string; start: number; end: number }) {
|
|
|
87
91
|
.catch((cause) => {
|
|
88
92
|
console.warn("diffmap file excerpt failed", cause);
|
|
89
93
|
});
|
|
90
|
-
}, [input.path, input.start, input.end, mode, pin]);
|
|
91
|
-
return excerpt;
|
|
94
|
+
}, [input.path, input.start, input.end, mode, pin, hostFiles]);
|
|
95
|
+
return hostExcerpt ?? excerpt;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hostFileExcerpt(
|
|
99
|
+
files: Record<string, string> | undefined,
|
|
100
|
+
input: { path: string; start: number; end: number },
|
|
101
|
+
): FileExcerptPayload | undefined {
|
|
102
|
+
if (files === undefined) return undefined;
|
|
103
|
+
const contents = files[input.path];
|
|
104
|
+
if (contents === undefined) return undefined;
|
|
105
|
+
const lines = contents.split(/\r?\n/);
|
|
106
|
+
const start = input.start;
|
|
107
|
+
const end = Math.min(input.end, lines.length);
|
|
108
|
+
return {
|
|
109
|
+
path: input.path,
|
|
110
|
+
start,
|
|
111
|
+
end,
|
|
112
|
+
contents: lines.slice(start - 1, end).join("\n"),
|
|
113
|
+
};
|
|
92
114
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { colors, radius, spacing, text } from "maui";
|
|
2
2
|
import { style, useStyles } from "purse-styles";
|
|
3
|
+
import { useViewerMode } from "./viewerMode.ts";
|
|
3
4
|
|
|
4
5
|
export function HtmlPlaceholder(props: { inline?: boolean }) {
|
|
5
6
|
const inline = props.inline === true;
|
|
6
7
|
const className = useStyles(inline ? styles.inline : styles.block);
|
|
7
|
-
const
|
|
8
|
+
const mode = useViewerMode();
|
|
9
|
+
const copy =
|
|
10
|
+
mode === "gist"
|
|
11
|
+
? "HTML from this gist is not rendered."
|
|
12
|
+
: "HTML is not rendered on the hosted viewer.";
|
|
8
13
|
if (inline) return <span className={className}>{copy}</span>;
|
|
9
14
|
return (
|
|
10
15
|
<aside className={className} data-diffmap-kind="html-disabled">
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import type { SourceDefinition } from "../definitions.js";
|
|
25
25
|
import { useGistPin } from "../gist/pin.ts";
|
|
26
26
|
import { useGitHubPin } from "../github/pin.ts";
|
|
27
|
+
import { useHostFiles } from "../host/files.ts";
|
|
27
28
|
import {
|
|
28
29
|
matchingSourceDiff,
|
|
29
30
|
requestSource,
|
|
@@ -80,6 +81,7 @@ export function SourceDiffPanel(props: {
|
|
|
80
81
|
const code = useStyles(styles.code);
|
|
81
82
|
const github = useGitHubPin();
|
|
82
83
|
const gist = useGistPin();
|
|
84
|
+
const hostFiles = useHostFiles();
|
|
83
85
|
const selection = props.selection;
|
|
84
86
|
const source = useSourceReference(selection?.reference);
|
|
85
87
|
const resolvedFile = source instanceof Error ? undefined : source;
|
|
@@ -103,7 +105,7 @@ export function SourceDiffPanel(props: {
|
|
|
103
105
|
? source.message
|
|
104
106
|
: selection?.reference.kind === "file" &&
|
|
105
107
|
source === undefined &&
|
|
106
|
-
mode
|
|
108
|
+
(mode === "local" || mode === "github" || hostFiles !== undefined)
|
|
107
109
|
? "Opening source…"
|
|
108
110
|
: undefined;
|
|
109
111
|
const selectedLines = useMemo<CodeViewLineSelection | null>(() => {
|
|
@@ -122,6 +124,13 @@ export function SourceDiffPanel(props: {
|
|
|
122
124
|
},
|
|
123
125
|
};
|
|
124
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;
|
|
125
134
|
|
|
126
135
|
const openPath = sourcePanelPath({
|
|
127
136
|
filePath: file?.path,
|
|
@@ -150,14 +159,25 @@ export function SourceDiffPanel(props: {
|
|
|
150
159
|
});
|
|
151
160
|
|
|
152
161
|
useEffect(() => {
|
|
153
|
-
if (
|
|
162
|
+
if (
|
|
163
|
+
rangeId === undefined ||
|
|
164
|
+
rangeStart === undefined ||
|
|
165
|
+
rangeEnd === undefined
|
|
166
|
+
) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
154
169
|
viewer.current?.scrollTo({
|
|
155
170
|
type: "range",
|
|
156
|
-
|
|
171
|
+
id: rangeId,
|
|
172
|
+
range: {
|
|
173
|
+
start: rangeStart,
|
|
174
|
+
end: rangeEnd,
|
|
175
|
+
...(rangeSide === undefined ? {} : { side: rangeSide }),
|
|
176
|
+
},
|
|
157
177
|
align: "center",
|
|
158
178
|
behavior: "instant",
|
|
159
179
|
});
|
|
160
|
-
}, [
|
|
180
|
+
}, [rangeId, rangeStart, rangeEnd, rangeSide]);
|
|
161
181
|
|
|
162
182
|
const onTokenClick: CodeViewOptions<undefined, undefined>["onTokenClick"] = (
|
|
163
183
|
token,
|
|
@@ -326,13 +346,12 @@ export function SourceDiffPanel(props: {
|
|
|
326
346
|
|
|
327
347
|
const styles = {
|
|
328
348
|
panel: style(flex({ direction: "column" }), border(["left"], "border"), {
|
|
329
|
-
|
|
349
|
+
gridArea: "diff",
|
|
330
350
|
minWidth: 0,
|
|
331
351
|
minHeight: 0,
|
|
332
352
|
overflow: "hidden",
|
|
333
353
|
backgroundColor: backgroundColor.app,
|
|
334
354
|
"@media (max-width: 900px)": {
|
|
335
|
-
gridColumn: "1",
|
|
336
355
|
height: "45vh",
|
|
337
356
|
borderLeft: "none",
|
|
338
357
|
borderTop: `1px solid ${borderColor.border}`,
|
|
@@ -351,7 +370,7 @@ const styles = {
|
|
|
351
370
|
flexShrink: 0,
|
|
352
371
|
},
|
|
353
372
|
),
|
|
354
|
-
titleRow: style(flex({ direction: "row",
|
|
373
|
+
titleRow: style(flex({ direction: "row", alignItems: "center" }), {
|
|
355
374
|
gap: "6px",
|
|
356
375
|
minWidth: 0,
|
|
357
376
|
}),
|
|
@@ -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,41 +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
|
-
"
|
|
178
|
-
|
|
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,
|
|
179
325
|
},
|
|
180
|
-
|
|
181
|
-
|
|
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",
|
|
182
337
|
},
|
|
183
|
-
"&[data-
|
|
184
|
-
|
|
185
|
-
textDecoration: "none",
|
|
338
|
+
"&[data-current='true']": {
|
|
339
|
+
backgroundColor: colors.gray[12],
|
|
186
340
|
},
|
|
187
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
|
+
),
|
|
188
374
|
list: style(text({ size: "sm", fontWeight: 400, color: "lowContrast" })),
|
|
189
375
|
item: style({
|
|
190
376
|
"&::before": {
|
|
@@ -206,6 +392,10 @@ const styles = {
|
|
|
206
392
|
"&[aria-current='location']": {
|
|
207
393
|
color: colors.accent[9],
|
|
208
394
|
fontWeight: 500,
|
|
395
|
+
backgroundColor: colors.accent[3],
|
|
396
|
+
},
|
|
397
|
+
"&[aria-current='location']:hover": {
|
|
398
|
+
backgroundColor: colors.accent[4],
|
|
209
399
|
},
|
|
210
400
|
},
|
|
211
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
|
+
});
|