@tanishqkancharla/diffmap 0.3.0 → 0.6.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 +171 -1
- package/index.html +6 -0
- package/package.json +3 -2
- package/src/annotations.ts +17 -1
- package/src/changes.ts +15 -0
- package/src/codeNotes.ts +123 -0
- package/src/errors.ts +5 -0
- package/src/extractDocument.ts +10 -2
- package/src/git.test.ts +13 -1
- package/src/git.ts +15 -2
- package/src/host/HostHome.tsx +22 -6
- package/src/host/home.md +154 -109
- package/src/host/home.test.ts +6 -8
- package/src/parseFence.ts +132 -23
- package/src/parseViewer.test.ts +359 -2
- package/src/parseViewer.ts +126 -25
- package/src/serve.test.ts +22 -1
- package/src/services.ts +396 -0
- package/src/share.ts +1 -1
- package/src/viewer/CallStackDiff.tsx +119 -55
- package/src/viewer/ChangesCard.tsx +133 -45
- package/src/viewer/CodeBlock.tsx +204 -0
- package/src/viewer/ComarkView.tsx +34 -0
- package/src/viewer/Fence.tsx +26 -2
- package/src/viewer/HtmlBlock.tsx +2 -1
- package/src/viewer/MermaidBlock.tsx +55 -14
- package/src/viewer/ServiceDiagramBlock.tsx +358 -0
- package/src/viewer/ServiceStage.tsx +1129 -0
- package/src/viewer/SourceDiffPanel.tsx +147 -48
- package/src/viewer/TableOfContents.tsx +47 -18
- package/src/viewer/TocButton.tsx +28 -0
- package/src/viewer/ViewerApp.tsx +28 -3
- package/src/viewer/diagramDiff.ts +69 -0
- package/src/viewer/diagramLinks.ts +54 -6
- package/src/viewer/notes.tsx +126 -0
- package/src/viewer/pierre.ts +20 -2
- package/src/viewer/serviceLayout.ts +200 -0
- package/vite.config.ts +5 -0
- package/vite.gist.config.ts +1 -0
|
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState, type MouseEvent } from "react";
|
|
|
2
2
|
import { CodeView, type CodeViewHandle } from "@pierre/diffs/react";
|
|
3
3
|
import type { CodeViewOptions, CodeViewLineSelection } from "@pierre/diffs";
|
|
4
4
|
import {
|
|
5
|
+
ActivityIndicator,
|
|
5
6
|
backgroundColor,
|
|
6
7
|
border,
|
|
7
8
|
borderColor,
|
|
@@ -9,7 +10,11 @@ import {
|
|
|
9
10
|
Button,
|
|
10
11
|
ChevronDown,
|
|
11
12
|
flex,
|
|
13
|
+
motion,
|
|
14
|
+
radius,
|
|
15
|
+
shadow,
|
|
12
16
|
spacing,
|
|
17
|
+
Text,
|
|
13
18
|
text,
|
|
14
19
|
useTheme,
|
|
15
20
|
} from "maui";
|
|
@@ -19,7 +24,12 @@ import {
|
|
|
19
24
|
type SourceAnnotation,
|
|
20
25
|
type SourceReference,
|
|
21
26
|
} from "../annotations.js";
|
|
22
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
changedFileItem,
|
|
29
|
+
orderForReview,
|
|
30
|
+
type Changes,
|
|
31
|
+
type Rev,
|
|
32
|
+
} from "../changes.js";
|
|
23
33
|
import type { SourceDefinition } from "../definitions.js";
|
|
24
34
|
import { useGistPin } from "../gist/pin.ts";
|
|
25
35
|
import { useGitHubPin } from "../github/pin.ts";
|
|
@@ -40,12 +50,25 @@ export type SourceSelection = {
|
|
|
40
50
|
type DefinitionNavigation = {
|
|
41
51
|
selection: SourceSelection | undefined;
|
|
42
52
|
history: SourceDefinition[];
|
|
43
|
-
|
|
53
|
+
/** A definition request in flight, or the error it ended with. */
|
|
54
|
+
request: "finding" | Error | undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Everything the panel renders from; it keeps the last loaded page while the next one loads. */
|
|
58
|
+
type PanelPage = {
|
|
59
|
+
rev: Rev;
|
|
60
|
+
review: string[];
|
|
61
|
+
selection: SourceSelection | undefined;
|
|
62
|
+
history: SourceDefinition[];
|
|
63
|
+
changes: Changes | Error | undefined;
|
|
64
|
+
source: SourceDefinition | Error | undefined;
|
|
44
65
|
};
|
|
45
66
|
|
|
46
67
|
export function SourceDiffPanel(props: {
|
|
47
68
|
/** The changes to show; references with their own rev open that rev instead. */
|
|
48
69
|
rev: Rev;
|
|
70
|
+
/** Paths to show first, in order, as the rev's changes card lists them. */
|
|
71
|
+
review: string[];
|
|
49
72
|
/** Local viewers pick other changes with the range picker. */
|
|
50
73
|
onRevChange?: (rev: Rev) => void;
|
|
51
74
|
selection: SourceSelection | undefined;
|
|
@@ -57,47 +80,81 @@ export function SourceDiffPanel(props: {
|
|
|
57
80
|
const [navigation, setNavigation] = useState<DefinitionNavigation>({
|
|
58
81
|
selection: props.selection,
|
|
59
82
|
history: [],
|
|
60
|
-
|
|
83
|
+
request: undefined,
|
|
61
84
|
});
|
|
62
85
|
if (navigation.selection !== props.selection) {
|
|
63
86
|
setNavigation({
|
|
64
87
|
selection: props.selection,
|
|
65
88
|
history: [],
|
|
66
|
-
|
|
89
|
+
request: undefined,
|
|
67
90
|
});
|
|
68
91
|
}
|
|
69
|
-
const { history } = navigation;
|
|
70
92
|
const request = useRef(0);
|
|
93
|
+
const hostFiles = useHostFiles();
|
|
94
|
+
const nextChanges = useChanges(props.rev);
|
|
95
|
+
const nextReference = props.selection?.reference;
|
|
96
|
+
// A rev-less reference resolves against current files; a commit reference names its lines directly.
|
|
97
|
+
const nextSource = useSourceReference(
|
|
98
|
+
nextReference?.kind === "file" && nextReference.rev === undefined
|
|
99
|
+
? nextReference
|
|
100
|
+
: undefined,
|
|
101
|
+
);
|
|
102
|
+
const loading =
|
|
103
|
+
nextChanges === undefined
|
|
104
|
+
? "Loading changes…"
|
|
105
|
+
: nextReference?.kind === "file" &&
|
|
106
|
+
nextReference.rev === undefined &&
|
|
107
|
+
nextSource === undefined &&
|
|
108
|
+
(mode === "local" || mode === "github" || hostFiles !== undefined)
|
|
109
|
+
? "Opening source…"
|
|
110
|
+
: navigation.request === "finding"
|
|
111
|
+
? "Finding definition…"
|
|
112
|
+
: undefined;
|
|
113
|
+
const next: PanelPage = {
|
|
114
|
+
rev: props.rev,
|
|
115
|
+
review: props.review,
|
|
116
|
+
selection: props.selection,
|
|
117
|
+
history: navigation.history,
|
|
118
|
+
changes: nextChanges,
|
|
119
|
+
source: nextSource,
|
|
120
|
+
};
|
|
121
|
+
// The new page replaces the old one only once it has loaded, so nothing half-loaded shows in between.
|
|
122
|
+
const [page, setPage] = useState(next);
|
|
123
|
+
if (
|
|
124
|
+
loading === undefined &&
|
|
125
|
+
(page.rev !== next.rev ||
|
|
126
|
+
page.review !== next.review ||
|
|
127
|
+
page.selection !== next.selection ||
|
|
128
|
+
page.history !== next.history ||
|
|
129
|
+
page.changes !== next.changes ||
|
|
130
|
+
page.source !== next.source)
|
|
131
|
+
) {
|
|
132
|
+
setPage(next);
|
|
133
|
+
}
|
|
134
|
+
const { history, selection, changes, source, review } = page;
|
|
71
135
|
const definition = history.at(-1);
|
|
72
136
|
const panel = useStyles(styles.panel);
|
|
73
|
-
const header = useStyles(styles.header);
|
|
137
|
+
const header = useStyles(styles.header, styles.dimmed);
|
|
74
138
|
const titleRow = useStyles(styles.titleRow);
|
|
75
139
|
const titleClass = useStyles(styles.title);
|
|
76
140
|
const pinClass = useStyles(styles.pin);
|
|
77
141
|
const links = useStyles(styles.links);
|
|
78
142
|
const code = useStyles(styles.code);
|
|
79
143
|
const summary = useStyles(styles.summary);
|
|
80
|
-
const codeWrap = useStyles(styles.codeWrap);
|
|
144
|
+
const codeWrap = useStyles(styles.codeWrap, styles.dimmed);
|
|
81
145
|
const collapseIcon = useStyles(styles.collapseIcon);
|
|
82
146
|
const github = useGitHubPin();
|
|
83
147
|
const gist = useGistPin();
|
|
84
|
-
const
|
|
85
|
-
const
|
|
148
|
+
const loadingClass = useStyles(styles.loading);
|
|
149
|
+
const spinner = useStyles(styles.spinner);
|
|
86
150
|
const reference = selection?.reference;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
()
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
);
|
|
95
|
-
// A rev-less reference resolves against current files; a commit reference names its lines directly.
|
|
96
|
-
const source = useSourceReference(
|
|
97
|
-
reference?.kind === "file" && reference.rev === undefined
|
|
98
|
-
? reference
|
|
99
|
-
: undefined,
|
|
100
|
-
);
|
|
151
|
+
const items = useMemo(() => {
|
|
152
|
+
if (changes === undefined || changes instanceof Error) return [];
|
|
153
|
+
const { reviewed, other } = orderForReview(changes.files, review);
|
|
154
|
+
return [...reviewed, ...other].flatMap(
|
|
155
|
+
(changed) => changedFileItem(changed) ?? [],
|
|
156
|
+
);
|
|
157
|
+
}, [changes, review]);
|
|
101
158
|
const resolvedFile = source instanceof Error ? undefined : source;
|
|
102
159
|
const inChanges =
|
|
103
160
|
resolvedFile !== undefined &&
|
|
@@ -108,25 +165,16 @@ export function SourceDiffPanel(props: {
|
|
|
108
165
|
? resolvedFile
|
|
109
166
|
: undefined);
|
|
110
167
|
const status =
|
|
111
|
-
navigation.
|
|
112
|
-
? navigation.
|
|
168
|
+
navigation.request instanceof Error
|
|
169
|
+
? navigation.request.message
|
|
113
170
|
: source instanceof Error
|
|
114
171
|
? source.message
|
|
115
172
|
: // A shown file replaces the changes, so their error is moot, as in a gist without uncommitted changes.
|
|
116
173
|
changes instanceof Error && file === undefined
|
|
117
174
|
? changes.message
|
|
118
|
-
: changes === undefined
|
|
119
|
-
? "
|
|
120
|
-
:
|
|
121
|
-
reference.rev === undefined &&
|
|
122
|
-
source === undefined &&
|
|
123
|
-
(mode === "local" ||
|
|
124
|
-
mode === "github" ||
|
|
125
|
-
hostFiles !== undefined)
|
|
126
|
-
? "Opening source…"
|
|
127
|
-
: items.length === 0 && file === undefined
|
|
128
|
-
? "No changes."
|
|
129
|
-
: undefined;
|
|
175
|
+
: changes !== undefined && items.length === 0 && file === undefined
|
|
176
|
+
? "No changes."
|
|
177
|
+
: undefined;
|
|
130
178
|
const selectedLines = useMemo<CodeViewLineSelection | null>(() => {
|
|
131
179
|
if (file !== undefined)
|
|
132
180
|
return { id: file.path, range: { start: file.start, end: file.end } };
|
|
@@ -153,9 +201,9 @@ export function SourceDiffPanel(props: {
|
|
|
153
201
|
rev: Rev;
|
|
154
202
|
selection: SourceSelection | undefined;
|
|
155
203
|
paths: ReadonlySet<string>;
|
|
156
|
-
}>({ rev:
|
|
157
|
-
if (collapse.rev !==
|
|
158
|
-
setCollapse({ rev:
|
|
204
|
+
}>({ rev: page.rev, selection, paths: new Set() });
|
|
205
|
+
if (collapse.rev !== page.rev || collapse.selection !== selection) {
|
|
206
|
+
setCollapse({ rev: page.rev, selection, paths: new Set() });
|
|
159
207
|
}
|
|
160
208
|
const rangeId = selectedLines?.id;
|
|
161
209
|
const rangeStart = selectedLines?.range.start;
|
|
@@ -297,28 +345,33 @@ export function SourceDiffPanel(props: {
|
|
|
297
345
|
text: lineText,
|
|
298
346
|
});
|
|
299
347
|
const currentRequest = ++request.current;
|
|
300
|
-
setNavigation({ selection, history,
|
|
348
|
+
setNavigation({ selection, history, request: "finding" });
|
|
301
349
|
// oxlint-disable-next-line typescript/no-floating-promises -- Token callbacks cannot await; the request owns its status update.
|
|
302
350
|
void requestSource("definition", params).then((result) => {
|
|
303
351
|
if (currentRequest !== request.current) return;
|
|
304
352
|
setNavigation((current) => {
|
|
305
353
|
if (current.selection !== selection) return current;
|
|
306
|
-
if (result instanceof Error)
|
|
307
|
-
return { ...current, status: result.message };
|
|
354
|
+
if (result instanceof Error) return { ...current, request: result };
|
|
308
355
|
return {
|
|
309
356
|
selection,
|
|
310
357
|
history: [...history, result],
|
|
311
|
-
|
|
358
|
+
request: undefined,
|
|
312
359
|
};
|
|
313
360
|
});
|
|
314
361
|
});
|
|
315
362
|
};
|
|
316
363
|
|
|
317
364
|
return (
|
|
318
|
-
<aside
|
|
365
|
+
<aside
|
|
366
|
+
id="source-diff-panel"
|
|
367
|
+
aria-label="Source changes"
|
|
368
|
+
aria-busy={loading !== undefined}
|
|
369
|
+
className={panel}
|
|
370
|
+
>
|
|
319
371
|
<div
|
|
320
372
|
className={header}
|
|
321
373
|
data-diffmap-kind="source-header"
|
|
374
|
+
data-loading={loading !== undefined}
|
|
322
375
|
// Pierre's file headers draw their own top border, which divides them from this header.
|
|
323
376
|
data-file-headers={!showTitle && items.length > 0}
|
|
324
377
|
>
|
|
@@ -334,7 +387,11 @@ export function SourceDiffPanel(props: {
|
|
|
334
387
|
props.onRevChange === undefined &&
|
|
335
388
|
loadedChanges !== undefined && (
|
|
336
389
|
<div className={titleRow}>
|
|
337
|
-
{
|
|
390
|
+
{selection?.annotation.title !== undefined ? (
|
|
391
|
+
<div className={titleClass} title={selection.annotation.title}>
|
|
392
|
+
{selection.annotation.title}
|
|
393
|
+
</div>
|
|
394
|
+
) : loadedChanges.url === undefined ? (
|
|
338
395
|
<div className={titleClass} title={loadedChanges.title}>
|
|
339
396
|
{loadedChanges.title}
|
|
340
397
|
</div>
|
|
@@ -362,7 +419,7 @@ export function SourceDiffPanel(props: {
|
|
|
362
419
|
setNavigation({
|
|
363
420
|
selection,
|
|
364
421
|
history: history.slice(0, -1),
|
|
365
|
-
|
|
422
|
+
request: undefined,
|
|
366
423
|
});
|
|
367
424
|
}}
|
|
368
425
|
>
|
|
@@ -414,7 +471,13 @@ export function SourceDiffPanel(props: {
|
|
|
414
471
|
)}
|
|
415
472
|
</div>
|
|
416
473
|
{/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- Pierre's file headers are the click targets; this only catches their clicks. */}
|
|
417
|
-
<div
|
|
474
|
+
<div
|
|
475
|
+
className={codeWrap}
|
|
476
|
+
data-loading={loading !== undefined}
|
|
477
|
+
// The last page stays visible while the next one loads, but it is no longer the one to act on.
|
|
478
|
+
inert={loading !== undefined}
|
|
479
|
+
onClick={onHeaderClick}
|
|
480
|
+
>
|
|
418
481
|
<CodeView
|
|
419
482
|
ref={viewer}
|
|
420
483
|
className={code}
|
|
@@ -455,6 +518,19 @@ export function SourceDiffPanel(props: {
|
|
|
455
518
|
)}
|
|
456
519
|
/>
|
|
457
520
|
</div>
|
|
521
|
+
<div
|
|
522
|
+
className={loadingClass}
|
|
523
|
+
role="status"
|
|
524
|
+
data-loading={loading !== undefined}
|
|
525
|
+
>
|
|
526
|
+
{loading !== undefined && (
|
|
527
|
+
// Inline in the label, the spinner sits on the text's baseline at its size, so it lines up in any font.
|
|
528
|
+
<Text size="sm" color="lowContrast">
|
|
529
|
+
<ActivityIndicator className={spinner} />
|
|
530
|
+
{loading}
|
|
531
|
+
</Text>
|
|
532
|
+
)}
|
|
533
|
+
</div>
|
|
458
534
|
</aside>
|
|
459
535
|
);
|
|
460
536
|
}
|
|
@@ -465,6 +541,7 @@ const panelBackgroundCss = `:host { --diffs-light-bg: ${backgroundColor.app}; --
|
|
|
465
541
|
|
|
466
542
|
const styles = {
|
|
467
543
|
panel: style(flex({ direction: "column" }), border(["left"], "border"), {
|
|
544
|
+
position: "relative",
|
|
468
545
|
gridArea: "diff",
|
|
469
546
|
minWidth: 0,
|
|
470
547
|
minHeight: 0,
|
|
@@ -524,6 +601,28 @@ const styles = {
|
|
|
524
601
|
minWidth: 0,
|
|
525
602
|
minHeight: 0,
|
|
526
603
|
}),
|
|
604
|
+
// Loads that finish within the delay swap pages without dimming.
|
|
605
|
+
dimmed: style(motion.standard("opacity"), {
|
|
606
|
+
"&[data-loading='true']": { opacity: 0.4, transitionDelay: "150ms" },
|
|
607
|
+
}),
|
|
608
|
+
loading: style(
|
|
609
|
+
radius.md,
|
|
610
|
+
shadow.medium,
|
|
611
|
+
spacing.padding({ x: 6, y: 3 }),
|
|
612
|
+
motion.standard("opacity"),
|
|
613
|
+
{
|
|
614
|
+
position: "absolute",
|
|
615
|
+
top: "50%",
|
|
616
|
+
left: "50%",
|
|
617
|
+
transform: "translate(-50%, -50%)",
|
|
618
|
+
whiteSpace: "nowrap",
|
|
619
|
+
backgroundColor: backgroundColor.app,
|
|
620
|
+
pointerEvents: "none",
|
|
621
|
+
opacity: 0,
|
|
622
|
+
"&[data-loading='true']": { opacity: 1, transitionDelay: "150ms" },
|
|
623
|
+
},
|
|
624
|
+
),
|
|
625
|
+
spinner: style({ marginRight: spacing.value(4) }),
|
|
527
626
|
collapseIcon: style({
|
|
528
627
|
display: "flex",
|
|
529
628
|
color: colors.gray[11],
|
|
@@ -10,6 +10,7 @@ import { motion, useReducedMotion, type Transition } from "motion/react";
|
|
|
10
10
|
import {
|
|
11
11
|
background,
|
|
12
12
|
colors,
|
|
13
|
+
Drawer,
|
|
13
14
|
focusRing,
|
|
14
15
|
motionEasing,
|
|
15
16
|
overlayMotion,
|
|
@@ -67,6 +68,9 @@ export function tocLineWidth(level: number, shallowestLevel: number) {
|
|
|
67
68
|
export function TableOfContents(props: {
|
|
68
69
|
headings: ViewerHeading[];
|
|
69
70
|
articleRef: RefObject<HTMLElement | null>;
|
|
71
|
+
/** The narrow-screen drawer, opened from the header's TOC button. */
|
|
72
|
+
drawerOpen: boolean;
|
|
73
|
+
onDrawerOpenChange: (open: boolean) => void;
|
|
70
74
|
}) {
|
|
71
75
|
const headings = tocHeadings(props.headings);
|
|
72
76
|
const items = nestHeadings(headings);
|
|
@@ -78,6 +82,7 @@ export function TableOfContents(props: {
|
|
|
78
82
|
const docked = useDocked(navRef);
|
|
79
83
|
const railClass = useStyles(styles.rail);
|
|
80
84
|
const dockedClass = useStyles(styles.docked);
|
|
85
|
+
const drawerClass = useStyles(styles.drawer);
|
|
81
86
|
const listClass = useStyles(styles.list);
|
|
82
87
|
|
|
83
88
|
if (items.length === 0) return undefined;
|
|
@@ -92,24 +97,43 @@ export function TableOfContents(props: {
|
|
|
92
97
|
);
|
|
93
98
|
|
|
94
99
|
return (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{list
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
<>
|
|
101
|
+
<nav
|
|
102
|
+
ref={navRef}
|
|
103
|
+
id="diffmap-toc"
|
|
104
|
+
className={railClass}
|
|
105
|
+
aria-label="Table of contents"
|
|
106
|
+
data-diffmap-kind="toc"
|
|
107
|
+
data-docked={docked ? "true" : "false"}
|
|
108
|
+
>
|
|
109
|
+
{docked ? (
|
|
110
|
+
<div className={dockedClass} data-diffmap-kind="toc-list">
|
|
111
|
+
{list}
|
|
112
|
+
</div>
|
|
113
|
+
) : (
|
|
114
|
+
<TocRail headings={headings} activeId={activeId}>
|
|
115
|
+
{list}
|
|
116
|
+
</TocRail>
|
|
117
|
+
)}
|
|
118
|
+
</nav>
|
|
119
|
+
<Drawer
|
|
120
|
+
isOpen={props.drawerOpen}
|
|
121
|
+
onOpenChange={props.onDrawerOpenChange}
|
|
122
|
+
aria-label="Table of contents"
|
|
123
|
+
>
|
|
124
|
+
<nav className={drawerClass} aria-label="Table of contents">
|
|
125
|
+
<TocList
|
|
126
|
+
items={items}
|
|
127
|
+
className={listClass}
|
|
128
|
+
activeId={activeId}
|
|
129
|
+
onSelect={(id) => {
|
|
130
|
+
setActiveId(id);
|
|
131
|
+
props.onDrawerOpenChange(false);
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
</nav>
|
|
135
|
+
</Drawer>
|
|
136
|
+
</>
|
|
113
137
|
);
|
|
114
138
|
}
|
|
115
139
|
|
|
@@ -390,6 +414,9 @@ const styles = {
|
|
|
390
414
|
justifyContent: "center",
|
|
391
415
|
alignItems: "flex-start",
|
|
392
416
|
pointerEvents: "none",
|
|
417
|
+
"@media (max-width: 600px)": {
|
|
418
|
+
display: "none",
|
|
419
|
+
},
|
|
393
420
|
}),
|
|
394
421
|
cluster: style(spacing.padding({ top: 4, bottom: 4, left: 2, right: 6 }), {
|
|
395
422
|
position: "relative",
|
|
@@ -455,6 +482,8 @@ const styles = {
|
|
|
455
482
|
pointerEvents: "auto",
|
|
456
483
|
...tocListRules,
|
|
457
484
|
}),
|
|
485
|
+
// The same list in Maui's Drawer, which supplies the surface and scrolling.
|
|
486
|
+
drawer: style(spacing.padding({ x: 3, y: 3 }), tocListRules),
|
|
458
487
|
list: style(text({ size: "sm", fontWeight: 400, color: "lowContrast" })),
|
|
459
488
|
item: style({
|
|
460
489
|
"&::before": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Button, UnorderedList } from "maui";
|
|
2
|
+
import { style, useStyles } from "purse-styles";
|
|
3
|
+
|
|
4
|
+
/** Opens the table of contents on narrow screens, where the gutter rail is hidden. */
|
|
5
|
+
export function TocButton(props: { expanded: boolean; onClick: () => void }) {
|
|
6
|
+
const className = useStyles(styles.button);
|
|
7
|
+
return (
|
|
8
|
+
<Button
|
|
9
|
+
className={className}
|
|
10
|
+
variant="quiet"
|
|
11
|
+
aria-label="Table of contents"
|
|
12
|
+
aria-haspopup="dialog"
|
|
13
|
+
aria-expanded={props.expanded}
|
|
14
|
+
onClick={props.onClick}
|
|
15
|
+
>
|
|
16
|
+
<UnorderedList size="sm" />
|
|
17
|
+
</Button>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const styles = {
|
|
22
|
+
button: style({
|
|
23
|
+
flexShrink: 0,
|
|
24
|
+
"@media (min-width: 601px)": {
|
|
25
|
+
display: "none",
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
};
|
package/src/viewer/ViewerApp.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import { ComarkView } from "./ComarkView.tsx";
|
|
|
19
19
|
import { SourceDiffPanel, type SourceSelection } from "./SourceDiffPanel.js";
|
|
20
20
|
import { CloseServerButton } from "./CloseServerButton.tsx";
|
|
21
21
|
import { DiffButton } from "./DiffButton.tsx";
|
|
22
|
+
import { TocButton } from "./TocButton.tsx";
|
|
22
23
|
import {
|
|
23
24
|
hasTableOfContents,
|
|
24
25
|
TableOfContents,
|
|
@@ -45,7 +46,7 @@ export function ViewerApp(props: {
|
|
|
45
46
|
const local = props.mode === "local";
|
|
46
47
|
const documentRev =
|
|
47
48
|
props.defaultRev ??
|
|
48
|
-
viewerDocument.
|
|
49
|
+
viewerDocument.changeCards[0]?.rev ??
|
|
49
50
|
viewerDocument.references.find((ref) => ref.rev !== undefined)?.rev ??
|
|
50
51
|
(local ? ALL : WORKING);
|
|
51
52
|
const [panelRev, setPanelRev] = useState(documentRev);
|
|
@@ -58,9 +59,14 @@ export function ViewerApp(props: {
|
|
|
58
59
|
const hasSourceDiffs =
|
|
59
60
|
local ||
|
|
60
61
|
viewerDocument.references.length > 0 ||
|
|
61
|
-
viewerDocument.
|
|
62
|
+
viewerDocument.changeCards.length > 0 ||
|
|
62
63
|
props.defaultRev !== undefined;
|
|
63
64
|
const diffPanelOpen = hasSourceDiffs && showDiffPanel;
|
|
65
|
+
// The panel lists files in the order the rev's changes card asks them to be reviewed.
|
|
66
|
+
const panelReview =
|
|
67
|
+
viewerDocument.changeCards.find(
|
|
68
|
+
(card) => card.rev === panelRev && card.review.length > 0,
|
|
69
|
+
)?.review ?? NO_REVIEW;
|
|
64
70
|
const body = useStyles(styles.body);
|
|
65
71
|
const [shutDown, setShutDown] = useState(false);
|
|
66
72
|
const parsedTitle =
|
|
@@ -82,6 +88,7 @@ export function ViewerApp(props: {
|
|
|
82
88
|
const stage = useStyles(styles.stage);
|
|
83
89
|
const articleRef = useRef<HTMLElement>(null);
|
|
84
90
|
const hasToc = hasTableOfContents(viewerDocument.headings);
|
|
91
|
+
const [tocDrawerOpen, setTocDrawerOpen] = useState(false);
|
|
85
92
|
useEffect(() => {
|
|
86
93
|
document.title = title;
|
|
87
94
|
}, [title]);
|
|
@@ -108,6 +115,12 @@ export function ViewerApp(props: {
|
|
|
108
115
|
<div className={shell}>
|
|
109
116
|
<header className={header}>
|
|
110
117
|
<div className={heading}>
|
|
118
|
+
{hasToc && (
|
|
119
|
+
<TocButton
|
|
120
|
+
expanded={tocDrawerOpen}
|
|
121
|
+
onClick={() => setTocDrawerOpen(true)}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
111
124
|
<div className={titleClass}>{title}</div>
|
|
112
125
|
</div>
|
|
113
126
|
<div className={actions}>
|
|
@@ -150,6 +163,8 @@ export function ViewerApp(props: {
|
|
|
150
163
|
<TableOfContents
|
|
151
164
|
headings={viewerDocument.headings}
|
|
152
165
|
articleRef={articleRef}
|
|
166
|
+
drawerOpen={tocDrawerOpen}
|
|
167
|
+
onDrawerOpenChange={setTocDrawerOpen}
|
|
153
168
|
/>
|
|
154
169
|
)}
|
|
155
170
|
</div>
|
|
@@ -157,6 +172,7 @@ export function ViewerApp(props: {
|
|
|
157
172
|
{diffPanelOpen && (
|
|
158
173
|
<SourceDiffPanel
|
|
159
174
|
rev={panelRev}
|
|
175
|
+
review={panelReview}
|
|
160
176
|
onRevChange={
|
|
161
177
|
local
|
|
162
178
|
? (rev) => {
|
|
@@ -177,6 +193,8 @@ export function ViewerApp(props: {
|
|
|
177
193
|
);
|
|
178
194
|
}
|
|
179
195
|
|
|
196
|
+
const NO_REVIEW: string[] = [];
|
|
197
|
+
|
|
180
198
|
function useViewerMeta(enabled: boolean) {
|
|
181
199
|
const [meta, setMeta] = useState<ViewerMeta>();
|
|
182
200
|
useEffect(() => {
|
|
@@ -265,6 +283,10 @@ const styles = {
|
|
|
265
283
|
backgroundColor: backgroundColor.app,
|
|
266
284
|
// Scrollport size for the sticky gutter rail (`100cqh`).
|
|
267
285
|
containerType: "size",
|
|
286
|
+
"@media (max-width: 600px)": {
|
|
287
|
+
paddingInline: spacing.value(6),
|
|
288
|
+
paddingBlock: spacing.value(8),
|
|
289
|
+
},
|
|
268
290
|
}),
|
|
269
291
|
prose: style({
|
|
270
292
|
display: "grid",
|
|
@@ -273,7 +295,10 @@ const styles = {
|
|
|
273
295
|
maxWidth: "none",
|
|
274
296
|
minWidth: 0,
|
|
275
297
|
"&[data-has-toc='true']": {
|
|
276
|
-
gridTemplateColumns: `minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr) minmax(0, ${proseMaxWidth}) minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr)`,
|
|
298
|
+
gridTemplateColumns: `var(--diffmap-toc-columns, minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr) minmax(0, ${proseMaxWidth}) minmax(${String(TOC_SIDE_MARGIN_PX)}px, 1fr))`,
|
|
299
|
+
},
|
|
300
|
+
"@media (max-width: 600px)": {
|
|
301
|
+
"--diffmap-toc-columns": "0 minmax(0, 1fr) 0",
|
|
277
302
|
},
|
|
278
303
|
}),
|
|
279
304
|
content: style({
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const edgeSelector =
|
|
2
|
+
".edge, .edge-label, .message, .class-relationship, .er-relationship";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Marks what changed in a `mermaid diff`. `svg` draws every line; `today`
|
|
6
|
+
* and `proposed` draw each side alone. A node or edge missing from Proposed
|
|
7
|
+
* gets data-change="-", one missing from Today "+", and a node whose label
|
|
8
|
+
* differs between the sides "~".
|
|
9
|
+
*/
|
|
10
|
+
export function markDiagramChanges(
|
|
11
|
+
svg: string,
|
|
12
|
+
sides: { today: string; proposed: string },
|
|
13
|
+
) {
|
|
14
|
+
const document = parse(svg);
|
|
15
|
+
const today = parse(sides.today);
|
|
16
|
+
const proposed = parse(sides.proposed);
|
|
17
|
+
|
|
18
|
+
const todayNodes = nodeLabels(today);
|
|
19
|
+
const proposedNodes = nodeLabels(proposed);
|
|
20
|
+
for (const node of document.querySelectorAll("[data-id]")) {
|
|
21
|
+
const id = node.getAttribute("data-id");
|
|
22
|
+
if (!proposedNodes.has(id)) node.setAttribute("data-change", "-");
|
|
23
|
+
else if (!todayNodes.has(id)) node.setAttribute("data-change", "+");
|
|
24
|
+
else if (todayNodes.get(id) !== proposedNodes.get(id))
|
|
25
|
+
node.setAttribute("data-change", "~");
|
|
26
|
+
}
|
|
27
|
+
// A removed sequence participant takes its lifeline with it.
|
|
28
|
+
for (const lifeline of document.querySelectorAll("[data-actor]")) {
|
|
29
|
+
const id = lifeline.getAttribute("data-actor");
|
|
30
|
+
if (!proposedNodes.has(id)) lifeline.setAttribute("data-change", "-");
|
|
31
|
+
else if (!todayNodes.has(id)) lifeline.setAttribute("data-change", "+");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const todayEdges = edgeKeys(today);
|
|
35
|
+
const proposedEdges = edgeKeys(proposed);
|
|
36
|
+
for (const edge of document.querySelectorAll(edgeSelector)) {
|
|
37
|
+
const key = edgeKey(edge);
|
|
38
|
+
if (!proposedEdges.has(key)) edge.setAttribute("data-change", "-");
|
|
39
|
+
else if (!todayEdges.has(key)) edge.setAttribute("data-change", "+");
|
|
40
|
+
}
|
|
41
|
+
return document.documentElement.outerHTML;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parse(svg: string) {
|
|
45
|
+
return new DOMParser().parseFromString(svg, "image/svg+xml");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nodeLabels(document: Document) {
|
|
49
|
+
return new Map(
|
|
50
|
+
[...document.querySelectorAll("[data-id]")].map((node) => [
|
|
51
|
+
node.getAttribute("data-id"),
|
|
52
|
+
node.getAttribute("data-label"),
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** An edge, edge label, or message by its kind, ends, and label. */
|
|
58
|
+
function edgeKey(edge: Element) {
|
|
59
|
+
return [
|
|
60
|
+
edge.getAttribute("class"),
|
|
61
|
+
edge.getAttribute("data-from"),
|
|
62
|
+
edge.getAttribute("data-to"),
|
|
63
|
+
edge.getAttribute("data-label") ?? "",
|
|
64
|
+
].join("\u0000");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function edgeKeys(document: Document) {
|
|
68
|
+
return new Set([...document.querySelectorAll(edgeSelector)].map(edgeKey));
|
|
69
|
+
}
|