@tanishqkancharla/diffmap 0.1.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -57
- package/index.html +18 -0
- package/package.json +7 -5
- package/src/annotations.ts +36 -29
- package/src/changes.ts +235 -0
- package/src/cli.ts +1 -0
- package/src/errors.ts +5 -0
- package/src/gist/GistApp.tsx +38 -17
- package/src/gist/fetchGist.ts +47 -0
- package/src/git.test.ts +402 -0
- package/src/git.ts +498 -0
- package/src/github/GitHubApp.tsx +39 -22
- package/src/github/fetchRepo.test.ts +59 -1
- package/src/github/fetchRepo.ts +99 -43
- package/src/github/patch.test.ts +18 -9
- package/src/github/patch.ts +30 -19
- package/src/host/HostHome.tsx +20 -7
- package/src/host/files.ts +5 -1
- package/src/host/home.md +17 -55
- package/src/host/home.test.ts +4 -3
- package/src/index.ts +1 -0
- package/src/parseFence.ts +6 -6
- package/src/parseViewer.test.ts +140 -0
- package/src/parseViewer.ts +104 -78
- package/src/serve.ts +34 -1
- package/src/share.ts +62 -1
- package/src/viewer/CallStackDiff.tsx +15 -3
- package/src/viewer/ChangesCard.tsx +267 -0
- package/src/viewer/ComarkView.tsx +83 -15
- package/src/viewer/Fence.tsx +9 -1
- package/src/viewer/MermaidBlock.tsx +99 -11
- package/src/viewer/RangePicker.tsx +526 -0
- package/src/viewer/SourceDiffPanel.tsx +254 -116
- package/src/viewer/TableOfContents.tsx +177 -93
- package/src/viewer/ViewerApp.tsx +34 -114
- package/src/viewer/changesSource.ts +91 -0
- package/src/viewer/sourceHeader.test.ts +2 -87
- package/src/viewer/sourceHeader.ts +4 -63
- package/src/viewer/sourceNavigation.ts +0 -43
- package/vite.config.ts +1 -0
- package/vite.gist.config.ts +57 -0
- package/src/viewer/TocButton.tsx +0 -27
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
backgroundColor,
|
|
3
|
+
borderColor,
|
|
4
|
+
colors,
|
|
5
|
+
Css,
|
|
6
|
+
File,
|
|
7
|
+
FileCode,
|
|
8
|
+
FileText,
|
|
9
|
+
focusRing,
|
|
10
|
+
GitBranch,
|
|
11
|
+
text,
|
|
12
|
+
} from "maui";
|
|
13
|
+
import { style, useStyles } from "purse-styles";
|
|
14
|
+
import type { SourceAnnotation, SourceNavigation } from "../annotations.js";
|
|
15
|
+
import {
|
|
16
|
+
isCommitRev,
|
|
17
|
+
shortRev,
|
|
18
|
+
WORKING,
|
|
19
|
+
type ChangedFile,
|
|
20
|
+
type Changes,
|
|
21
|
+
type Rev,
|
|
22
|
+
} from "../changes.js";
|
|
23
|
+
import { codeFontFamily } from "../codeFont.js";
|
|
24
|
+
import { useChanges } from "./changesSource.ts";
|
|
25
|
+
import { pierreShell } from "./pierre.js";
|
|
26
|
+
|
|
27
|
+
/** A phase's file summary. Review opens every file; a row focuses one. */
|
|
28
|
+
export function ChangesCard(props: { rev: Rev } & SourceNavigation) {
|
|
29
|
+
const changes = useChanges(props.rev);
|
|
30
|
+
const shell = useStyles(pierreShell, styles.shell);
|
|
31
|
+
const header = useStyles(styles.header);
|
|
32
|
+
const row = useStyles(styles.row);
|
|
33
|
+
const review = useStyles(styles.review);
|
|
34
|
+
const muted = useStyles(styles.muted);
|
|
35
|
+
const range = useStyles(styles.range);
|
|
36
|
+
const open = (annotation: SourceAnnotation) =>
|
|
37
|
+
props.onSelectAnnotation(annotation);
|
|
38
|
+
const files =
|
|
39
|
+
changes === undefined || changes instanceof Error ? [] : changes.files;
|
|
40
|
+
return (
|
|
41
|
+
<div className={shell} data-diffmap-kind="changes">
|
|
42
|
+
<div className={header}>
|
|
43
|
+
<GitBranch size="sm" />
|
|
44
|
+
<span className={range}>{revLabel(props.rev, changes)}</span>
|
|
45
|
+
{changes === undefined || changes instanceof Error ? (
|
|
46
|
+
<span className={muted}>
|
|
47
|
+
{changes === undefined ? "Loading changes…" : changes.message}
|
|
48
|
+
</span>
|
|
49
|
+
) : (
|
|
50
|
+
<ChangesSummary changes={changes} />
|
|
51
|
+
)}
|
|
52
|
+
{files.length > 0 && (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
className={review}
|
|
56
|
+
aria-controls="source-diff-panel"
|
|
57
|
+
onClick={() =>
|
|
58
|
+
open({
|
|
59
|
+
text: "Review",
|
|
60
|
+
references: [{ kind: "changes", rev: props.rev }],
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
Review
|
|
65
|
+
</button>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
{files.map((file) => {
|
|
69
|
+
const annotation: SourceAnnotation = {
|
|
70
|
+
text: file.path,
|
|
71
|
+
references: [
|
|
72
|
+
props.rev === WORKING
|
|
73
|
+
? { kind: "file", path: file.path }
|
|
74
|
+
: { kind: "file", rev: props.rev, path: file.path },
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
return (
|
|
78
|
+
<button
|
|
79
|
+
key={file.path}
|
|
80
|
+
type="button"
|
|
81
|
+
className={row}
|
|
82
|
+
title={file.path}
|
|
83
|
+
aria-controls="source-diff-panel"
|
|
84
|
+
aria-pressed={
|
|
85
|
+
props.selectedAnnotation?.text === file.path &&
|
|
86
|
+
props.selectedAnnotation.references[0]?.kind === "file" &&
|
|
87
|
+
props.selectedAnnotation.references[0].rev ===
|
|
88
|
+
annotation.references[0]!.rev
|
|
89
|
+
}
|
|
90
|
+
onClick={() => open(annotation)}
|
|
91
|
+
>
|
|
92
|
+
<FileIcon path={file.path} />
|
|
93
|
+
<span className="name">
|
|
94
|
+
{file.path.split("/").pop()}
|
|
95
|
+
{file.status === "deleted" && (
|
|
96
|
+
<span className={muted}> deleted</span>
|
|
97
|
+
)}
|
|
98
|
+
</span>
|
|
99
|
+
<DiffStat file={file} />
|
|
100
|
+
</button>
|
|
101
|
+
);
|
|
102
|
+
})}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** "9 files changed +214 −119" for a whole rev; the card and the Diff panel header share it. */
|
|
108
|
+
export function ChangesSummary(props: {
|
|
109
|
+
changes: Changes;
|
|
110
|
+
className?: string;
|
|
111
|
+
}) {
|
|
112
|
+
const summary = useStyles(styles.summary);
|
|
113
|
+
const { files, truncated } = props.changes;
|
|
114
|
+
const additions = files.reduce((sum, file) => sum + file.additions, 0);
|
|
115
|
+
const deletions = files.reduce((sum, file) => sum + file.deletions, 0);
|
|
116
|
+
const count = files.length;
|
|
117
|
+
return (
|
|
118
|
+
<span className={`${summary} ${props.className ?? ""}`}>
|
|
119
|
+
<span
|
|
120
|
+
title={
|
|
121
|
+
truncated === true
|
|
122
|
+
? "GitHub listed only these files. Open the commit on GitHub for the rest."
|
|
123
|
+
: undefined
|
|
124
|
+
}
|
|
125
|
+
>
|
|
126
|
+
{count}
|
|
127
|
+
{truncated === true && "+"} {count === 1 ? "file" : "files"} changed
|
|
128
|
+
</span>
|
|
129
|
+
{additions > 0 && <span className="added">+{additions}</span>}
|
|
130
|
+
{deletions > 0 && <span className="removed">−{deletions}</span>}
|
|
131
|
+
</span>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Labels the rev as the range picker does: a commit by its title, a range as `<from>…<to>`. */
|
|
136
|
+
function revLabel(rev: Rev, changes: Changes | Error | undefined) {
|
|
137
|
+
const single = isCommitRev(rev) && !rev.includes("..");
|
|
138
|
+
if (single && changes !== undefined && !(changes instanceof Error))
|
|
139
|
+
return changes.title;
|
|
140
|
+
return shortRev(rev).replace("..", "…");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function DiffStat(props: { file: ChangedFile }) {
|
|
144
|
+
if (props.file.binary) return <span className="stat">binary</span>;
|
|
145
|
+
return (
|
|
146
|
+
<span className="stat">
|
|
147
|
+
{props.file.additions > 0 && (
|
|
148
|
+
<span className="added">+{props.file.additions}</span>
|
|
149
|
+
)}
|
|
150
|
+
{props.file.deletions > 0 && (
|
|
151
|
+
<span className="removed">−{props.file.deletions}</span>
|
|
152
|
+
)}
|
|
153
|
+
</span>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function FileIcon(props: { path: string }) {
|
|
158
|
+
const extension = props.path.split(".").pop()?.toLowerCase() ?? "";
|
|
159
|
+
if (/^(css|scss|less)$/.test(extension)) return <Css size="sm" />;
|
|
160
|
+
if (/^(md|mdx|txt|rst)$/.test(extension)) return <FileText size="sm" />;
|
|
161
|
+
if (
|
|
162
|
+
/^(c|cc|cpp|cs|go|h|java|jsx?|kt|mjs|cjs|py|rb|rs|sh|swift|tsx?|vue|svelte|json|ya?ml|toml|html)$/.test(
|
|
163
|
+
extension,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
return <FileCode size="sm" />;
|
|
167
|
+
return <File size="sm" />;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Left inset of the file rows' icons. */
|
|
171
|
+
const ROW_INSET = 6;
|
|
172
|
+
/** Review's box inset from the card's top and right edges: (36px header − 24px button) / 2. */
|
|
173
|
+
const EDGE = 6;
|
|
174
|
+
/** Right inset of the stats, which Review's text also ends at. */
|
|
175
|
+
const STAT_INSET = 12;
|
|
176
|
+
|
|
177
|
+
const styles = {
|
|
178
|
+
shell: style(text({ size: "sm", color: "highContrast" }), {
|
|
179
|
+
backgroundColor: backgroundColor.app,
|
|
180
|
+
maxWidth: "100%",
|
|
181
|
+
}),
|
|
182
|
+
// Same insets and gap as the file rows, so the branch icon lines up with the file icons
|
|
183
|
+
// and the range with the names. Review's box sits EDGE from the top and right; its
|
|
184
|
+
// text lines up with the stats.
|
|
185
|
+
header: style({
|
|
186
|
+
display: "flex",
|
|
187
|
+
alignItems: "center",
|
|
188
|
+
gap: "10px",
|
|
189
|
+
minHeight: "36px",
|
|
190
|
+
paddingLeft: `${String(ROW_INSET)}px`,
|
|
191
|
+
paddingRight: `${String(EDGE)}px`,
|
|
192
|
+
"& > svg": { flexShrink: 0, color: colors.gray[11] },
|
|
193
|
+
// Divides the summary from the file rows; a card with no files has no divider.
|
|
194
|
+
"&:not(:last-child)": { borderBottom: `1px solid ${borderColor.border}` },
|
|
195
|
+
}),
|
|
196
|
+
range: style(text({ size: "sm", fontWeight: 500, color: "highContrast" }), {
|
|
197
|
+
flex: "1 1 auto",
|
|
198
|
+
minWidth: 0,
|
|
199
|
+
overflow: "hidden",
|
|
200
|
+
textOverflow: "ellipsis",
|
|
201
|
+
whiteSpace: "nowrap",
|
|
202
|
+
}),
|
|
203
|
+
summary: style(text({ size: "sm", color: "lowContrast" }), {
|
|
204
|
+
display: "flex",
|
|
205
|
+
gap: "8px",
|
|
206
|
+
flexShrink: 0,
|
|
207
|
+
whiteSpace: "nowrap",
|
|
208
|
+
fontVariantNumeric: "tabular-nums",
|
|
209
|
+
"& .added, & .removed": { fontFamily: codeFontFamily },
|
|
210
|
+
"& .added": { color: colors.green[11] },
|
|
211
|
+
"& .removed": { color: colors.red[11] },
|
|
212
|
+
}),
|
|
213
|
+
review: style(
|
|
214
|
+
text({ size: "sm", fontWeight: 400, color: "highContrast" }),
|
|
215
|
+
{
|
|
216
|
+
height: "24px",
|
|
217
|
+
padding: `0 ${String(STAT_INSET - EDGE)}px`,
|
|
218
|
+
border: 0,
|
|
219
|
+
borderRadius: "4px",
|
|
220
|
+
background: "transparent",
|
|
221
|
+
cursor: "pointer",
|
|
222
|
+
"&:hover": { backgroundColor: colors.gray[3] },
|
|
223
|
+
},
|
|
224
|
+
focusRing(),
|
|
225
|
+
),
|
|
226
|
+
row: style(
|
|
227
|
+
{
|
|
228
|
+
paddingLeft: `${String(ROW_INSET)}px`,
|
|
229
|
+
paddingRight: `${String(STAT_INSET)}px`,
|
|
230
|
+
display: "flex",
|
|
231
|
+
alignItems: "center",
|
|
232
|
+
gap: "10px",
|
|
233
|
+
width: "100%",
|
|
234
|
+
minHeight: "32px",
|
|
235
|
+
font: "inherit",
|
|
236
|
+
textAlign: "left",
|
|
237
|
+
color: "inherit",
|
|
238
|
+
background: "transparent",
|
|
239
|
+
border: 0,
|
|
240
|
+
cursor: "pointer",
|
|
241
|
+
"& > svg": { flexShrink: 0, color: colors.gray[11] },
|
|
242
|
+
"& .name": {
|
|
243
|
+
flex: "1 1 auto",
|
|
244
|
+
minWidth: 0,
|
|
245
|
+
overflow: "hidden",
|
|
246
|
+
textOverflow: "ellipsis",
|
|
247
|
+
whiteSpace: "nowrap",
|
|
248
|
+
},
|
|
249
|
+
"& .stat": {
|
|
250
|
+
display: "flex",
|
|
251
|
+
gap: "8px",
|
|
252
|
+
flexShrink: 0,
|
|
253
|
+
fontFamily: codeFontFamily,
|
|
254
|
+
fontVariantNumeric: "tabular-nums",
|
|
255
|
+
color: colors.gray[11],
|
|
256
|
+
},
|
|
257
|
+
"& .added": { color: colors.green[11] },
|
|
258
|
+
"& .removed": { color: colors.red[11] },
|
|
259
|
+
"&:hover": { backgroundColor: colors.gray[3] },
|
|
260
|
+
"&[aria-pressed='true'], &[aria-pressed='true']:hover": {
|
|
261
|
+
backgroundColor: colors.blue[3],
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
focusRing(),
|
|
265
|
+
),
|
|
266
|
+
muted: style({ color: colors.gray[11] }),
|
|
267
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createElement, type ReactNode, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Checkbox,
|
|
4
|
+
Code,
|
|
4
5
|
colors,
|
|
5
|
-
|
|
6
|
+
focusRing,
|
|
6
7
|
radius,
|
|
7
8
|
spacing,
|
|
8
9
|
Table,
|
|
@@ -19,7 +20,7 @@ import type {
|
|
|
19
20
|
ViewerElement,
|
|
20
21
|
ViewerNode,
|
|
21
22
|
} from "../parseViewer.js";
|
|
22
|
-
import type { SourceNavigation } from "../annotations.js";
|
|
23
|
+
import type { SourceAnnotation, SourceNavigation } from "../annotations.js";
|
|
23
24
|
import { Fence } from "./Fence.tsx";
|
|
24
25
|
import { HtmlPlaceholder } from "./HtmlPlaceholder.tsx";
|
|
25
26
|
import { useViewerMode } from "./viewerMode.ts";
|
|
@@ -60,6 +61,11 @@ function renderNode(
|
|
|
60
61
|
if (node.type === "view") {
|
|
61
62
|
return <Fence key={key} fence={node.fence} {...navigation} />;
|
|
62
63
|
}
|
|
64
|
+
if (node.type === "reference") {
|
|
65
|
+
return (
|
|
66
|
+
<InlineReference key={key} annotation={node.annotation} {...navigation} />
|
|
67
|
+
);
|
|
68
|
+
}
|
|
63
69
|
return renderElement(node, key, navigation, trustedHtml);
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -89,7 +95,15 @@ function renderElement(
|
|
|
89
95
|
);
|
|
90
96
|
}
|
|
91
97
|
if (node.tag === "code") {
|
|
92
|
-
return
|
|
98
|
+
return (
|
|
99
|
+
<Code
|
|
100
|
+
key={key}
|
|
101
|
+
{...domAttrs(node)}
|
|
102
|
+
style={{ whiteSpace: "normal", overflowWrap: "anywhere" }}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</Code>
|
|
106
|
+
);
|
|
93
107
|
}
|
|
94
108
|
if (voidTags.has(node.tag)) {
|
|
95
109
|
return createElement(node.tag, { key, ...domAttrs(node) });
|
|
@@ -156,12 +170,33 @@ function renderTable(
|
|
|
156
170
|
);
|
|
157
171
|
}
|
|
158
172
|
|
|
173
|
+
function InlineReference(
|
|
174
|
+
props: { annotation: SourceAnnotation } & SourceNavigation,
|
|
175
|
+
) {
|
|
176
|
+
const className = useStyles(inlineReferenceClass);
|
|
177
|
+
return (
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
className={className}
|
|
181
|
+
aria-pressed={props.selectedAnnotation === props.annotation}
|
|
182
|
+
aria-controls="source-diff-panel"
|
|
183
|
+
title={props.annotation.text}
|
|
184
|
+
onClick={() => props.onSelectAnnotation(props.annotation)}
|
|
185
|
+
>
|
|
186
|
+
<Code>{props.annotation.text}</Code>
|
|
187
|
+
</button>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
159
191
|
function TaskListItem(props: { node: ViewerElement; children: ReactNode }) {
|
|
160
192
|
const [checked, setChecked] = useState(props.node.attrs.checked === true);
|
|
193
|
+
const className = useStyles(taskListItemClass);
|
|
161
194
|
return (
|
|
162
195
|
<li
|
|
163
196
|
id={props.node.attrs.id}
|
|
164
|
-
className={props.node.attrs.className
|
|
197
|
+
className={[className, props.node.attrs.className]
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.join(" ")}
|
|
165
200
|
data-task=""
|
|
166
201
|
>
|
|
167
202
|
<span className="diffmap-task-checkbox">
|
|
@@ -171,13 +206,14 @@ function TaskListItem(props: { node: ViewerElement; children: ReactNode }) {
|
|
|
171
206
|
setChecked={setChecked}
|
|
172
207
|
/>
|
|
173
208
|
</span>
|
|
174
|
-
{props.children}
|
|
209
|
+
<div className="diffmap-task-content">{props.children}</div>
|
|
175
210
|
</li>
|
|
176
211
|
);
|
|
177
212
|
}
|
|
178
213
|
|
|
179
214
|
function nodeText(node: ViewerNode): string {
|
|
180
215
|
if (node.type === "text") return node.value;
|
|
216
|
+
if (node.type === "reference") return node.annotation.text;
|
|
181
217
|
if (node.type !== "element") return "";
|
|
182
218
|
return node.children.map(nodeText).join("");
|
|
183
219
|
}
|
|
@@ -194,16 +230,48 @@ function domAttrs(node: ViewerElement) {
|
|
|
194
230
|
};
|
|
195
231
|
}
|
|
196
232
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return <code className={className}>{props.children}</code>;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const inlineCodeClass = style(
|
|
203
|
-
monospace,
|
|
204
|
-
radius.md,
|
|
205
|
-
spacing.padding({ x: 2, y: 1 }),
|
|
233
|
+
const inlineReferenceClass = style(
|
|
234
|
+
radius.sm,
|
|
206
235
|
{
|
|
207
|
-
|
|
236
|
+
display: "inline",
|
|
237
|
+
padding: 0,
|
|
238
|
+
border: 0,
|
|
239
|
+
font: "inherit",
|
|
240
|
+
color: "inherit",
|
|
241
|
+
background: "transparent",
|
|
242
|
+
cursor: "pointer",
|
|
243
|
+
"& > code": { color: colors.blue[11] },
|
|
244
|
+
"&:hover > code": { backgroundColor: colors.blue[3] },
|
|
245
|
+
"&[aria-pressed='true'] > code": {
|
|
246
|
+
backgroundColor: colors.blue[4],
|
|
247
|
+
color: colors.blue[12],
|
|
248
|
+
},
|
|
208
249
|
},
|
|
250
|
+
focusRing(),
|
|
209
251
|
);
|
|
252
|
+
|
|
253
|
+
const taskListItemClass = style({
|
|
254
|
+
display: "grid",
|
|
255
|
+
gridTemplateColumns: "auto minmax(0, 1fr)",
|
|
256
|
+
alignItems: "start",
|
|
257
|
+
columnGap: spacing.value(3),
|
|
258
|
+
"& > .diffmap-task-checkbox": {
|
|
259
|
+
display: "flex",
|
|
260
|
+
alignItems: "center",
|
|
261
|
+
minHeight: "1lh",
|
|
262
|
+
},
|
|
263
|
+
"& > .diffmap-task-content": {
|
|
264
|
+
minWidth: 0,
|
|
265
|
+
},
|
|
266
|
+
"& .diffmap-task-checkbox label > span:last-child": {
|
|
267
|
+
position: "absolute",
|
|
268
|
+
width: "1px",
|
|
269
|
+
height: "1px",
|
|
270
|
+
padding: 0,
|
|
271
|
+
margin: "-1px",
|
|
272
|
+
overflow: "hidden",
|
|
273
|
+
clip: "rect(0, 0, 0, 0)",
|
|
274
|
+
whiteSpace: "nowrap",
|
|
275
|
+
border: 0,
|
|
276
|
+
},
|
|
277
|
+
});
|
package/src/viewer/Fence.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { CodeBlock } from "maui";
|
|
|
2
2
|
import type { Fence as FenceModel } from "../parseFence.js";
|
|
3
3
|
import type { SourceNavigation } from "../annotations.js";
|
|
4
4
|
import { CallStackDiff } from "./CallStackDiff.tsx";
|
|
5
|
+
import { ChangesCard } from "./ChangesCard.tsx";
|
|
5
6
|
import { CodeDiff } from "./CodeDiff.tsx";
|
|
6
7
|
import { FileExcerpt } from "./FileExcerpt.tsx";
|
|
7
8
|
import { HtmlBlock } from "./HtmlBlock.tsx";
|
|
@@ -25,7 +26,14 @@ export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
|
25
26
|
if (!trustedHtml) return <HtmlPlaceholder />;
|
|
26
27
|
return <HtmlBlock source={fence.source} />;
|
|
27
28
|
}
|
|
28
|
-
if (fence.kind === "
|
|
29
|
+
if (fence.kind === "changes")
|
|
30
|
+
return (
|
|
31
|
+
<ChangesCard
|
|
32
|
+
rev={fence.rev}
|
|
33
|
+
selectedAnnotation={props.selectedAnnotation}
|
|
34
|
+
onSelectAnnotation={props.onSelectAnnotation}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
29
37
|
if (fence.kind === "callstack")
|
|
30
38
|
return (
|
|
31
39
|
<CallStackDiff
|
|
@@ -1,19 +1,80 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Close,
|
|
5
|
+
Dialog,
|
|
6
|
+
Maximize,
|
|
7
|
+
backgroundColor,
|
|
8
|
+
colors,
|
|
9
|
+
fontFamily,
|
|
10
|
+
spacing,
|
|
11
|
+
} from "maui";
|
|
3
12
|
import { style, useStyles } from "purse-styles";
|
|
4
13
|
import { mermaidSvg } from "../mermaid.js";
|
|
5
14
|
import type { DiagramAnnotation } from "../diagramAnnotations.js";
|
|
6
15
|
import type { SourceNavigation } from "../annotations.js";
|
|
7
16
|
import { linkDiagram } from "./diagramLinks.js";
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
) {
|
|
18
|
+
type MermaidProps = {
|
|
19
|
+
source: string;
|
|
20
|
+
annotations: DiagramAnnotation[];
|
|
21
|
+
} & SourceNavigation;
|
|
22
|
+
|
|
23
|
+
export function MermaidBlock(props: MermaidProps) {
|
|
24
|
+
const [fullscreen, setFullscreen] = useState(false);
|
|
25
|
+
const frame = useStyles(styles.frame);
|
|
26
|
+
const expand = useStyles(styles.expand);
|
|
27
|
+
const dialog = useStyles(styles.dialog);
|
|
28
|
+
const header = useStyles(styles.header);
|
|
29
|
+
return (
|
|
30
|
+
<div className={frame}>
|
|
31
|
+
<MermaidDiagram {...props} fullscreen={false} />
|
|
32
|
+
<Button
|
|
33
|
+
className={expand}
|
|
34
|
+
size="sm"
|
|
35
|
+
variant="quiet"
|
|
36
|
+
aria-label="Full screen"
|
|
37
|
+
onClick={() => setFullscreen(true)}
|
|
38
|
+
>
|
|
39
|
+
<Maximize size="xs" />
|
|
40
|
+
</Button>
|
|
41
|
+
<Dialog
|
|
42
|
+
isOpen={fullscreen}
|
|
43
|
+
onOpenChange={setFullscreen}
|
|
44
|
+
size="lg"
|
|
45
|
+
aria-label="Diagram"
|
|
46
|
+
className={dialog}
|
|
47
|
+
>
|
|
48
|
+
<div className={header}>
|
|
49
|
+
<Button
|
|
50
|
+
size="sm"
|
|
51
|
+
variant="quiet"
|
|
52
|
+
slot="close"
|
|
53
|
+
aria-label="Exit full screen"
|
|
54
|
+
>
|
|
55
|
+
<Close size="xs" />
|
|
56
|
+
</Button>
|
|
57
|
+
</div>
|
|
58
|
+
<MermaidDiagram
|
|
59
|
+
{...props}
|
|
60
|
+
fullscreen
|
|
61
|
+
onSelectAnnotation={(annotation) => {
|
|
62
|
+
// The Diff panel opens behind the dialog, so leave full screen.
|
|
63
|
+
setFullscreen(false);
|
|
64
|
+
props.onSelectAnnotation(annotation);
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
</Dialog>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function MermaidDiagram(props: MermaidProps & { fullscreen: boolean }) {
|
|
15
73
|
const container = useRef<HTMLDivElement>(null);
|
|
16
|
-
const shell = useStyles(
|
|
74
|
+
const shell = useStyles(
|
|
75
|
+
styles.shell,
|
|
76
|
+
props.fullscreen ? styles.fullscreenShell : styles.inlineShell,
|
|
77
|
+
);
|
|
17
78
|
const svg = useMemo(
|
|
18
79
|
() =>
|
|
19
80
|
mermaidSvg({
|
|
@@ -91,9 +152,37 @@ export function MermaidBlock(
|
|
|
91
152
|
}
|
|
92
153
|
|
|
93
154
|
const styles = {
|
|
94
|
-
|
|
155
|
+
frame: style({
|
|
156
|
+
position: "relative",
|
|
95
157
|
width: "100%",
|
|
158
|
+
minWidth: 0,
|
|
159
|
+
}),
|
|
160
|
+
expand: style({
|
|
161
|
+
position: "absolute",
|
|
162
|
+
top: 0,
|
|
163
|
+
insetInlineEnd: 0,
|
|
164
|
+
}),
|
|
165
|
+
dialog: style({
|
|
166
|
+
width: "calc(100vw - 32px)",
|
|
167
|
+
height: "calc(100dvh - 32px)",
|
|
168
|
+
padding: spacing.value(6),
|
|
169
|
+
"& > [role='dialog']": { flex: 1, minHeight: 0, gap: spacing.value(2) },
|
|
170
|
+
}),
|
|
171
|
+
header: style({
|
|
172
|
+
display: "flex",
|
|
173
|
+
justifyContent: "flex-end",
|
|
174
|
+
}),
|
|
175
|
+
inlineShell: style({
|
|
96
176
|
maxHeight: "60vh",
|
|
177
|
+
"& svg": { maxHeight: "60vh" },
|
|
178
|
+
}),
|
|
179
|
+
fullscreenShell: style({
|
|
180
|
+
flex: 1,
|
|
181
|
+
"& svg": { height: "100%" },
|
|
182
|
+
}),
|
|
183
|
+
shell: style({
|
|
184
|
+
width: "100%",
|
|
185
|
+
minHeight: 0,
|
|
97
186
|
overflow: "auto",
|
|
98
187
|
minWidth: 0,
|
|
99
188
|
border: 0,
|
|
@@ -120,7 +209,6 @@ const styles = {
|
|
|
120
209
|
display: "block",
|
|
121
210
|
width: "100%",
|
|
122
211
|
height: "auto",
|
|
123
|
-
maxHeight: "60vh",
|
|
124
212
|
},
|
|
125
213
|
}),
|
|
126
214
|
};
|