@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.
- package/README.md +64 -57
- package/index.html +18 -0
- package/package.json +7 -5
- package/src/annotations.ts +36 -29
- package/src/changes.ts +235 -0
- package/src/cli.ts +1 -0
- package/src/errors.ts +5 -0
- package/src/gist/GistApp.tsx +38 -17
- package/src/gist/fetchGist.ts +47 -0
- package/src/git.test.ts +402 -0
- package/src/git.ts +498 -0
- package/src/github/GitHubApp.tsx +39 -22
- package/src/github/fetchRepo.test.ts +59 -1
- package/src/github/fetchRepo.ts +99 -43
- package/src/github/patch.test.ts +18 -9
- package/src/github/patch.ts +30 -19
- package/src/host/HostHome.tsx +20 -7
- package/src/host/files.ts +5 -1
- package/src/host/home.md +17 -55
- package/src/host/home.test.ts +4 -3
- package/src/index.ts +1 -0
- package/src/parseFence.ts +6 -6
- package/src/parseViewer.test.ts +140 -0
- package/src/parseViewer.ts +104 -78
- package/src/serve.ts +34 -1
- package/src/share.ts +62 -1
- package/src/viewer/CallStackDiff.tsx +15 -3
- package/src/viewer/ChangesCard.tsx +267 -0
- package/src/viewer/ComarkView.tsx +83 -15
- package/src/viewer/Fence.tsx +9 -1
- package/src/viewer/MermaidBlock.tsx +99 -11
- package/src/viewer/RangePicker.tsx +526 -0
- package/src/viewer/SourceDiffPanel.tsx +254 -116
- package/src/viewer/TableOfContents.tsx +177 -93
- package/src/viewer/ViewerApp.tsx +34 -114
- package/src/viewer/changesSource.ts +91 -0
- package/src/viewer/sourceHeader.test.ts +2 -87
- package/src/viewer/sourceHeader.ts +4 -63
- package/src/viewer/sourceNavigation.ts +0 -43
- package/vite.config.ts +1 -0
- package/vite.gist.config.ts +57 -0
- package/src/viewer/TocButton.tsx +0 -27
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
18
|
-
const TOC_LINE_MIN_PX =
|
|
19
|
-
const TOC_LINE_STEP_PX =
|
|
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
|
-
|
|
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
|
-
*
|
|
30
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
"&
|
|
354
|
+
"& ol": {
|
|
277
355
|
listStyle: "none",
|
|
278
356
|
counterReset: "none",
|
|
279
357
|
margin: 0,
|
|
280
358
|
padding: 0,
|
|
281
359
|
},
|
|
282
|
-
"&
|
|
360
|
+
"& ol ol": {
|
|
283
361
|
paddingInlineStart: spacing.value(6),
|
|
284
362
|
},
|
|
285
|
-
"&
|
|
363
|
+
"& ol > li::before": {
|
|
286
364
|
content: "none",
|
|
287
365
|
},
|
|
288
|
-
"&
|
|
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:
|
|
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: "
|
|
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": {
|
package/src/viewer/ViewerApp.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
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({
|