@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.
Files changed (50) hide show
  1. package/README.md +25 -14
  2. package/package.json +3 -3
  3. package/src/cli.test.ts +50 -0
  4. package/src/cli.ts +126 -51
  5. package/src/contentPlugin.ts +5 -3
  6. package/src/errors.ts +6 -1
  7. package/src/gist/GistApp.tsx +34 -21
  8. package/src/gist/GistStatus.tsx +8 -43
  9. package/src/gist/main.tsx +2 -2
  10. package/src/gist/pin.ts +14 -0
  11. package/src/github/GitHubApp.tsx +235 -0
  12. package/src/github/fetchRepo.test.ts +17 -0
  13. package/src/github/fetchRepo.ts +300 -0
  14. package/src/github/patch.test.ts +30 -0
  15. package/src/github/patch.ts +40 -0
  16. package/src/github/pin.ts +14 -0
  17. package/src/github/route.test.ts +68 -0
  18. package/src/github/route.ts +175 -0
  19. package/src/host/HostApp.tsx +22 -0
  20. package/src/host/HostHome.tsx +73 -0
  21. package/src/host/files.ts +10 -0
  22. package/src/host/home.md +194 -0
  23. package/src/host/home.test.ts +82 -0
  24. package/src/index.ts +1 -0
  25. package/src/mermaid.ts +29 -1
  26. package/src/packageVersion.ts +7 -0
  27. package/src/parseViewer.test.ts +185 -0
  28. package/src/parseViewer.ts +27 -3
  29. package/src/serve.test.ts +306 -0
  30. package/src/serve.ts +23 -5
  31. package/src/share.ts +5 -1
  32. package/src/update.test.ts +19 -0
  33. package/src/update.ts +285 -0
  34. package/src/viewer/CallStackDiff.tsx +26 -6
  35. package/src/viewer/CloseServerButton.tsx +16 -0
  36. package/src/viewer/FileExcerpt.tsx +52 -3
  37. package/src/viewer/HtmlPlaceholder.tsx +6 -1
  38. package/src/viewer/SourceDiffPanel.tsx +155 -59
  39. package/src/viewer/TableOfContents.tsx +230 -39
  40. package/src/viewer/TocButton.tsx +27 -0
  41. package/src/viewer/ViewerApp.tsx +182 -47
  42. package/src/viewer/main.tsx +10 -3
  43. package/src/viewer/sourceHeader.test.ts +131 -0
  44. package/src/viewer/sourceHeader.ts +103 -0
  45. package/src/viewer/sourceNavigation.ts +81 -3
  46. package/src/viewer/viewerMode.ts +1 -1
  47. package/src/viewer/vite-env.d.ts +2 -1
  48. package/vite.config.ts +1 -1
  49. package/src/viewer/Check.tsx +0 -24
  50. 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 options = pierreDiffOptions({
72
- themeType: resolvedTheme,
73
- disableFileHeader: false,
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" && source === undefined
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 (selectedLines === null) return;
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
- ...selectedLines,
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
- }, [selectedLines]);
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 === "gist") return;
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
- <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
- }}
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
- {history.length === 1 ? "Back to reference" : "Back"}
202
- </Button>
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
- <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
- </>
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
- gridColumn: "3",
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
- overflowWrap: "anywhere",
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 { colors, focusRing, radius, spacing, text } from "maui";
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 items = nestHeadings(tocHeadings(props.headings));
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 navClass = useStyles(styles.nav);
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
- className={navClass}
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
- <TocList
28
- items={items}
29
- className={listClass}
30
- activeId={activeId}
31
- onSelect={setActiveId}
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
- nav: style(spacing.padding({ left: 16, right: 6, top: 8, bottom: 8 }), {
155
- boxSizing: "border-box",
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
- 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),
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
- "&[data-diffmap-kind='toc'] ol > li::before": {
180
- content: "none",
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-diffmap-kind='toc'] a": {
183
- fontWeight: 400,
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
+ });