@tanishqkancharla/diffmap 0.4.0 → 0.6.1
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 +78 -4
- package/package.json +1 -1
- package/src/annotations.ts +17 -1
- package/src/extractDocument.ts +10 -2
- package/src/host/home.md +89 -103
- package/src/host/home.test.ts +11 -1
- package/src/parseFence.ts +81 -12
- package/src/parseViewer.test.ts +147 -4
- package/src/parseViewer.ts +44 -9
- package/src/serve.test.ts +22 -1
- package/src/services.ts +122 -15
- package/src/viewer/CallStackDiff.tsx +29 -33
- package/src/viewer/ChangesCard.tsx +0 -2
- package/src/viewer/CodeBlock.tsx +79 -7
- package/src/viewer/ComarkView.tsx +34 -0
- package/src/viewer/Fence.tsx +4 -0
- package/src/viewer/HtmlBlock.tsx +2 -1
- package/src/viewer/MermaidBlock.tsx +55 -14
- package/src/viewer/ServiceDiagramBlock.tsx +90 -54
- package/src/viewer/ServiceStage.tsx +206 -60
- package/src/viewer/SourceDiffPanel.tsx +138 -42
- package/src/viewer/TableOfContents.tsx +44 -18
- package/src/viewer/TocButton.tsx +28 -0
- package/src/viewer/ViewerApp.tsx +10 -0
- package/src/viewer/diagramDiff.ts +69 -0
- package/src/viewer/diagramLinks.ts +54 -6
- package/src/viewer/notes.tsx +0 -1
- package/src/viewer/pierre.ts +20 -2
- package/src/viewer/serviceLayout.ts +16 -5
- package/vite.config.ts +4 -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,7 +50,18 @@ 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: {
|
|
@@ -59,35 +80,74 @@ export function SourceDiffPanel(props: {
|
|
|
59
80
|
const [navigation, setNavigation] = useState<DefinitionNavigation>({
|
|
60
81
|
selection: props.selection,
|
|
61
82
|
history: [],
|
|
62
|
-
|
|
83
|
+
request: undefined,
|
|
63
84
|
});
|
|
64
85
|
if (navigation.selection !== props.selection) {
|
|
65
86
|
setNavigation({
|
|
66
87
|
selection: props.selection,
|
|
67
88
|
history: [],
|
|
68
|
-
|
|
89
|
+
request: undefined,
|
|
69
90
|
});
|
|
70
91
|
}
|
|
71
|
-
const { history } = navigation;
|
|
72
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;
|
|
73
135
|
const definition = history.at(-1);
|
|
74
136
|
const panel = useStyles(styles.panel);
|
|
75
|
-
const header = useStyles(styles.header);
|
|
137
|
+
const header = useStyles(styles.header, styles.dimmed);
|
|
76
138
|
const titleRow = useStyles(styles.titleRow);
|
|
77
139
|
const titleClass = useStyles(styles.title);
|
|
78
140
|
const pinClass = useStyles(styles.pin);
|
|
79
141
|
const links = useStyles(styles.links);
|
|
80
142
|
const code = useStyles(styles.code);
|
|
81
143
|
const summary = useStyles(styles.summary);
|
|
82
|
-
const codeWrap = useStyles(styles.codeWrap);
|
|
144
|
+
const codeWrap = useStyles(styles.codeWrap, styles.dimmed);
|
|
83
145
|
const collapseIcon = useStyles(styles.collapseIcon);
|
|
84
146
|
const github = useGitHubPin();
|
|
85
147
|
const gist = useGistPin();
|
|
86
|
-
const
|
|
87
|
-
const
|
|
148
|
+
const loadingClass = useStyles(styles.loading);
|
|
149
|
+
const spinner = useStyles(styles.spinner);
|
|
88
150
|
const reference = selection?.reference;
|
|
89
|
-
const changes = useChanges(props.rev);
|
|
90
|
-
const { review } = props;
|
|
91
151
|
const items = useMemo(() => {
|
|
92
152
|
if (changes === undefined || changes instanceof Error) return [];
|
|
93
153
|
const { reviewed, other } = orderForReview(changes.files, review);
|
|
@@ -95,12 +155,6 @@ export function SourceDiffPanel(props: {
|
|
|
95
155
|
(changed) => changedFileItem(changed) ?? [],
|
|
96
156
|
);
|
|
97
157
|
}, [changes, review]);
|
|
98
|
-
// A rev-less reference resolves against current files; a commit reference names its lines directly.
|
|
99
|
-
const source = useSourceReference(
|
|
100
|
-
reference?.kind === "file" && reference.rev === undefined
|
|
101
|
-
? reference
|
|
102
|
-
: undefined,
|
|
103
|
-
);
|
|
104
158
|
const resolvedFile = source instanceof Error ? undefined : source;
|
|
105
159
|
const inChanges =
|
|
106
160
|
resolvedFile !== undefined &&
|
|
@@ -111,25 +165,16 @@ export function SourceDiffPanel(props: {
|
|
|
111
165
|
? resolvedFile
|
|
112
166
|
: undefined);
|
|
113
167
|
const status =
|
|
114
|
-
navigation.
|
|
115
|
-
? navigation.
|
|
168
|
+
navigation.request instanceof Error
|
|
169
|
+
? navigation.request.message
|
|
116
170
|
: source instanceof Error
|
|
117
171
|
? source.message
|
|
118
172
|
: // A shown file replaces the changes, so their error is moot, as in a gist without uncommitted changes.
|
|
119
173
|
changes instanceof Error && file === undefined
|
|
120
174
|
? changes.message
|
|
121
|
-
: changes === undefined
|
|
122
|
-
? "
|
|
123
|
-
:
|
|
124
|
-
reference.rev === undefined &&
|
|
125
|
-
source === undefined &&
|
|
126
|
-
(mode === "local" ||
|
|
127
|
-
mode === "github" ||
|
|
128
|
-
hostFiles !== undefined)
|
|
129
|
-
? "Opening source…"
|
|
130
|
-
: items.length === 0 && file === undefined
|
|
131
|
-
? "No changes."
|
|
132
|
-
: undefined;
|
|
175
|
+
: changes !== undefined && items.length === 0 && file === undefined
|
|
176
|
+
? "No changes."
|
|
177
|
+
: undefined;
|
|
133
178
|
const selectedLines = useMemo<CodeViewLineSelection | null>(() => {
|
|
134
179
|
if (file !== undefined)
|
|
135
180
|
return { id: file.path, range: { start: file.start, end: file.end } };
|
|
@@ -156,9 +201,9 @@ export function SourceDiffPanel(props: {
|
|
|
156
201
|
rev: Rev;
|
|
157
202
|
selection: SourceSelection | undefined;
|
|
158
203
|
paths: ReadonlySet<string>;
|
|
159
|
-
}>({ rev:
|
|
160
|
-
if (collapse.rev !==
|
|
161
|
-
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() });
|
|
162
207
|
}
|
|
163
208
|
const rangeId = selectedLines?.id;
|
|
164
209
|
const rangeStart = selectedLines?.range.start;
|
|
@@ -300,28 +345,33 @@ export function SourceDiffPanel(props: {
|
|
|
300
345
|
text: lineText,
|
|
301
346
|
});
|
|
302
347
|
const currentRequest = ++request.current;
|
|
303
|
-
setNavigation({ selection, history,
|
|
348
|
+
setNavigation({ selection, history, request: "finding" });
|
|
304
349
|
// oxlint-disable-next-line typescript/no-floating-promises -- Token callbacks cannot await; the request owns its status update.
|
|
305
350
|
void requestSource("definition", params).then((result) => {
|
|
306
351
|
if (currentRequest !== request.current) return;
|
|
307
352
|
setNavigation((current) => {
|
|
308
353
|
if (current.selection !== selection) return current;
|
|
309
|
-
if (result instanceof Error)
|
|
310
|
-
return { ...current, status: result.message };
|
|
354
|
+
if (result instanceof Error) return { ...current, request: result };
|
|
311
355
|
return {
|
|
312
356
|
selection,
|
|
313
357
|
history: [...history, result],
|
|
314
|
-
|
|
358
|
+
request: undefined,
|
|
315
359
|
};
|
|
316
360
|
});
|
|
317
361
|
});
|
|
318
362
|
};
|
|
319
363
|
|
|
320
364
|
return (
|
|
321
|
-
<aside
|
|
365
|
+
<aside
|
|
366
|
+
id="source-diff-panel"
|
|
367
|
+
aria-label="Source changes"
|
|
368
|
+
aria-busy={loading !== undefined}
|
|
369
|
+
className={panel}
|
|
370
|
+
>
|
|
322
371
|
<div
|
|
323
372
|
className={header}
|
|
324
373
|
data-diffmap-kind="source-header"
|
|
374
|
+
data-loading={loading !== undefined}
|
|
325
375
|
// Pierre's file headers draw their own top border, which divides them from this header.
|
|
326
376
|
data-file-headers={!showTitle && items.length > 0}
|
|
327
377
|
>
|
|
@@ -337,7 +387,11 @@ export function SourceDiffPanel(props: {
|
|
|
337
387
|
props.onRevChange === undefined &&
|
|
338
388
|
loadedChanges !== undefined && (
|
|
339
389
|
<div className={titleRow}>
|
|
340
|
-
{
|
|
390
|
+
{selection?.annotation.title !== undefined ? (
|
|
391
|
+
<div className={titleClass} title={selection.annotation.title}>
|
|
392
|
+
{selection.annotation.title}
|
|
393
|
+
</div>
|
|
394
|
+
) : loadedChanges.url === undefined ? (
|
|
341
395
|
<div className={titleClass} title={loadedChanges.title}>
|
|
342
396
|
{loadedChanges.title}
|
|
343
397
|
</div>
|
|
@@ -365,7 +419,7 @@ export function SourceDiffPanel(props: {
|
|
|
365
419
|
setNavigation({
|
|
366
420
|
selection,
|
|
367
421
|
history: history.slice(0, -1),
|
|
368
|
-
|
|
422
|
+
request: undefined,
|
|
369
423
|
});
|
|
370
424
|
}}
|
|
371
425
|
>
|
|
@@ -417,7 +471,13 @@ export function SourceDiffPanel(props: {
|
|
|
417
471
|
)}
|
|
418
472
|
</div>
|
|
419
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. */}
|
|
420
|
-
<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
|
+
>
|
|
421
481
|
<CodeView
|
|
422
482
|
ref={viewer}
|
|
423
483
|
className={code}
|
|
@@ -458,6 +518,19 @@ export function SourceDiffPanel(props: {
|
|
|
458
518
|
)}
|
|
459
519
|
/>
|
|
460
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>
|
|
461
534
|
</aside>
|
|
462
535
|
);
|
|
463
536
|
}
|
|
@@ -468,6 +541,7 @@ const panelBackgroundCss = `:host { --diffs-light-bg: ${backgroundColor.app}; --
|
|
|
468
541
|
|
|
469
542
|
const styles = {
|
|
470
543
|
panel: style(flex({ direction: "column" }), border(["left"], "border"), {
|
|
544
|
+
position: "relative",
|
|
471
545
|
gridArea: "diff",
|
|
472
546
|
minWidth: 0,
|
|
473
547
|
minHeight: 0,
|
|
@@ -527,6 +601,28 @@ const styles = {
|
|
|
527
601
|
minWidth: 0,
|
|
528
602
|
minHeight: 0,
|
|
529
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) }),
|
|
530
626
|
collapseIcon: style({
|
|
531
627
|
display: "flex",
|
|
532
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
|
|
|
@@ -458,6 +482,8 @@ const styles = {
|
|
|
458
482
|
pointerEvents: "auto",
|
|
459
483
|
...tocListRules,
|
|
460
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),
|
|
461
487
|
list: style(text({ size: "sm", fontWeight: 400, color: "lowContrast" })),
|
|
462
488
|
item: style({
|
|
463
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,
|
|
@@ -87,6 +88,7 @@ export function ViewerApp(props: {
|
|
|
87
88
|
const stage = useStyles(styles.stage);
|
|
88
89
|
const articleRef = useRef<HTMLElement>(null);
|
|
89
90
|
const hasToc = hasTableOfContents(viewerDocument.headings);
|
|
91
|
+
const [tocDrawerOpen, setTocDrawerOpen] = useState(false);
|
|
90
92
|
useEffect(() => {
|
|
91
93
|
document.title = title;
|
|
92
94
|
}, [title]);
|
|
@@ -113,6 +115,12 @@ export function ViewerApp(props: {
|
|
|
113
115
|
<div className={shell}>
|
|
114
116
|
<header className={header}>
|
|
115
117
|
<div className={heading}>
|
|
118
|
+
{hasToc && (
|
|
119
|
+
<TocButton
|
|
120
|
+
expanded={tocDrawerOpen}
|
|
121
|
+
onClick={() => setTocDrawerOpen(true)}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
116
124
|
<div className={titleClass}>{title}</div>
|
|
117
125
|
</div>
|
|
118
126
|
<div className={actions}>
|
|
@@ -155,6 +163,8 @@ export function ViewerApp(props: {
|
|
|
155
163
|
<TableOfContents
|
|
156
164
|
headings={viewerDocument.headings}
|
|
157
165
|
articleRef={articleRef}
|
|
166
|
+
drawerOpen={tocDrawerOpen}
|
|
167
|
+
onDrawerOpenChange={setTocDrawerOpen}
|
|
158
168
|
/>
|
|
159
169
|
)}
|
|
160
170
|
</div>
|
|
@@ -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
|
+
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import { colors } from "maui";
|
|
1
2
|
import type { DiagramAnnotation } from "../diagramAnnotations.js";
|
|
2
3
|
import { DiffmapAnnotationError } from "../errors.js";
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Arrowhead colors for edges that are not plain gray: changed in a diff,
|
|
7
|
+
* hovered, or selected. Later tones win when several apply.
|
|
8
|
+
*/
|
|
9
|
+
const markerTones = [
|
|
10
|
+
["added", "[data-change='+']", colors.green[9]],
|
|
11
|
+
["removed", "[data-change='-']", colors.red[9]],
|
|
12
|
+
["hover", ".source-edge:hover", colors.blue[8]],
|
|
13
|
+
["selected", ".source-edge[aria-pressed='true']", colors.blue[9]],
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
4
16
|
export function linkDiagram(svg: string, links: DiagramAnnotation[]) {
|
|
5
17
|
const document = new DOMParser().parseFromString(svg, "image/svg+xml");
|
|
6
|
-
|
|
7
|
-
if (markerPart.getAttribute("fill") === "var(--_arrow)")
|
|
8
|
-
markerPart.setAttribute("fill", "context-stroke");
|
|
9
|
-
if (markerPart.getAttribute("stroke") === "var(--_arrow)")
|
|
10
|
-
markerPart.setAttribute("stroke", "context-stroke");
|
|
11
|
-
}
|
|
18
|
+
colorMarkers(document);
|
|
12
19
|
const edges = [
|
|
13
20
|
...document.querySelectorAll<SVGElement>(
|
|
14
21
|
".edge, .message, .class-relationship, .er-relationship",
|
|
@@ -72,3 +79,44 @@ export function linkDiagram(svg: string, links: DiagramAnnotation[]) {
|
|
|
72
79
|
}
|
|
73
80
|
return document.documentElement.outerHTML;
|
|
74
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A marker draws in its own colors, not its edge's, and `context-stroke`
|
|
85
|
+
* falls back to black where browsers lack it. So each marker gets one copy
|
|
86
|
+
* per tone, and a style rule swaps an edge to the copy its state needs.
|
|
87
|
+
*/
|
|
88
|
+
function colorMarkers(document: Document) {
|
|
89
|
+
const rules: string[] = [];
|
|
90
|
+
for (const marker of document.querySelectorAll("marker[id]")) {
|
|
91
|
+
const id = marker.id;
|
|
92
|
+
for (const [tone, , color] of markerTones) {
|
|
93
|
+
// SAFETY: cloning an element preserves its element type.
|
|
94
|
+
const copy = marker.cloneNode(true) as Element;
|
|
95
|
+
copy.id = `${id}--${tone}`;
|
|
96
|
+
paintArrow(copy, color);
|
|
97
|
+
marker.after(copy);
|
|
98
|
+
}
|
|
99
|
+
// Mermaid draws arrowheads in the accent color; match the gray line.
|
|
100
|
+
paintArrow(marker, "var(--_line)");
|
|
101
|
+
for (const end of ["start", "end"]) {
|
|
102
|
+
const uses = `[marker-${end}="url(#${id})"]`;
|
|
103
|
+
if (document.querySelector(uses) === null) continue;
|
|
104
|
+
for (const [tone, state] of markerTones)
|
|
105
|
+
rules.push(
|
|
106
|
+
`:is(${state}, ${state} *)${uses} { marker-${end}: url(#${id}--${tone}); }`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (rules.length === 0) return;
|
|
111
|
+
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
|
112
|
+
style.textContent = rules.join("\n");
|
|
113
|
+
document.documentElement.prepend(style);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Repaints a marker's mermaid arrow color. */
|
|
117
|
+
function paintArrow(marker: Element, color: string) {
|
|
118
|
+
for (const part of [marker, ...marker.querySelectorAll("*")])
|
|
119
|
+
for (const paint of ["fill", "stroke"])
|
|
120
|
+
if (part.getAttribute(paint) === "var(--_arrow)")
|
|
121
|
+
part.setAttribute(paint, color);
|
|
122
|
+
}
|
package/src/viewer/notes.tsx
CHANGED
|
@@ -105,7 +105,6 @@ export function NoteText(props: { text: string }) {
|
|
|
105
105
|
|
|
106
106
|
const symbolNoteClass = style(focusRing(), {
|
|
107
107
|
borderRadius: "3px",
|
|
108
|
-
cursor: "help",
|
|
109
108
|
textDecorationLine: "underline",
|
|
110
109
|
textDecorationStyle: "dotted",
|
|
111
110
|
textDecorationColor: colors.gray[9],
|
package/src/viewer/pierre.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
backgroundColor,
|
|
3
|
+
border,
|
|
4
|
+
colors,
|
|
5
|
+
radius,
|
|
6
|
+
shadow,
|
|
7
|
+
spacing,
|
|
8
|
+
text,
|
|
9
|
+
} from "maui";
|
|
2
10
|
import { style } from "purse-styles";
|
|
3
11
|
import { codeFontFamily } from "../codeFont.js";
|
|
4
12
|
import { diffsTheme } from "./diffsTheme.ts";
|
|
5
13
|
|
|
6
|
-
|
|
14
|
+
// Pierre paints its own white; widgets use the element surface instead.
|
|
15
|
+
const pierreUnsafeCss = `:host { --diffs-font-family: ${codeFontFamily}; --diffs-light-bg: ${backgroundColor.app}; --diffs-dark-bg: ${backgroundColor.app}; }`;
|
|
7
16
|
|
|
8
17
|
export const sourceSelectionCss = `
|
|
9
18
|
[data-selected-line] {
|
|
@@ -18,11 +27,20 @@ export const sourceSelectionCss = `
|
|
|
18
27
|
}
|
|
19
28
|
`;
|
|
20
29
|
|
|
30
|
+
/** The card every widget sits in: a subtle ring on the page background. */
|
|
21
31
|
export const pierreShell = style(radius.md, shadow.subtle, {
|
|
22
32
|
overflow: "hidden",
|
|
23
33
|
minWidth: 0,
|
|
24
34
|
});
|
|
25
35
|
|
|
36
|
+
/** The header row atop a card: a fence's `[Title]` or a diagram's caption. */
|
|
37
|
+
export const fenceTitle = style(
|
|
38
|
+
text({ size: "xs", fontWeight: 500, color: "lowContrast" }),
|
|
39
|
+
spacing.padding({ x: 6, y: 4 }),
|
|
40
|
+
border(["bottom"], "border"),
|
|
41
|
+
{ margin: 0, overflowWrap: "anywhere", backgroundColor: colors.gray[2] },
|
|
42
|
+
);
|
|
43
|
+
|
|
26
44
|
export function pierreDiffOptions(input: {
|
|
27
45
|
themeType: "light" | "dark";
|
|
28
46
|
disableFileHeader: boolean;
|