@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
|
@@ -1,39 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
backgroundColor,
|
|
3
|
-
colors,
|
|
4
|
-
focusRing,
|
|
5
|
-
LARGE_SCALE,
|
|
6
|
-
spacing,
|
|
7
|
-
text,
|
|
8
|
-
Tooltip,
|
|
9
|
-
} from "maui";
|
|
1
|
+
import { colors, focusRing, LARGE_SCALE, text, Tooltip } from "maui";
|
|
10
2
|
import { defineVars, style, useStyles } from "purse-styles";
|
|
11
3
|
import { codeFontFamily } from "../codeFont.js";
|
|
12
|
-
import
|
|
4
|
+
import {
|
|
5
|
+
callStackRow,
|
|
6
|
+
type SourceAnnotation,
|
|
7
|
+
type SourceNavigation,
|
|
8
|
+
} from "../annotations.js";
|
|
13
9
|
import { lineSymbolMatches, type CodeNote } from "../codeNotes.js";
|
|
14
10
|
import { NoteText, NoteTokens } from "./notes.tsx";
|
|
15
|
-
import { pierreShell } from "./pierre.js";
|
|
11
|
+
import { fenceTitle, pierreShell } from "./pierre.js";
|
|
16
12
|
|
|
17
13
|
export function CallStackDiff(
|
|
18
14
|
props: {
|
|
19
15
|
lines: SourceAnnotation[];
|
|
20
16
|
highlights: number[];
|
|
21
17
|
notes: CodeNote[];
|
|
18
|
+
title: string | undefined;
|
|
22
19
|
} & SourceNavigation,
|
|
23
20
|
) {
|
|
24
21
|
const shell = useStyles(pierreShell, styles.shell);
|
|
22
|
+
const titleClassName = useStyles(fenceTitle);
|
|
25
23
|
const rowClassName = useStyles(styles.row);
|
|
26
24
|
return (
|
|
27
|
-
<
|
|
25
|
+
<figure className={shell} data-diffmap-kind="callstack">
|
|
26
|
+
{props.title !== undefined && (
|
|
27
|
+
<figcaption className={titleClassName}>{props.title}</figcaption>
|
|
28
|
+
)}
|
|
28
29
|
{props.lines.map((line, index) => {
|
|
29
|
-
const sign = line.text
|
|
30
|
-
? "+"
|
|
31
|
-
: line.text.startsWith("-")
|
|
32
|
-
? "-"
|
|
33
|
-
: " ";
|
|
34
|
-
const label = line.text.replace(/^[+ -]/, "");
|
|
35
|
-
const tree = /^[ \t│┃├└─]*/u.exec(label)![0];
|
|
36
|
-
const description = label.slice(tree.length);
|
|
30
|
+
const { sign, tree, description } = callStackRow(line.text);
|
|
37
31
|
const comment = description.indexOf("#");
|
|
38
32
|
const linked = line.references.length > 0;
|
|
39
33
|
const row = index + 1;
|
|
@@ -125,27 +119,30 @@ export function CallStackDiff(
|
|
|
125
119
|
</Tooltip>
|
|
126
120
|
);
|
|
127
121
|
})}
|
|
128
|
-
</
|
|
122
|
+
</figure>
|
|
129
123
|
);
|
|
130
124
|
}
|
|
131
125
|
|
|
132
126
|
const rowSize = defineVars({
|
|
133
|
-
height: { default: "
|
|
127
|
+
height: { default: "22px", [LARGE_SCALE]: "32px" },
|
|
134
128
|
});
|
|
135
129
|
|
|
136
130
|
const styles = {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
131
|
+
// Sized like Maui's code block, but in the proportional UI font.
|
|
132
|
+
shell: style(text({ size: "xs", fontWeight: 400, color: "highContrast" }), {
|
|
133
|
+
margin: 0,
|
|
134
|
+
paddingBlock: "8px",
|
|
135
|
+
overflowX: "hidden",
|
|
136
|
+
maxWidth: "100%",
|
|
137
|
+
// The shell's top padding goes below the title, between it and the rows.
|
|
138
|
+
"& > figcaption": {
|
|
139
|
+
marginTop: "-8px",
|
|
140
|
+
marginBottom: "8px",
|
|
144
141
|
},
|
|
145
|
-
),
|
|
142
|
+
}),
|
|
146
143
|
row: style(
|
|
147
|
-
spacing.padding({ x: 3 }),
|
|
148
144
|
{
|
|
145
|
+
paddingInline: "12px",
|
|
149
146
|
display: "flex",
|
|
150
147
|
alignItems: "center",
|
|
151
148
|
gap: "12px",
|
|
@@ -174,7 +171,6 @@ const styles = {
|
|
|
174
171
|
backgroundColor: colors.red[5],
|
|
175
172
|
boxShadow: `inset 3px 0 ${colors.red[9]}`,
|
|
176
173
|
},
|
|
177
|
-
"&[data-note]": { cursor: "help" },
|
|
178
174
|
// The sign fills the row gutter, so the dot follows the label instead.
|
|
179
175
|
"&[data-note='start'] .stack-label::after": {
|
|
180
176
|
content: '""',
|
|
@@ -204,7 +200,7 @@ const styles = {
|
|
|
204
200
|
alignSelf: "center",
|
|
205
201
|
flex: "1 1 auto",
|
|
206
202
|
minWidth: 0,
|
|
207
|
-
paddingBlock: "
|
|
203
|
+
paddingBlock: "1px",
|
|
208
204
|
overflow: "hidden",
|
|
209
205
|
textOverflow: "ellipsis",
|
|
210
206
|
whiteSpace: "nowrap",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useId, useState } from "react";
|
|
2
2
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
3
3
|
import {
|
|
4
|
-
backgroundColor,
|
|
5
4
|
borderColor,
|
|
6
5
|
ChevronRight,
|
|
7
6
|
colors,
|
|
@@ -245,7 +244,6 @@ const STAT_INSET = 12;
|
|
|
245
244
|
|
|
246
245
|
const styles = {
|
|
247
246
|
shell: style(text({ size: "sm", color: "highContrast" }), {
|
|
248
|
-
backgroundColor: backgroundColor.app,
|
|
249
247
|
maxWidth: "100%",
|
|
250
248
|
}),
|
|
251
249
|
// Same insets and gap as the file rows, so the branch icon lines up with the file icons
|
package/src/viewer/CodeBlock.tsx
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
import { useEffect, useState, type ReactNode } from "react";
|
|
2
2
|
import { getSharedHighlighter } from "@pierre/diffs";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Check,
|
|
6
|
+
codeBlockStyles,
|
|
7
|
+
colors,
|
|
8
|
+
Copy,
|
|
9
|
+
Tooltip,
|
|
10
|
+
useTheme,
|
|
11
|
+
} from "maui";
|
|
4
12
|
import { style, useStyles } from "purse-styles";
|
|
5
13
|
import { codeFontFamily } from "../codeFont.js";
|
|
6
14
|
import { lineSymbolMatches, type CodeNote } from "../codeNotes.js";
|
|
7
15
|
import { lineNoteDot, NoteText, NoteTokens, type Token } from "./notes.tsx";
|
|
16
|
+
import { fenceTitle, pierreShell } from "./pierre.ts";
|
|
8
17
|
|
|
9
|
-
/** A pseudocode fence with highlighted lines and
|
|
18
|
+
/** A pseudocode fence with an optional title, highlighted lines, hover notes, and a copy button. */
|
|
10
19
|
export function CodeBlock(props: {
|
|
11
20
|
lang: string;
|
|
12
21
|
source: string;
|
|
13
22
|
highlights: number[];
|
|
14
23
|
notes: CodeNote[];
|
|
24
|
+
title: string | undefined;
|
|
15
25
|
}) {
|
|
16
26
|
const className = useStyles(codeBlockStyles.root, styles.block);
|
|
27
|
+
const figureClassName = useStyles(pierreShell, styles.figure);
|
|
28
|
+
const titleClassName = useStyles(fenceTitle);
|
|
29
|
+
const copyClassName = useStyles(styles.copy);
|
|
30
|
+
const [copied, setCopied] = useState(false);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!copied) return;
|
|
33
|
+
const timer = setTimeout(() => setCopied(false), 1500);
|
|
34
|
+
return () => clearTimeout(timer);
|
|
35
|
+
}, [copied]);
|
|
17
36
|
const lines = useTokenLines(props.source, props.lang);
|
|
18
37
|
const sourceLines = props.source.split("\n");
|
|
19
38
|
|
|
@@ -54,9 +73,34 @@ export function CodeBlock(props: {
|
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
return (
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
<figure
|
|
77
|
+
className={figureClassName}
|
|
78
|
+
data-wrap={proseLangs.has(props.lang) || undefined}
|
|
79
|
+
>
|
|
80
|
+
{props.title !== undefined && (
|
|
81
|
+
<figcaption className={titleClassName}>{props.title}</figcaption>
|
|
82
|
+
)}
|
|
83
|
+
<pre className={className} data-diffmap-kind="code">
|
|
84
|
+
<code>{rows}</code>
|
|
85
|
+
</pre>
|
|
86
|
+
<Button
|
|
87
|
+
className={copyClassName}
|
|
88
|
+
variant="quiet"
|
|
89
|
+
data-copy=""
|
|
90
|
+
data-copied={copied || undefined}
|
|
91
|
+
size="sm"
|
|
92
|
+
aria-label={copied ? "Copied" : "Copy code"}
|
|
93
|
+
onPress={() => {
|
|
94
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- Press handlers cannot await; the icon only changes once the write succeeds.
|
|
95
|
+
void navigator.clipboard.writeText(props.source).then(
|
|
96
|
+
() => setCopied(true),
|
|
97
|
+
(cause) => console.warn("diffmap copy failed", cause),
|
|
98
|
+
);
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{copied ? <Check size="sm" /> : <Copy size="sm" />}
|
|
102
|
+
</Button>
|
|
103
|
+
</figure>
|
|
60
104
|
);
|
|
61
105
|
}
|
|
62
106
|
|
|
@@ -95,11 +139,40 @@ function useTokenLines(source: string, lang: string): Token[][] {
|
|
|
95
139
|
return source.split("\n").map((line) => [{ content: line }]);
|
|
96
140
|
}
|
|
97
141
|
|
|
142
|
+
/** Prose-like fences wrap long lines instead of scrolling. */
|
|
143
|
+
const proseLangs = new Set(["text", "txt", "plaintext", "md", "markdown"]);
|
|
144
|
+
|
|
98
145
|
const styles = {
|
|
146
|
+
// The figure is the card, so the block inside drops its own edge.
|
|
147
|
+
figure: style({
|
|
148
|
+
position: "relative",
|
|
149
|
+
margin: 0,
|
|
150
|
+
"& > pre": {
|
|
151
|
+
boxShadow: "none",
|
|
152
|
+
borderRadius: 0,
|
|
153
|
+
backgroundColor: "transparent",
|
|
154
|
+
},
|
|
155
|
+
// Room for the copy button, which sits in the title row.
|
|
156
|
+
"& > figcaption": { paddingRight: "40px" },
|
|
157
|
+
"&:hover > [data-copy], &:focus-within > [data-copy]": { opacity: 1 },
|
|
158
|
+
"&[data-wrap] > pre > code": { display: "block" },
|
|
159
|
+
"&[data-wrap] .line": { whiteSpace: "pre-wrap", overflowWrap: "anywhere" },
|
|
160
|
+
}),
|
|
161
|
+
// Hidden until the block is hovered or focused, and while it confirms a copy.
|
|
162
|
+
// Touch screens have no hover, so the button always shows there.
|
|
163
|
+
copy: style({
|
|
164
|
+
position: "absolute",
|
|
165
|
+
top: "6px",
|
|
166
|
+
right: "6px",
|
|
167
|
+
opacity: 0,
|
|
168
|
+
"&[data-copied]": { opacity: 1 },
|
|
169
|
+
"@media (hover: none)": { opacity: 1 },
|
|
170
|
+
}),
|
|
99
171
|
block: style({
|
|
100
172
|
fontFamily: codeFontFamily,
|
|
101
173
|
paddingInline: 0,
|
|
102
|
-
|
|
174
|
+
// `:is(pre)` outranks the prose rule that shrinks inline code to 0.875em.
|
|
175
|
+
"&:is(pre) > code": {
|
|
103
176
|
display: "inline-block",
|
|
104
177
|
verticalAlign: "top",
|
|
105
178
|
minWidth: "100%",
|
|
@@ -117,7 +190,6 @@ const styles = {
|
|
|
117
190
|
"& .line-note": {
|
|
118
191
|
display: "block",
|
|
119
192
|
position: "relative",
|
|
120
|
-
cursor: "help",
|
|
121
193
|
outline: "none",
|
|
122
194
|
},
|
|
123
195
|
"& .line-note::before": lineNoteDot,
|
|
@@ -87,6 +87,13 @@ function renderElement(
|
|
|
87
87
|
</blockquote>
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
|
+
if (node.tag === "details") {
|
|
91
|
+
return (
|
|
92
|
+
<Details key={key} summary={node.attrs.summary ?? "Details"}>
|
|
93
|
+
{children}
|
|
94
|
+
</Details>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
90
97
|
if (node.tag === "li" && node.attrs.task === true) {
|
|
91
98
|
return (
|
|
92
99
|
<TaskListItem key={key} node={node}>
|
|
@@ -188,6 +195,18 @@ function InlineReference(
|
|
|
188
195
|
);
|
|
189
196
|
}
|
|
190
197
|
|
|
198
|
+
/** MDC `::details{summary="…"}`: content folded under a summary row. */
|
|
199
|
+
function Details(props: { summary: string; children: ReactNode }) {
|
|
200
|
+
const className = useStyles(detailsClass);
|
|
201
|
+
const summaryClassName = useStyles(detailsSummaryClass);
|
|
202
|
+
return (
|
|
203
|
+
<details className={className}>
|
|
204
|
+
<summary className={summaryClassName}>{props.summary}</summary>
|
|
205
|
+
<div className="diffmap-details-content">{props.children}</div>
|
|
206
|
+
</details>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
function TaskListItem(props: { node: ViewerElement; children: ReactNode }) {
|
|
192
211
|
const [checked, setChecked] = useState(props.node.attrs.checked === true);
|
|
193
212
|
const className = useStyles(taskListItemClass);
|
|
@@ -250,6 +269,21 @@ const inlineReferenceClass = style(
|
|
|
250
269
|
focusRing(),
|
|
251
270
|
);
|
|
252
271
|
|
|
272
|
+
const detailsSummaryClass = style(radius.sm, focusRing(), {
|
|
273
|
+
cursor: "pointer",
|
|
274
|
+
width: "fit-content",
|
|
275
|
+
fontWeight: 500,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const detailsClass = style({
|
|
279
|
+
"& > .diffmap-details-content": {
|
|
280
|
+
display: "flex",
|
|
281
|
+
flexDirection: "column",
|
|
282
|
+
gap: spacing.value(8),
|
|
283
|
+
marginTop: spacing.value(6),
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
|
|
253
287
|
const taskListItemClass = style({
|
|
254
288
|
display: "grid",
|
|
255
289
|
gridTemplateColumns: "auto minmax(0, 1fr)",
|
package/src/viewer/Fence.tsx
CHANGED
|
@@ -19,6 +19,8 @@ export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
|
19
19
|
<MermaidBlock
|
|
20
20
|
source={fence.source}
|
|
21
21
|
annotations={fence.annotations}
|
|
22
|
+
sides={fence.sides}
|
|
23
|
+
title={fence.title}
|
|
22
24
|
selectedAnnotation={props.selectedAnnotation}
|
|
23
25
|
onSelectAnnotation={props.onSelectAnnotation}
|
|
24
26
|
/>
|
|
@@ -51,6 +53,7 @@ export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
|
51
53
|
lines={fence.lines}
|
|
52
54
|
highlights={fence.highlights}
|
|
53
55
|
notes={fence.notes}
|
|
56
|
+
title={fence.title}
|
|
54
57
|
selectedAnnotation={props.selectedAnnotation}
|
|
55
58
|
onSelectAnnotation={props.onSelectAnnotation}
|
|
56
59
|
/>
|
|
@@ -74,6 +77,7 @@ export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
|
74
77
|
source={fence.source}
|
|
75
78
|
highlights={fence.highlights}
|
|
76
79
|
notes={fence.notes}
|
|
80
|
+
title={fence.title}
|
|
77
81
|
/>
|
|
78
82
|
);
|
|
79
83
|
}
|
package/src/viewer/HtmlBlock.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { radius, shadow, spacing } from "maui";
|
|
1
|
+
import { backgroundColor, radius, shadow, spacing } from "maui";
|
|
2
2
|
import { style, useStyles } from "purse-styles";
|
|
3
3
|
|
|
4
4
|
export function HtmlBlock(props: { source: string }) {
|
|
@@ -15,5 +15,6 @@ export function HtmlBlock(props: { source: string }) {
|
|
|
15
15
|
const styles = {
|
|
16
16
|
shell: style(radius.md, shadow.subtle, spacing.padding({ all: 4 }), {
|
|
17
17
|
minWidth: 0,
|
|
18
|
+
backgroundColor: backgroundColor.element,
|
|
18
19
|
}),
|
|
19
20
|
};
|
|
@@ -8,26 +8,36 @@ import {
|
|
|
8
8
|
colors,
|
|
9
9
|
fontFamily,
|
|
10
10
|
spacing,
|
|
11
|
+
text,
|
|
11
12
|
} from "maui";
|
|
12
13
|
import { style, useStyles } from "purse-styles";
|
|
13
14
|
import { mermaidSvg } from "../mermaid.js";
|
|
14
15
|
import type { DiagramAnnotation } from "../diagramAnnotations.js";
|
|
15
16
|
import type { SourceNavigation } from "../annotations.js";
|
|
17
|
+
import { markDiagramChanges } from "./diagramDiff.js";
|
|
16
18
|
import { linkDiagram } from "./diagramLinks.js";
|
|
19
|
+
import { fenceTitle, pierreShell } from "./pierre.js";
|
|
17
20
|
|
|
18
21
|
type MermaidProps = {
|
|
19
22
|
source: string;
|
|
20
23
|
annotations: DiagramAnnotation[];
|
|
24
|
+
sides: { today: string; proposed: string } | undefined;
|
|
25
|
+
title: string | undefined;
|
|
21
26
|
} & SourceNavigation;
|
|
22
27
|
|
|
23
28
|
export function MermaidBlock(props: MermaidProps) {
|
|
24
29
|
const [fullscreen, setFullscreen] = useState(false);
|
|
25
|
-
const frame = useStyles(styles.frame);
|
|
30
|
+
const frame = useStyles(pierreShell, styles.frame);
|
|
26
31
|
const expand = useStyles(styles.expand);
|
|
27
32
|
const dialog = useStyles(styles.dialog);
|
|
28
33
|
const header = useStyles(styles.header);
|
|
34
|
+
const caption = useStyles(fenceTitle);
|
|
35
|
+
const dialogTitle = useStyles(styles.dialogTitle);
|
|
29
36
|
return (
|
|
30
|
-
<
|
|
37
|
+
<figure className={frame}>
|
|
38
|
+
{props.title !== undefined && (
|
|
39
|
+
<figcaption className={caption}>{props.title}</figcaption>
|
|
40
|
+
)}
|
|
31
41
|
<MermaidDiagram {...props} fullscreen={false} />
|
|
32
42
|
<Button
|
|
33
43
|
className={expand}
|
|
@@ -42,10 +52,11 @@ export function MermaidBlock(props: MermaidProps) {
|
|
|
42
52
|
isOpen={fullscreen}
|
|
43
53
|
onOpenChange={setFullscreen}
|
|
44
54
|
size="lg"
|
|
45
|
-
aria-label="Diagram"
|
|
55
|
+
aria-label={props.title ?? "Diagram"}
|
|
46
56
|
className={dialog}
|
|
47
57
|
>
|
|
48
58
|
<div className={header}>
|
|
59
|
+
<span className={dialogTitle}>{props.title}</span>
|
|
49
60
|
<Button
|
|
50
61
|
size="sm"
|
|
51
62
|
variant="quiet"
|
|
@@ -65,7 +76,7 @@ export function MermaidBlock(props: MermaidProps) {
|
|
|
65
76
|
}}
|
|
66
77
|
/>
|
|
67
78
|
</Dialog>
|
|
68
|
-
</
|
|
79
|
+
</figure>
|
|
69
80
|
);
|
|
70
81
|
}
|
|
71
82
|
|
|
@@ -75,10 +86,10 @@ function MermaidDiagram(props: MermaidProps & { fullscreen: boolean }) {
|
|
|
75
86
|
styles.shell,
|
|
76
87
|
props.fullscreen ? styles.fullscreenShell : styles.inlineShell,
|
|
77
88
|
);
|
|
78
|
-
const svg = useMemo(
|
|
79
|
-
() =>
|
|
89
|
+
const svg = useMemo(() => {
|
|
90
|
+
const draw = (source: string) =>
|
|
80
91
|
mermaidSvg({
|
|
81
|
-
source
|
|
92
|
+
source,
|
|
82
93
|
bg: backgroundColor.app,
|
|
83
94
|
fg: colors.gray[12],
|
|
84
95
|
accent: colors.accent[9],
|
|
@@ -86,9 +97,15 @@ function MermaidDiagram(props: MermaidProps & { fullscreen: boolean }) {
|
|
|
86
97
|
surface: backgroundColor.element,
|
|
87
98
|
border: colors.gray[6],
|
|
88
99
|
font: fontFamily,
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
});
|
|
101
|
+
const all = draw(props.source);
|
|
102
|
+
if (all instanceof Error || props.sides === undefined) return all;
|
|
103
|
+
const today = draw(props.sides.today);
|
|
104
|
+
if (today instanceof Error) return today;
|
|
105
|
+
const proposed = draw(props.sides.proposed);
|
|
106
|
+
if (proposed instanceof Error) return proposed;
|
|
107
|
+
return markDiagramChanges(all, { today, proposed });
|
|
108
|
+
}, [props.source, props.sides]);
|
|
92
109
|
const linked = useMemo(
|
|
93
110
|
() => (svg instanceof Error ? svg : linkDiagram(svg, props.annotations)),
|
|
94
111
|
[svg, props.annotations],
|
|
@@ -152,16 +169,23 @@ function MermaidDiagram(props: MermaidProps & { fullscreen: boolean }) {
|
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
const styles = {
|
|
172
|
+
// A card like a code block: the title row, then the diagram, padded.
|
|
155
173
|
frame: style({
|
|
156
174
|
position: "relative",
|
|
157
175
|
width: "100%",
|
|
158
|
-
|
|
176
|
+
margin: 0,
|
|
177
|
+
// Room for the full-screen button, which sits in the title row.
|
|
178
|
+
"& > figcaption": { paddingRight: "40px" },
|
|
179
|
+
"& > [data-diffmap-kind='mermaid']": { padding: spacing.value(6) },
|
|
159
180
|
}),
|
|
160
181
|
expand: style({
|
|
161
182
|
position: "absolute",
|
|
162
|
-
top:
|
|
163
|
-
|
|
183
|
+
top: "6px",
|
|
184
|
+
right: "6px",
|
|
164
185
|
}),
|
|
186
|
+
dialogTitle: style(
|
|
187
|
+
text({ size: "xs", fontWeight: 500, color: "lowContrast" }),
|
|
188
|
+
),
|
|
165
189
|
dialog: style({
|
|
166
190
|
width: "calc(100vw - 32px)",
|
|
167
191
|
height: "calc(100dvh - 32px)",
|
|
@@ -170,7 +194,8 @@ const styles = {
|
|
|
170
194
|
}),
|
|
171
195
|
header: style({
|
|
172
196
|
display: "flex",
|
|
173
|
-
|
|
197
|
+
alignItems: "center",
|
|
198
|
+
justifyContent: "space-between",
|
|
174
199
|
}),
|
|
175
200
|
inlineShell: style({
|
|
176
201
|
maxHeight: "60vh",
|
|
@@ -210,5 +235,21 @@ const styles = {
|
|
|
210
235
|
width: "100%",
|
|
211
236
|
height: "auto",
|
|
212
237
|
},
|
|
238
|
+
...changeStyles("-", colors.red),
|
|
239
|
+
...changeStyles("+", colors.green),
|
|
240
|
+
...changeStyles("~", colors.amber),
|
|
213
241
|
}),
|
|
214
242
|
};
|
|
243
|
+
|
|
244
|
+
/** Colors a `mermaid diff`'s marked nodes and edges; removed ones are dashed. */
|
|
245
|
+
function changeStyles(sign: string, scale: typeof colors.red) {
|
|
246
|
+
const marked = `[data-change='${sign}']`;
|
|
247
|
+
const dash = sign === "-" ? { strokeDasharray: "5 3" } : {};
|
|
248
|
+
return {
|
|
249
|
+
[`& [data-id]${marked} > :is(rect, polygon, circle, ellipse, path), & .edge-label${marked} > rect`]:
|
|
250
|
+
{ fill: scale[2], stroke: scale[9], strokeWidth: 1.5, ...dash },
|
|
251
|
+
[`& ${marked} text`]: { fill: scale[11] },
|
|
252
|
+
[`& :is(polyline, line)${marked}, & ${marked} > :is(line, polyline, path)`]:
|
|
253
|
+
{ stroke: scale[9], strokeWidth: 1.5, ...dash },
|
|
254
|
+
};
|
|
255
|
+
}
|