@tanishqkancharla/diffmap 0.1.5 → 0.3.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.
Files changed (42) hide show
  1. package/README.md +64 -57
  2. package/index.html +18 -0
  3. package/package.json +7 -5
  4. package/src/annotations.ts +36 -29
  5. package/src/changes.ts +235 -0
  6. package/src/cli.ts +1 -0
  7. package/src/errors.ts +5 -0
  8. package/src/gist/GistApp.tsx +38 -17
  9. package/src/gist/fetchGist.ts +47 -0
  10. package/src/git.test.ts +402 -0
  11. package/src/git.ts +498 -0
  12. package/src/github/GitHubApp.tsx +39 -22
  13. package/src/github/fetchRepo.test.ts +59 -1
  14. package/src/github/fetchRepo.ts +99 -43
  15. package/src/github/patch.test.ts +18 -9
  16. package/src/github/patch.ts +30 -19
  17. package/src/host/HostHome.tsx +20 -7
  18. package/src/host/files.ts +5 -1
  19. package/src/host/home.md +17 -55
  20. package/src/host/home.test.ts +4 -3
  21. package/src/index.ts +1 -0
  22. package/src/parseFence.ts +6 -6
  23. package/src/parseViewer.test.ts +140 -0
  24. package/src/parseViewer.ts +104 -78
  25. package/src/serve.ts +34 -1
  26. package/src/share.ts +62 -1
  27. package/src/viewer/CallStackDiff.tsx +15 -3
  28. package/src/viewer/ChangesCard.tsx +267 -0
  29. package/src/viewer/ComarkView.tsx +83 -15
  30. package/src/viewer/Fence.tsx +9 -1
  31. package/src/viewer/MermaidBlock.tsx +99 -11
  32. package/src/viewer/RangePicker.tsx +526 -0
  33. package/src/viewer/SourceDiffPanel.tsx +254 -116
  34. package/src/viewer/TableOfContents.tsx +177 -93
  35. package/src/viewer/ViewerApp.tsx +34 -114
  36. package/src/viewer/changesSource.ts +91 -0
  37. package/src/viewer/sourceHeader.test.ts +2 -87
  38. package/src/viewer/sourceHeader.ts +4 -63
  39. package/src/viewer/sourceNavigation.ts +0 -43
  40. package/vite.config.ts +1 -0
  41. package/vite.gist.config.ts +57 -0
  42. package/src/viewer/TocButton.tsx +0 -27
@@ -1,9 +1,18 @@
1
- import { useEffect, useRef, useState, type RefObject } from "react";
1
+ import {
2
+ useEffect,
3
+ useLayoutEffect,
4
+ useRef,
5
+ useState,
6
+ type ReactNode,
7
+ type RefObject,
8
+ } from "react";
9
+ import { motion, useReducedMotion, type Transition } from "motion/react";
2
10
  import {
3
11
  background,
4
12
  colors,
5
13
  focusRing,
6
14
  motionEasing,
15
+ overlayMotion,
7
16
  radius,
8
17
  shadow,
9
18
  spacing,
@@ -14,23 +23,37 @@ import type { ViewerHeading } from "../parseViewer.js";
14
23
 
15
24
  type TocItem = ViewerHeading & { children: TocItem[] };
16
25
 
17
- const TOC_LINE_MAX_PX = 24;
18
- const TOC_LINE_MIN_PX = 8;
19
- const TOC_LINE_STEP_PX = 6;
26
+ const TOC_LINE_MAX_PX = 12;
27
+ const TOC_LINE_MIN_PX = 3;
28
+ const TOC_LINE_STEP_PX = 3;
20
29
  const DWELL_MS = 50;
21
30
  const LEAVE_MS = 100;
22
31
  const TOC_TRANSITION_MS = 180;
23
- const TOC_MOTION = `opacity ${String(TOC_TRANSITION_MS)}ms ${motionEasing}, transform ${String(TOC_TRANSITION_MS)}ms ${motionEasing}`;
32
+
33
+ // Maui's Dialog motion (no-bounce spring in, ease-in out), about 25% faster
34
+ // so the menu keeps up with hover.
35
+ const cardEnter: Transition = {
36
+ type: "spring",
37
+ visualDuration: 0.15,
38
+ bounce: overlayMotion.dialogEnter.bounce,
39
+ };
40
+ const cardExit: Transition = {
41
+ duration: 0.09,
42
+ ease: "easeIn",
43
+ };
44
+ const cardReduced: Transition = {
45
+ duration: overlayMotion.reduced.durationMs / 1000,
46
+ ease: "easeInOut",
47
+ };
24
48
 
25
49
  /** Equal side gutters so the line rail fits and the article column stays centered. */
26
50
  export const TOC_SIDE_MARGIN_PX = 48;
27
51
 
28
52
  /**
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.
53
+ * Left gutter width at which the list sits there as-is. Narrower gutters (a
54
+ * small window, or an open Diff panel) collapse it to lines and a hover card.
32
55
  */
33
- export const TOC_OPEN_MIN_WIDTH_PX = 1101;
56
+ const TOC_DOCKED_MIN_GUTTER_PX = 220;
34
57
 
35
58
  export function hasTableOfContents(headings: ViewerHeading[]) {
36
59
  return tocHeadings(headings).length > 0;
@@ -44,9 +67,6 @@ export function tocLineWidth(level: number, shallowestLevel: number) {
44
67
  export function TableOfContents(props: {
45
68
  headings: ViewerHeading[];
46
69
  articleRef: RefObject<HTMLElement | null>;
47
- open: boolean;
48
- onDwellOpen: () => void;
49
- onDwellClose: () => void;
50
70
  }) {
51
71
  const headings = tocHeadings(props.headings);
52
72
  const items = nestHeadings(headings);
@@ -54,16 +74,59 @@ export function TableOfContents(props: {
54
74
  props.headings,
55
75
  props.articleRef,
56
76
  );
77
+ const navRef = useRef<HTMLElement>(null);
78
+ const docked = useDocked(navRef);
57
79
  const railClass = useStyles(styles.rail);
80
+ const dockedClass = useStyles(styles.docked);
81
+ const listClass = useStyles(styles.list);
82
+
83
+ if (items.length === 0) return undefined;
84
+
85
+ const list = (
86
+ <TocList
87
+ items={items}
88
+ className={listClass}
89
+ activeId={activeId}
90
+ onSelect={setActiveId}
91
+ />
92
+ );
93
+
94
+ return (
95
+ <nav
96
+ ref={navRef}
97
+ id="diffmap-toc"
98
+ className={railClass}
99
+ aria-label="Table of contents"
100
+ data-diffmap-kind="toc"
101
+ data-docked={docked ? "true" : "false"}
102
+ >
103
+ {docked ? (
104
+ <div className={dockedClass} data-diffmap-kind="toc-list">
105
+ {list}
106
+ </div>
107
+ ) : (
108
+ <TocRail headings={headings} activeId={activeId}>
109
+ {list}
110
+ </TocRail>
111
+ )}
112
+ </nav>
113
+ );
114
+ }
115
+
116
+ /** Heading lines that open the list in a floating card on hover. */
117
+ function TocRail(props: {
118
+ headings: ViewerHeading[];
119
+ activeId: string | undefined;
120
+ children: ReactNode;
121
+ }) {
58
122
  const clusterClass = useStyles(styles.cluster);
59
123
  const lineClass = useStyles(styles.line);
60
124
  const cardClass = useStyles(styles.card);
61
- const listClass = useStyles(styles.list);
125
+ const reduceMotion = Boolean(useReducedMotion());
126
+ const [open, setOpen] = useState(false);
62
127
  const hoveringRef = useRef(false);
63
128
  const dwellTimer = useRef<number | undefined>(undefined);
64
129
  const closeTimer = useRef<number | undefined>(undefined);
65
- const onDwellOpen = props.onDwellOpen;
66
- const onDwellClose = props.onDwellClose;
67
130
 
68
131
  useEffect(() => {
69
132
  const onPageLeave = () => {
@@ -74,7 +137,7 @@ export function TableOfContents(props: {
74
137
  window.clearTimeout(closeTimer.current);
75
138
  dwellTimer.current = undefined;
76
139
  closeTimer.current = undefined;
77
- onDwellClose();
140
+ setOpen(false);
78
141
  };
79
142
  document.documentElement.addEventListener("mouseleave", onPageLeave);
80
143
  return () => {
@@ -82,77 +145,69 @@ export function TableOfContents(props: {
82
145
  window.clearTimeout(dwellTimer.current);
83
146
  window.clearTimeout(closeTimer.current);
84
147
  };
85
- }, [onDwellClose]);
86
-
87
- if (items.length === 0) return undefined;
148
+ }, []);
88
149
 
89
- const shallowest = headings.reduce(
150
+ const shallowest = props.headings.reduce(
90
151
  (min, heading) => Math.min(min, heading.level),
91
152
  6,
92
153
  );
93
154
 
94
155
  return (
95
- <nav
96
- id="diffmap-toc"
97
- className={railClass}
98
- aria-label="Table of contents"
99
- data-diffmap-kind="toc"
100
- data-open={props.open ? "true" : "false"}
101
- >
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);
156
+ <div
157
+ className={clusterClass}
158
+ data-open={open ? "true" : "false"}
159
+ onPointerEnter={() => {
160
+ hoveringRef.current = true;
161
+ window.clearTimeout(closeTimer.current);
162
+ closeTimer.current = undefined;
163
+ if (open || dwellTimer.current !== undefined) return;
164
+ dwellTimer.current = window.setTimeout(() => {
119
165
  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
- }}
166
+ if (!hoveringRef.current) return;
167
+ setOpen(true);
168
+ }, DWELL_MS);
169
+ }}
170
+ onPointerLeave={() => {
171
+ hoveringRef.current = false;
172
+ window.clearTimeout(dwellTimer.current);
173
+ dwellTimer.current = undefined;
174
+ window.clearTimeout(closeTimer.current);
175
+ closeTimer.current = window.setTimeout(() => {
176
+ closeTimer.current = undefined;
177
+ if (hoveringRef.current) return;
178
+ setOpen(false);
179
+ }, LEAVE_MS);
180
+ }}
181
+ >
182
+ {props.headings.map((heading) => (
183
+ <span
184
+ key={heading.id}
185
+ className={lineClass}
186
+ data-diffmap-kind="toc-line"
187
+ data-level={heading.level}
188
+ data-current={props.activeId === heading.id ? "true" : "false"}
189
+ style={{ width: tocLineWidth(heading.level, shallowest) }}
190
+ />
191
+ ))}
192
+ <motion.div
193
+ className={cardClass}
194
+ // Motion owns transform, so it also applies the vertical centering.
195
+ style={{ y: "-50%" }}
196
+ initial={false}
197
+ animate={
198
+ open
199
+ ? { opacity: 1, scale: 1 }
200
+ : { opacity: 0, scale: reduceMotion ? 1 : 0.94 }
201
+ }
202
+ transition={reduceMotion ? cardReduced : open ? cardEnter : cardExit}
203
+ data-diffmap-kind="toc-card"
204
+ data-open={open ? "true" : "false"}
205
+ aria-hidden={!open}
206
+ inert={!open}
127
207
  >
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>
155
- </nav>
208
+ {props.children}
209
+ </motion.div>
210
+ </div>
156
211
  );
157
212
  }
158
213
 
@@ -239,6 +294,29 @@ function nestHeadings(headings: ViewerHeading[]): TocItem[] {
239
294
  return items;
240
295
  }
241
296
 
297
+ /**
298
+ * The nav fills the gutter column in both modes, so its width is the gutter's
299
+ * and does not depend on which mode renders inside it.
300
+ */
301
+ function useDocked(navRef: RefObject<HTMLElement | null>) {
302
+ const [docked, setDocked] = useState(false);
303
+
304
+ useLayoutEffect(() => {
305
+ const node = navRef.current;
306
+ if (node === null) return;
307
+ const measure = () => {
308
+ if (node.clientWidth === 0) return;
309
+ setDocked(node.clientWidth >= TOC_DOCKED_MIN_GUTTER_PX);
310
+ };
311
+ measure();
312
+ const observer = new ResizeObserver(measure);
313
+ observer.observe(node);
314
+ return () => observer.disconnect();
315
+ }, [navRef]);
316
+
317
+ return docked;
318
+ }
319
+
242
320
  function useActiveHeading(
243
321
  headings: ViewerHeading[],
244
322
  articleRef: RefObject<HTMLElement | null>,
@@ -273,19 +351,19 @@ function useActiveHeading(
273
351
  }
274
352
 
275
353
  const tocListRules = {
276
- "&[data-diffmap-kind='toc-card'] ol": {
354
+ "& ol": {
277
355
  listStyle: "none",
278
356
  counterReset: "none",
279
357
  margin: 0,
280
358
  padding: 0,
281
359
  },
282
- "&[data-diffmap-kind='toc-card'] ol ol": {
360
+ "& ol ol": {
283
361
  paddingInlineStart: spacing.value(6),
284
362
  },
285
- "&[data-diffmap-kind='toc-card'] ol > li::before": {
363
+ "& ol > li::before": {
286
364
  content: "none",
287
365
  },
288
- "&[data-diffmap-kind='toc-card'] a": {
366
+ "& a": {
289
367
  fontWeight: 400,
290
368
  textDecoration: "none",
291
369
  },
@@ -315,10 +393,11 @@ const styles = {
315
393
  }),
316
394
  cluster: style(spacing.padding({ top: 4, bottom: 4, left: 2, right: 6 }), {
317
395
  position: "relative",
396
+ minWidth: "40px",
318
397
  display: "flex",
319
398
  flexDirection: "column",
320
399
  alignItems: "flex-start",
321
- gap: spacing.value(6),
400
+ gap: "12px",
322
401
  pointerEvents: "auto",
323
402
  "&[data-open='true'] [data-diffmap-kind='toc-line']": {
324
403
  opacity: 0,
@@ -326,7 +405,7 @@ const styles = {
326
405
  }),
327
406
  line: style({
328
407
  display: "block",
329
- height: "2px",
408
+ height: "1px",
330
409
  flexShrink: 0,
331
410
  borderRadius: "999px",
332
411
  backgroundColor: colors.gray[8],
@@ -356,21 +435,26 @@ const styles = {
356
435
  overflowY: "auto",
357
436
  overscrollBehavior: "contain",
358
437
  transformOrigin: "left center",
359
- opacity: 0,
360
438
  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
439
  "&[data-open='true']": {
367
- opacity: 1,
368
440
  pointerEvents: "auto",
369
- transform: "translateY(-50%) translateX(0px) scale(1)",
370
441
  },
371
442
  ...tocListRules,
372
443
  },
373
444
  ),
445
+ // The same list as the card, laid straight into the gutter: no surface,
446
+ // shadow, or hover-to-open. Stays inside the gutter so it never covers prose.
447
+ docked: style(spacing.padding({ right: 6 }), {
448
+ boxSizing: "border-box",
449
+ width: "100%",
450
+ maxWidth: "240px",
451
+ maxHeight: "calc(100cqh - 24px)",
452
+ overflowX: "hidden",
453
+ overflowY: "auto",
454
+ overscrollBehavior: "contain",
455
+ pointerEvents: "auto",
456
+ ...tocListRules,
457
+ }),
374
458
  list: style(text({ size: "sm", fontWeight: 400, color: "lowContrast" })),
375
459
  item: style({
376
460
  "&::before": {
@@ -1,10 +1,4 @@
1
- import {
2
- useCallback,
3
- useEffect,
4
- useRef,
5
- useState,
6
- type ReactNode,
7
- } from "react";
1
+ import { useEffect, useRef, useState, type ReactNode } from "react";
8
2
  import {
9
3
  backgroundColor,
10
4
  border,
@@ -19,16 +13,15 @@ import {
19
13
  text,
20
14
  } from "maui";
21
15
  import { style, useStyles } from "purse-styles";
16
+ import { ALL, WORKING, type Rev } from "../changes.js";
22
17
  import type { ViewerDocument } from "../parseViewer.js";
23
18
  import { ComarkView } from "./ComarkView.tsx";
24
19
  import { SourceDiffPanel, type SourceSelection } from "./SourceDiffPanel.js";
25
20
  import { CloseServerButton } from "./CloseServerButton.tsx";
26
21
  import { DiffButton } from "./DiffButton.tsx";
27
- import { TocButton } from "./TocButton.tsx";
28
22
  import {
29
23
  hasTableOfContents,
30
24
  TableOfContents,
31
- TOC_OPEN_MIN_WIDTH_PX,
32
25
  TOC_SIDE_MARGIN_PX,
33
26
  } from "./TableOfContents.tsx";
34
27
  import { ViewerModeContext, type ViewerMode } from "./viewerMode.ts";
@@ -42,13 +35,31 @@ export function ViewerApp(props: {
42
35
  mode: ViewerMode;
43
36
  title?: string;
44
37
  headerActions?: ReactNode;
38
+ /** Changes the Diff button opens before any reference is selected, such as a pull request. */
39
+ defaultRev?: Rev;
45
40
  }) {
46
41
  const viewerDocument = props.document;
47
42
  const meta = useViewerMeta(props.mode === "local");
48
43
  const [selection, setSelection] = useState<SourceSelection>();
49
44
  const [showDiffPanel, setShowDiffPanel] = useState(false);
45
+ const local = props.mode === "local";
46
+ const documentRev =
47
+ props.defaultRev ??
48
+ viewerDocument.changeRevs[0] ??
49
+ viewerDocument.references.find((ref) => ref.rev !== undefined)?.rev ??
50
+ (local ? ALL : WORKING);
51
+ const [panelRev, setPanelRev] = useState(documentRev);
52
+ // A reference opens its own rev; a rev-less one opens uncommitted changes locally.
53
+ const select = (next: SourceSelection) => {
54
+ setSelection(next);
55
+ setPanelRev(next.reference.rev ?? (local ? WORKING : documentRev));
56
+ };
57
+ // A local viewer always has git changes to show.
50
58
  const hasSourceDiffs =
51
- viewerDocument.sourceDiffs.length > 0 || viewerDocument.hasReferences;
59
+ local ||
60
+ viewerDocument.references.length > 0 ||
61
+ viewerDocument.changeRevs.length > 0 ||
62
+ props.defaultRev !== undefined;
52
63
  const diffPanelOpen = hasSourceDiffs && showDiffPanel;
53
64
  const body = useStyles(styles.body);
54
65
  const [shutDown, setShutDown] = useState(false);
@@ -71,61 +82,6 @@ export function ViewerApp(props: {
71
82
  const stage = useStyles(styles.stage);
72
83
  const articleRef = useRef<HTMLElement>(null);
73
84
  const hasToc = hasTableOfContents(viewerDocument.headings);
74
- const tocDismissed = useRef(false);
75
- const tocPinned = useRef(false);
76
- const tocFits = useRef(window.innerWidth >= TOC_OPEN_MIN_WIDTH_PX);
77
- const remeasureToc = useRef<() => void>(() => {});
78
- const [tocOpen, setTocOpen] = useState<TocMode | undefined>(() =>
79
- hasToc && window.innerWidth >= TOC_OPEN_MIN_WIDTH_PX ? "space" : undefined,
80
- );
81
- const tocExpanded = tocOpen !== undefined;
82
- const onDwellOpen = useCallback(() => {
83
- setTocOpen((mode) =>
84
- mode === "click" || mode === "space" ? mode : "dwell",
85
- );
86
- }, []);
87
- const onDwellClose = useCallback(() => {
88
- setTocOpen((mode) => {
89
- if (mode === "click" || mode === "space") return mode;
90
- if (tocFits.current && !tocDismissed.current) {
91
- return tocPinned.current ? "click" : "space";
92
- }
93
- return undefined;
94
- });
95
- }, []);
96
-
97
- useEffect(() => {
98
- if (!hasToc) return;
99
- const measure = () => {
100
- const node = articleRef.current;
101
- if (node !== null && node.clientWidth === 0) return;
102
- const width = node?.clientWidth ?? window.innerWidth;
103
- const next = width >= TOC_OPEN_MIN_WIDTH_PX;
104
- const previous = tocFits.current;
105
- tocFits.current = next;
106
- if (previous === next) return;
107
- if (!next) {
108
- setTocOpen(undefined);
109
- return;
110
- }
111
- setTocOpen((mode) => {
112
- if (mode === "click" || mode === "dwell") return mode;
113
- if (tocDismissed.current) return undefined;
114
- return tocPinned.current ? "click" : "space";
115
- });
116
- };
117
- remeasureToc.current = measure;
118
- const observer = new ResizeObserver(measure);
119
- const articleNode = articleRef.current;
120
- if (articleNode) observer.observe(articleNode);
121
- window.addEventListener("resize", measure);
122
- return () => {
123
- observer.disconnect();
124
- window.removeEventListener("resize", measure);
125
- remeasureToc.current = () => {};
126
- };
127
- }, [hasToc]);
128
-
129
85
  useEffect(() => {
130
86
  document.title = title;
131
87
  }, [title]);
@@ -152,32 +108,13 @@ export function ViewerApp(props: {
152
108
  <div className={shell}>
153
109
  <header className={header}>
154
110
  <div className={heading}>
155
- {hasToc && (
156
- <TocButton
157
- expanded={tocExpanded}
158
- onClick={() => {
159
- if (tocExpanded) {
160
- tocDismissed.current = true;
161
- tocPinned.current = false;
162
- setTocOpen(undefined);
163
- return;
164
- }
165
- tocDismissed.current = false;
166
- tocPinned.current = true;
167
- setTocOpen("click");
168
- }}
169
- />
170
- )}
171
111
  <div className={titleClass}>{title}</div>
172
112
  </div>
173
113
  <div className={actions}>
174
114
  {hasSourceDiffs && (
175
115
  <DiffButton
176
116
  pressed={showDiffPanel}
177
- onClick={() => {
178
- setShowDiffPanel((open) => !open);
179
- window.requestAnimationFrame(() => remeasureToc.current());
180
- }}
117
+ onClick={() => setShowDiffPanel((open) => !open)}
181
118
  />
182
119
  )}
183
120
  {props.headerActions}
@@ -202,13 +139,10 @@ export function ViewerApp(props: {
202
139
  selectedAnnotation={selection?.annotation}
203
140
  onSelectAnnotation={(line) => {
204
141
  setShowDiffPanel(true);
205
- setSelection({
142
+ select({
206
143
  annotation: line,
207
144
  reference: line.references[0]!,
208
145
  });
209
- window.requestAnimationFrame(() =>
210
- remeasureToc.current(),
211
- );
212
146
  }}
213
147
  />
214
148
  </div>
@@ -216,18 +150,24 @@ export function ViewerApp(props: {
216
150
  <TableOfContents
217
151
  headings={viewerDocument.headings}
218
152
  articleRef={articleRef}
219
- open={tocExpanded}
220
- onDwellOpen={onDwellOpen}
221
- onDwellClose={onDwellClose}
222
153
  />
223
154
  )}
224
155
  </div>
225
156
  </article>
226
157
  {diffPanelOpen && (
227
158
  <SourceDiffPanel
228
- items={viewerDocument.sourceDiffs}
159
+ rev={panelRev}
160
+ onRevChange={
161
+ local
162
+ ? (rev) => {
163
+ // A focused reference's lines belong to its own rev.
164
+ setSelection(undefined);
165
+ setPanelRev(rev);
166
+ }
167
+ : undefined
168
+ }
229
169
  selection={selection}
230
- onSelect={setSelection}
170
+ onSelect={select}
231
171
  />
232
172
  )}
233
173
  </div>
@@ -237,8 +177,6 @@ export function ViewerApp(props: {
237
177
  );
238
178
  }
239
179
 
240
- type TocMode = "click" | "dwell" | "space";
241
-
242
180
  function useViewerMeta(enabled: boolean) {
243
181
  const [meta, setMeta] = useState<ViewerMeta>();
244
182
  useEffect(() => {
@@ -350,24 +288,6 @@ const styles = {
350
288
  "& ul > li[data-task]::before, & ol > li[data-task]::before": {
351
289
  content: "none",
352
290
  },
353
- "& ul > li[data-task] > .diffmap-task-checkbox, & ol > li[data-task] > .diffmap-task-checkbox":
354
- {
355
- // Maui proseHtml md listPadding.
356
- position: "absolute",
357
- left: "-20px",
358
- top: "6px",
359
- },
360
- "& .diffmap-task-checkbox label > span:last-child": {
361
- position: "absolute",
362
- width: "1px",
363
- height: "1px",
364
- padding: 0,
365
- margin: "-1px",
366
- overflow: "hidden",
367
- clip: "rect(0, 0, 0, 0)",
368
- whiteSpace: "nowrap",
369
- border: 0,
370
- },
371
291
  }),
372
292
  closed: style(
373
293
  flex({