@tanishqkancharla/diffmap 0.1.4 → 0.2.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 +69 -61
- package/index.html +18 -0
- package/package.json +7 -5
- package/src/annotations.ts +36 -29
- package/src/changes.ts +229 -0
- package/src/cli.test.ts +50 -0
- package/src/cli.ts +41 -5
- package/src/errors.ts +10 -0
- package/src/gist/GistApp.tsx +39 -20
- package/src/gist/GistStatus.tsx +2 -51
- package/src/gist/fetchGist.ts +23 -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 +86 -0
- package/src/host/files.ts +10 -0
- package/src/host/home.md +156 -0
- package/src/host/home.test.ts +83 -0
- package/src/index.ts +1 -0
- package/src/packageVersion.ts +7 -0
- package/src/parseFence.ts +6 -6
- package/src/parseViewer.test.ts +87 -0
- package/src/parseViewer.ts +103 -77
- package/src/serve.test.ts +306 -0
- package/src/serve.ts +48 -5
- package/src/share.ts +18 -0
- package/src/update.test.ts +19 -0
- package/src/update.ts +285 -0
- package/src/viewer/CallStackDiff.tsx +15 -3
- package/src/viewer/ChangesCard.tsx +267 -0
- package/src/viewer/CloseServerButton.tsx +16 -0
- package/src/viewer/ComarkView.tsx +83 -15
- package/src/viewer/Fence.tsx +9 -1
- package/src/viewer/FileExcerpt.tsx +25 -3
- package/src/viewer/HtmlPlaceholder.tsx +6 -1
- package/src/viewer/MermaidBlock.tsx +99 -11
- package/src/viewer/RangePicker.tsx +526 -0
- package/src/viewer/SourceDiffPanel.tsx +277 -121
- package/src/viewer/TableOfContents.tsx +313 -39
- package/src/viewer/ViewerApp.tsx +120 -68
- 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 +33 -42
- package/src/viewer/viewerMode.ts +1 -1
- package/vite.config.ts +2 -1
- package/vite.gist.config.ts +57 -0
- package/src/viewer/Check.tsx +0 -24
- package/src/viewer/DoneButton.tsx +0 -32
|
@@ -1,39 +1,216 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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";
|
|
10
|
+
import {
|
|
11
|
+
background,
|
|
12
|
+
colors,
|
|
13
|
+
focusRing,
|
|
14
|
+
motionEasing,
|
|
15
|
+
overlayMotion,
|
|
16
|
+
radius,
|
|
17
|
+
shadow,
|
|
18
|
+
spacing,
|
|
19
|
+
text,
|
|
20
|
+
} from "maui";
|
|
3
21
|
import { style, useStyles } from "purse-styles";
|
|
4
22
|
import type { ViewerHeading } from "../parseViewer.js";
|
|
5
23
|
|
|
6
24
|
type TocItem = ViewerHeading & { children: TocItem[] };
|
|
7
25
|
|
|
26
|
+
const TOC_LINE_MAX_PX = 12;
|
|
27
|
+
const TOC_LINE_MIN_PX = 3;
|
|
28
|
+
const TOC_LINE_STEP_PX = 3;
|
|
29
|
+
const DWELL_MS = 50;
|
|
30
|
+
const LEAVE_MS = 100;
|
|
31
|
+
const TOC_TRANSITION_MS = 180;
|
|
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
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Equal side gutters so the line rail fits and the article column stays centered. */
|
|
50
|
+
export const TOC_SIDE_MARGIN_PX = 48;
|
|
51
|
+
|
|
52
|
+
/**
|
|
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.
|
|
55
|
+
*/
|
|
56
|
+
const TOC_DOCKED_MIN_GUTTER_PX = 220;
|
|
57
|
+
|
|
58
|
+
export function hasTableOfContents(headings: ViewerHeading[]) {
|
|
59
|
+
return tocHeadings(headings).length > 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function tocLineWidth(level: number, shallowestLevel: number) {
|
|
63
|
+
const depth = Math.max(0, level - shallowestLevel);
|
|
64
|
+
return Math.max(TOC_LINE_MIN_PX, TOC_LINE_MAX_PX - depth * TOC_LINE_STEP_PX);
|
|
65
|
+
}
|
|
66
|
+
|
|
8
67
|
export function TableOfContents(props: {
|
|
9
68
|
headings: ViewerHeading[];
|
|
10
69
|
articleRef: RefObject<HTMLElement | null>;
|
|
11
70
|
}) {
|
|
12
|
-
const
|
|
71
|
+
const headings = tocHeadings(props.headings);
|
|
72
|
+
const items = nestHeadings(headings);
|
|
13
73
|
const { activeId, setActiveId } = useActiveHeading(
|
|
14
74
|
props.headings,
|
|
15
75
|
props.articleRef,
|
|
16
76
|
);
|
|
17
|
-
const
|
|
77
|
+
const navRef = useRef<HTMLElement>(null);
|
|
78
|
+
const docked = useDocked(navRef);
|
|
79
|
+
const railClass = useStyles(styles.rail);
|
|
80
|
+
const dockedClass = useStyles(styles.docked);
|
|
18
81
|
const listClass = useStyles(styles.list);
|
|
82
|
+
|
|
19
83
|
if (items.length === 0) return undefined;
|
|
20
84
|
|
|
85
|
+
const list = (
|
|
86
|
+
<TocList
|
|
87
|
+
items={items}
|
|
88
|
+
className={listClass}
|
|
89
|
+
activeId={activeId}
|
|
90
|
+
onSelect={setActiveId}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
|
|
21
94
|
return (
|
|
22
95
|
<nav
|
|
23
|
-
|
|
96
|
+
ref={navRef}
|
|
97
|
+
id="diffmap-toc"
|
|
98
|
+
className={railClass}
|
|
24
99
|
aria-label="Table of contents"
|
|
25
100
|
data-diffmap-kind="toc"
|
|
101
|
+
data-docked={docked ? "true" : "false"}
|
|
26
102
|
>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
)}
|
|
33
112
|
</nav>
|
|
34
113
|
);
|
|
35
114
|
}
|
|
36
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
|
+
}) {
|
|
122
|
+
const clusterClass = useStyles(styles.cluster);
|
|
123
|
+
const lineClass = useStyles(styles.line);
|
|
124
|
+
const cardClass = useStyles(styles.card);
|
|
125
|
+
const reduceMotion = Boolean(useReducedMotion());
|
|
126
|
+
const [open, setOpen] = useState(false);
|
|
127
|
+
const hoveringRef = useRef(false);
|
|
128
|
+
const dwellTimer = useRef<number | undefined>(undefined);
|
|
129
|
+
const closeTimer = useRef<number | undefined>(undefined);
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const onPageLeave = () => {
|
|
133
|
+
// Leaving the window must close a movement-opened nav, never open one.
|
|
134
|
+
// A left-edge exit used to land in the dwell zone and spring the drawer.
|
|
135
|
+
hoveringRef.current = false;
|
|
136
|
+
window.clearTimeout(dwellTimer.current);
|
|
137
|
+
window.clearTimeout(closeTimer.current);
|
|
138
|
+
dwellTimer.current = undefined;
|
|
139
|
+
closeTimer.current = undefined;
|
|
140
|
+
setOpen(false);
|
|
141
|
+
};
|
|
142
|
+
document.documentElement.addEventListener("mouseleave", onPageLeave);
|
|
143
|
+
return () => {
|
|
144
|
+
document.documentElement.removeEventListener("mouseleave", onPageLeave);
|
|
145
|
+
window.clearTimeout(dwellTimer.current);
|
|
146
|
+
window.clearTimeout(closeTimer.current);
|
|
147
|
+
};
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
const shallowest = props.headings.reduce(
|
|
151
|
+
(min, heading) => Math.min(min, heading.level),
|
|
152
|
+
6,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return (
|
|
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(() => {
|
|
165
|
+
dwellTimer.current = undefined;
|
|
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}
|
|
207
|
+
>
|
|
208
|
+
{props.children}
|
|
209
|
+
</motion.div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
37
214
|
function TocList(props: {
|
|
38
215
|
items: TocItem[];
|
|
39
216
|
className: string;
|
|
@@ -117,6 +294,29 @@ function nestHeadings(headings: ViewerHeading[]): TocItem[] {
|
|
|
117
294
|
return items;
|
|
118
295
|
}
|
|
119
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
|
+
|
|
120
320
|
function useActiveHeading(
|
|
121
321
|
headings: ViewerHeading[],
|
|
122
322
|
articleRef: RefObject<HTMLElement | null>,
|
|
@@ -150,40 +350,110 @@ function useActiveHeading(
|
|
|
150
350
|
return { activeId, setActiveId };
|
|
151
351
|
}
|
|
152
352
|
|
|
353
|
+
const tocListRules = {
|
|
354
|
+
"& ol": {
|
|
355
|
+
listStyle: "none",
|
|
356
|
+
counterReset: "none",
|
|
357
|
+
margin: 0,
|
|
358
|
+
padding: 0,
|
|
359
|
+
},
|
|
360
|
+
"& ol ol": {
|
|
361
|
+
paddingInlineStart: spacing.value(6),
|
|
362
|
+
},
|
|
363
|
+
"& ol > li::before": {
|
|
364
|
+
content: "none",
|
|
365
|
+
},
|
|
366
|
+
"& a": {
|
|
367
|
+
fontWeight: 400,
|
|
368
|
+
textDecoration: "none",
|
|
369
|
+
},
|
|
370
|
+
} as const;
|
|
371
|
+
|
|
153
372
|
const styles = {
|
|
154
|
-
|
|
155
|
-
|
|
373
|
+
rail: style({
|
|
374
|
+
// Sit in the prose grid's left gutter (column 1). Lines share that outer
|
|
375
|
+
// edge and grow inward. Sticky height keeps them in view, off the
|
|
376
|
+
// scrollbar, and on the article when Diff is open.
|
|
156
377
|
gridColumn: "1",
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
378
|
+
gridRow: "1",
|
|
379
|
+
position: "sticky",
|
|
380
|
+
top: 0,
|
|
381
|
+
alignSelf: "start",
|
|
382
|
+
justifySelf: "stretch",
|
|
383
|
+
boxSizing: "border-box",
|
|
384
|
+
width: "100%",
|
|
385
|
+
minWidth: 0,
|
|
386
|
+
height: "100cqh",
|
|
387
|
+
zIndex: 3,
|
|
388
|
+
display: "flex",
|
|
389
|
+
flexDirection: "column",
|
|
390
|
+
justifyContent: "center",
|
|
391
|
+
alignItems: "flex-start",
|
|
392
|
+
pointerEvents: "none",
|
|
393
|
+
}),
|
|
394
|
+
cluster: style(spacing.padding({ top: 4, bottom: 4, left: 2, right: 6 }), {
|
|
395
|
+
position: "relative",
|
|
396
|
+
minWidth: "40px",
|
|
397
|
+
display: "flex",
|
|
398
|
+
flexDirection: "column",
|
|
399
|
+
alignItems: "flex-start",
|
|
400
|
+
gap: "12px",
|
|
401
|
+
pointerEvents: "auto",
|
|
402
|
+
"&[data-open='true'] [data-diffmap-kind='toc-line']": {
|
|
403
|
+
opacity: 0,
|
|
176
404
|
},
|
|
177
|
-
|
|
178
|
-
|
|
405
|
+
}),
|
|
406
|
+
line: style({
|
|
407
|
+
display: "block",
|
|
408
|
+
height: "1px",
|
|
409
|
+
flexShrink: 0,
|
|
410
|
+
borderRadius: "999px",
|
|
411
|
+
backgroundColor: colors.gray[8],
|
|
412
|
+
opacity: 1,
|
|
413
|
+
transition: `opacity ${String(TOC_TRANSITION_MS)}ms ${motionEasing}`,
|
|
414
|
+
"@media (prefers-reduced-motion: reduce)": {
|
|
415
|
+
transition: "none",
|
|
179
416
|
},
|
|
180
|
-
"&[data-
|
|
181
|
-
|
|
417
|
+
"&[data-current='true']": {
|
|
418
|
+
backgroundColor: colors.gray[12],
|
|
182
419
|
},
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
420
|
+
}),
|
|
421
|
+
card: style(
|
|
422
|
+
radius.lg,
|
|
423
|
+
shadow.medium,
|
|
424
|
+
background.element,
|
|
425
|
+
spacing.padding({ x: 3, y: 3 }),
|
|
426
|
+
{
|
|
427
|
+
position: "absolute",
|
|
428
|
+
insetInlineStart: 0,
|
|
429
|
+
top: "50%",
|
|
430
|
+
zIndex: 1,
|
|
431
|
+
boxSizing: "border-box",
|
|
432
|
+
width: "min(240px, calc(100cqw - 24px))",
|
|
433
|
+
maxHeight: "min(32rem, calc(100cqh - 24px))",
|
|
434
|
+
overflowX: "hidden",
|
|
435
|
+
overflowY: "auto",
|
|
436
|
+
overscrollBehavior: "contain",
|
|
437
|
+
transformOrigin: "left center",
|
|
438
|
+
pointerEvents: "none",
|
|
439
|
+
"&[data-open='true']": {
|
|
440
|
+
pointerEvents: "auto",
|
|
441
|
+
},
|
|
442
|
+
...tocListRules,
|
|
186
443
|
},
|
|
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,
|
|
187
457
|
}),
|
|
188
458
|
list: style(text({ size: "sm", fontWeight: 400, color: "lowContrast" })),
|
|
189
459
|
item: style({
|
|
@@ -206,6 +476,10 @@ const styles = {
|
|
|
206
476
|
"&[aria-current='location']": {
|
|
207
477
|
color: colors.accent[9],
|
|
208
478
|
fontWeight: 500,
|
|
479
|
+
backgroundColor: colors.accent[3],
|
|
480
|
+
},
|
|
481
|
+
"&[aria-current='location']:hover": {
|
|
482
|
+
backgroundColor: colors.accent[4],
|
|
209
483
|
},
|
|
210
484
|
},
|
|
211
485
|
),
|