@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
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { useId, useState } from "react";
|
|
2
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
1
3
|
import {
|
|
2
|
-
backgroundColor,
|
|
3
4
|
borderColor,
|
|
5
|
+
ChevronRight,
|
|
4
6
|
colors,
|
|
5
7
|
Css,
|
|
6
8
|
File,
|
|
@@ -8,12 +10,15 @@ import {
|
|
|
8
10
|
FileText,
|
|
9
11
|
focusRing,
|
|
10
12
|
GitBranch,
|
|
13
|
+
motionDurationMs,
|
|
14
|
+
motionEasing,
|
|
11
15
|
text,
|
|
12
16
|
} from "maui";
|
|
13
17
|
import { style, useStyles } from "purse-styles";
|
|
14
|
-
import type {
|
|
18
|
+
import type { SourceNavigation } from "../annotations.js";
|
|
15
19
|
import {
|
|
16
20
|
isCommitRev,
|
|
21
|
+
orderForReview,
|
|
17
22
|
shortRev,
|
|
18
23
|
WORKING,
|
|
19
24
|
type ChangedFile,
|
|
@@ -24,19 +29,32 @@ import { codeFontFamily } from "../codeFont.js";
|
|
|
24
29
|
import { useChanges } from "./changesSource.ts";
|
|
25
30
|
import { pierreShell } from "./pierre.js";
|
|
26
31
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A phase's file summary. Review opens every file; a row focuses one. Files
|
|
34
|
+
* the fence lists come first, in its order; the rest fold under a dimmed
|
|
35
|
+
* "N other files" section.
|
|
36
|
+
*/
|
|
37
|
+
export function ChangesCard(
|
|
38
|
+
props: { rev: Rev; review: string[] } & SourceNavigation,
|
|
39
|
+
) {
|
|
29
40
|
const changes = useChanges(props.rev);
|
|
41
|
+
const [showOther, setShowOther] = useState(false);
|
|
42
|
+
const otherId = useId();
|
|
43
|
+
const reducedMotion = useReducedMotion();
|
|
30
44
|
const shell = useStyles(pierreShell, styles.shell);
|
|
31
45
|
const header = useStyles(styles.header);
|
|
32
|
-
const row = useStyles(styles.row);
|
|
33
46
|
const review = useStyles(styles.review);
|
|
34
47
|
const muted = useStyles(styles.muted);
|
|
35
48
|
const range = useStyles(styles.range);
|
|
36
|
-
const
|
|
37
|
-
props.onSelectAnnotation(annotation);
|
|
49
|
+
const toggle = useStyles(styles.row, styles.toggle);
|
|
38
50
|
const files =
|
|
39
51
|
changes === undefined || changes instanceof Error ? [] : changes.files;
|
|
52
|
+
const { reviewed, other } = orderForReview(files, props.review);
|
|
53
|
+
const rowProps = {
|
|
54
|
+
rev: props.rev,
|
|
55
|
+
selectedAnnotation: props.selectedAnnotation,
|
|
56
|
+
onSelectAnnotation: props.onSelectAnnotation,
|
|
57
|
+
};
|
|
40
58
|
return (
|
|
41
59
|
<div className={shell} data-diffmap-kind="changes">
|
|
42
60
|
<div className={header}>
|
|
@@ -55,7 +73,7 @@ export function ChangesCard(props: { rev: Rev } & SourceNavigation) {
|
|
|
55
73
|
className={review}
|
|
56
74
|
aria-controls="source-diff-panel"
|
|
57
75
|
onClick={() =>
|
|
58
|
-
|
|
76
|
+
props.onSelectAnnotation({
|
|
59
77
|
text: "Review",
|
|
60
78
|
references: [{ kind: "changes", rev: props.rev }],
|
|
61
79
|
})
|
|
@@ -65,45 +83,95 @@ export function ChangesCard(props: { rev: Rev } & SourceNavigation) {
|
|
|
65
83
|
</button>
|
|
66
84
|
)}
|
|
67
85
|
</div>
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
? { kind: "file", path: file.path }
|
|
74
|
-
: { kind: "file", rev: props.rev, path: file.path },
|
|
75
|
-
],
|
|
76
|
-
};
|
|
77
|
-
return (
|
|
86
|
+
{reviewed.map((file) => (
|
|
87
|
+
<FileRow key={file.path} file={file} {...rowProps} />
|
|
88
|
+
))}
|
|
89
|
+
{other.length > 0 && (
|
|
90
|
+
<>
|
|
78
91
|
<button
|
|
79
|
-
key={file.path}
|
|
80
92
|
type="button"
|
|
81
|
-
className={
|
|
82
|
-
|
|
83
|
-
aria-controls=
|
|
84
|
-
|
|
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)}
|
|
93
|
+
className={toggle}
|
|
94
|
+
aria-expanded={showOther}
|
|
95
|
+
aria-controls={otherId}
|
|
96
|
+
onClick={() => setShowOther((shown) => !shown)}
|
|
91
97
|
>
|
|
92
|
-
<
|
|
98
|
+
<ChevronRight size="sm" />
|
|
93
99
|
<span className="name">
|
|
94
|
-
{
|
|
95
|
-
{file.status === "deleted" && (
|
|
96
|
-
<span className={muted}> deleted</span>
|
|
97
|
-
)}
|
|
100
|
+
{other.length} other {other.length === 1 ? "file" : "files"}
|
|
98
101
|
</span>
|
|
99
|
-
<DiffStat
|
|
102
|
+
<DiffStat
|
|
103
|
+
additions={other.reduce((sum, file) => sum + file.additions, 0)}
|
|
104
|
+
deletions={other.reduce((sum, file) => sum + file.deletions, 0)}
|
|
105
|
+
binary={false}
|
|
106
|
+
/>
|
|
100
107
|
</button>
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
<AnimatePresence initial={false}>
|
|
109
|
+
{showOther && (
|
|
110
|
+
<motion.div
|
|
111
|
+
key="other"
|
|
112
|
+
id={otherId}
|
|
113
|
+
role="group"
|
|
114
|
+
aria-label="Other files"
|
|
115
|
+
style={{ overflow: "hidden" }}
|
|
116
|
+
initial={{ height: 0, opacity: 0 }}
|
|
117
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
118
|
+
exit={{ height: 0, opacity: 0 }}
|
|
119
|
+
transition={
|
|
120
|
+
reducedMotion === true
|
|
121
|
+
? { duration: 0 }
|
|
122
|
+
: { type: "spring", visualDuration: 0.22, bounce: 0 }
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
{other.map((file) => (
|
|
126
|
+
<FileRow key={file.path} file={file} dimmed {...rowProps} />
|
|
127
|
+
))}
|
|
128
|
+
</motion.div>
|
|
129
|
+
)}
|
|
130
|
+
</AnimatePresence>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
103
133
|
</div>
|
|
104
134
|
);
|
|
105
135
|
}
|
|
106
136
|
|
|
137
|
+
/** One changed file; selecting it opens that file in the Diff panel. */
|
|
138
|
+
function FileRow(
|
|
139
|
+
props: { rev: Rev; file: ChangedFile; dimmed?: boolean } & SourceNavigation,
|
|
140
|
+
) {
|
|
141
|
+
const row = useStyles(styles.row);
|
|
142
|
+
const muted = useStyles(styles.muted);
|
|
143
|
+
const { file, rev } = props;
|
|
144
|
+
const reference =
|
|
145
|
+
rev === WORKING
|
|
146
|
+
? { kind: "file" as const, path: file.path }
|
|
147
|
+
: { kind: "file" as const, rev, path: file.path };
|
|
148
|
+
const selected = props.selectedAnnotation?.references[0];
|
|
149
|
+
return (
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
className={row}
|
|
153
|
+
title={file.path}
|
|
154
|
+
data-dimmed={props.dimmed === true}
|
|
155
|
+
aria-controls="source-diff-panel"
|
|
156
|
+
aria-pressed={
|
|
157
|
+
props.selectedAnnotation?.text === file.path &&
|
|
158
|
+
selected?.kind === "file" &&
|
|
159
|
+
selected.rev === reference.rev
|
|
160
|
+
}
|
|
161
|
+
onClick={() =>
|
|
162
|
+
props.onSelectAnnotation({ text: file.path, references: [reference] })
|
|
163
|
+
}
|
|
164
|
+
>
|
|
165
|
+
<FileIcon path={file.path} />
|
|
166
|
+
<span className="name">
|
|
167
|
+
{file.path.split("/").pop()}
|
|
168
|
+
{file.status === "deleted" && <span className={muted}> deleted</span>}
|
|
169
|
+
</span>
|
|
170
|
+
<DiffStat {...file} />
|
|
171
|
+
</button>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
107
175
|
/** "9 files changed +214 −119" for a whole rev; the card and the Diff panel header share it. */
|
|
108
176
|
export function ChangesSummary(props: {
|
|
109
177
|
changes: Changes;
|
|
@@ -140,15 +208,15 @@ function revLabel(rev: Rev, changes: Changes | Error | undefined) {
|
|
|
140
208
|
return shortRev(rev).replace("..", "…");
|
|
141
209
|
}
|
|
142
210
|
|
|
143
|
-
function DiffStat(
|
|
144
|
-
|
|
211
|
+
function DiffStat(
|
|
212
|
+
props: Pick<ChangedFile, "additions" | "deletions" | "binary">,
|
|
213
|
+
) {
|
|
214
|
+
if (props.binary) return <span className="stat">binary</span>;
|
|
145
215
|
return (
|
|
146
216
|
<span className="stat">
|
|
147
|
-
{props.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
{props.file.deletions > 0 && (
|
|
151
|
-
<span className="removed">−{props.file.deletions}</span>
|
|
217
|
+
{props.additions > 0 && <span className="added">+{props.additions}</span>}
|
|
218
|
+
{props.deletions > 0 && (
|
|
219
|
+
<span className="removed">−{props.deletions}</span>
|
|
152
220
|
)}
|
|
153
221
|
</span>
|
|
154
222
|
);
|
|
@@ -176,7 +244,6 @@ const STAT_INSET = 12;
|
|
|
176
244
|
|
|
177
245
|
const styles = {
|
|
178
246
|
shell: style(text({ size: "sm", color: "highContrast" }), {
|
|
179
|
-
backgroundColor: backgroundColor.app,
|
|
180
247
|
maxWidth: "100%",
|
|
181
248
|
}),
|
|
182
249
|
// Same insets and gap as the file rows, so the branch icon lines up with the file icons
|
|
@@ -260,8 +327,29 @@ const styles = {
|
|
|
260
327
|
"&[aria-pressed='true'], &[aria-pressed='true']:hover": {
|
|
261
328
|
backgroundColor: colors.blue[3],
|
|
262
329
|
},
|
|
330
|
+
// Files outside the review list recede until you point at or pick one.
|
|
331
|
+
"&[data-dimmed='true'] > *": {
|
|
332
|
+
opacity: 0.5,
|
|
333
|
+
transition: `opacity ${String(motionDurationMs)}ms ${motionEasing}`,
|
|
334
|
+
},
|
|
335
|
+
"&[data-dimmed='true']:is(:hover, :focus-visible, [aria-pressed='true']) > *":
|
|
336
|
+
{ opacity: 1 },
|
|
263
337
|
},
|
|
264
338
|
focusRing(),
|
|
265
339
|
),
|
|
340
|
+
// A quiet footer row: the chevron sits in the icon column, so the count lines up with file names.
|
|
341
|
+
toggle: style({
|
|
342
|
+
color: colors.gray[11],
|
|
343
|
+
borderTop: `1px solid ${borderColor.border}`,
|
|
344
|
+
"& > svg": { transition: "transform 150ms ease-out" },
|
|
345
|
+
"&[aria-expanded='true'] > svg": { transform: "rotate(90deg)" },
|
|
346
|
+
// The total stat steps aside once the files show their own.
|
|
347
|
+
"& > .stat": {
|
|
348
|
+
opacity: 0.7,
|
|
349
|
+
transition: `opacity ${String(motionDurationMs)}ms ${motionEasing}`,
|
|
350
|
+
},
|
|
351
|
+
"&[aria-expanded='true'] > .stat": { opacity: 0 },
|
|
352
|
+
"&:hover": { color: colors.gray[12] },
|
|
353
|
+
}),
|
|
266
354
|
muted: style({ color: colors.gray[11] }),
|
|
267
355
|
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
2
|
+
import { getSharedHighlighter } from "@pierre/diffs";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Check,
|
|
6
|
+
codeBlockStyles,
|
|
7
|
+
colors,
|
|
8
|
+
Copy,
|
|
9
|
+
Tooltip,
|
|
10
|
+
useTheme,
|
|
11
|
+
} from "maui";
|
|
12
|
+
import { style, useStyles } from "purse-styles";
|
|
13
|
+
import { codeFontFamily } from "../codeFont.js";
|
|
14
|
+
import { lineSymbolMatches, type CodeNote } from "../codeNotes.js";
|
|
15
|
+
import { lineNoteDot, NoteText, NoteTokens, type Token } from "./notes.tsx";
|
|
16
|
+
import { fenceTitle, pierreShell } from "./pierre.ts";
|
|
17
|
+
|
|
18
|
+
/** A pseudocode fence with an optional title, highlighted lines, hover notes, and a copy button. */
|
|
19
|
+
export function CodeBlock(props: {
|
|
20
|
+
lang: string;
|
|
21
|
+
source: string;
|
|
22
|
+
highlights: number[];
|
|
23
|
+
notes: CodeNote[];
|
|
24
|
+
title: string | undefined;
|
|
25
|
+
}) {
|
|
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]);
|
|
36
|
+
const lines = useTokenLines(props.source, props.lang);
|
|
37
|
+
const sourceLines = props.source.split("\n");
|
|
38
|
+
|
|
39
|
+
const renderLine = (index: number) => (
|
|
40
|
+
<span
|
|
41
|
+
key={index}
|
|
42
|
+
className="line"
|
|
43
|
+
data-highlighted={props.highlights.includes(index + 1) || undefined}
|
|
44
|
+
>
|
|
45
|
+
<NoteTokens
|
|
46
|
+
tokens={lines[index] ?? []}
|
|
47
|
+
matches={lineSymbolMatches(sourceLines[index]!, props.notes)}
|
|
48
|
+
/>
|
|
49
|
+
</span>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const rows: ReactNode[] = [];
|
|
53
|
+
for (let index = 0; index < sourceLines.length;) {
|
|
54
|
+
const note = props.notes.find(
|
|
55
|
+
(candidate) =>
|
|
56
|
+
candidate.target.kind === "lines" &&
|
|
57
|
+
candidate.target.start === index + 1,
|
|
58
|
+
);
|
|
59
|
+
if (note === undefined || note.target.kind !== "lines") {
|
|
60
|
+
rows.push(renderLine(index));
|
|
61
|
+
index++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const range: ReactNode[] = [];
|
|
65
|
+
for (; index < note.target.end; index++) range.push(renderLine(index));
|
|
66
|
+
rows.push(
|
|
67
|
+
<Tooltip key={`note-${index}`} content={<NoteText text={note.text} />}>
|
|
68
|
+
<span className="line-note" tabIndex={0}>
|
|
69
|
+
{range}
|
|
70
|
+
</span>
|
|
71
|
+
</Tooltip>,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
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>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Tokens per line from Pierre's highlighter; plain text until it loads or for unknown languages. */
|
|
108
|
+
function useTokenLines(source: string, lang: string): Token[][] {
|
|
109
|
+
const { resolvedTheme } = useTheme();
|
|
110
|
+
const [highlighted, setHighlighted] = useState<{
|
|
111
|
+
key: string;
|
|
112
|
+
lines: Token[][];
|
|
113
|
+
}>();
|
|
114
|
+
const key = `${resolvedTheme}\n${lang}\n${source}`;
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
let cancelled = false;
|
|
117
|
+
const theme = `pierre-${resolvedTheme}`;
|
|
118
|
+
const tokenize = async (language: string) => {
|
|
119
|
+
const highlighter = await getSharedHighlighter({
|
|
120
|
+
themes: [theme],
|
|
121
|
+
langs: [language],
|
|
122
|
+
});
|
|
123
|
+
return highlighter.codeToTokens(source, { lang: language, theme }).tokens;
|
|
124
|
+
};
|
|
125
|
+
// oxlint-disable-next-line typescript/no-floating-promises -- React effects cannot await; this request owns the token update.
|
|
126
|
+
void tokenize(lang)
|
|
127
|
+
.catch(() => tokenize("text"))
|
|
128
|
+
.then((lines) => {
|
|
129
|
+
if (!cancelled) setHighlighted({ key, lines });
|
|
130
|
+
})
|
|
131
|
+
.catch((cause) => {
|
|
132
|
+
console.warn("diffmap code highlight failed", cause);
|
|
133
|
+
});
|
|
134
|
+
return () => {
|
|
135
|
+
cancelled = true;
|
|
136
|
+
};
|
|
137
|
+
}, [key, lang, resolvedTheme, source]);
|
|
138
|
+
if (highlighted?.key === key) return highlighted.lines;
|
|
139
|
+
return source.split("\n").map((line) => [{ content: line }]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Prose-like fences wrap long lines instead of scrolling. */
|
|
143
|
+
const proseLangs = new Set(["text", "txt", "plaintext", "md", "markdown"]);
|
|
144
|
+
|
|
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
|
+
}),
|
|
171
|
+
block: style({
|
|
172
|
+
fontFamily: codeFontFamily,
|
|
173
|
+
paddingInline: 0,
|
|
174
|
+
// `:is(pre)` outranks the prose rule that shrinks inline code to 0.875em.
|
|
175
|
+
"&:is(pre) > code": {
|
|
176
|
+
display: "inline-block",
|
|
177
|
+
verticalAlign: "top",
|
|
178
|
+
minWidth: "100%",
|
|
179
|
+
font: "inherit",
|
|
180
|
+
},
|
|
181
|
+
"& .line": {
|
|
182
|
+
display: "block",
|
|
183
|
+
minHeight: "1lh",
|
|
184
|
+
paddingInline: "12px",
|
|
185
|
+
},
|
|
186
|
+
"& .line[data-highlighted]": {
|
|
187
|
+
backgroundColor: colors.blue[3],
|
|
188
|
+
boxShadow: `inset 3px 0 ${colors.blue[9]}`,
|
|
189
|
+
},
|
|
190
|
+
"& .line-note": {
|
|
191
|
+
display: "block",
|
|
192
|
+
position: "relative",
|
|
193
|
+
cursor: "help",
|
|
194
|
+
outline: "none",
|
|
195
|
+
},
|
|
196
|
+
"& .line-note::before": lineNoteDot,
|
|
197
|
+
"& .line-note:hover > .line:not([data-highlighted])": {
|
|
198
|
+
backgroundColor: colors.gray[3],
|
|
199
|
+
},
|
|
200
|
+
"& .line-note:focus-visible": {
|
|
201
|
+
boxShadow: `inset 0 0 0 2px ${colors.blue[8]}`,
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
@@ -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
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { CodeBlock } from "maui";
|
|
2
1
|
import type { Fence as FenceModel } from "../parseFence.js";
|
|
3
2
|
import type { SourceNavigation } from "../annotations.js";
|
|
4
3
|
import { CallStackDiff } from "./CallStackDiff.tsx";
|
|
5
4
|
import { ChangesCard } from "./ChangesCard.tsx";
|
|
5
|
+
import { CodeBlock } from "./CodeBlock.tsx";
|
|
6
6
|
import { CodeDiff } from "./CodeDiff.tsx";
|
|
7
7
|
import { FileExcerpt } from "./FileExcerpt.tsx";
|
|
8
8
|
import { HtmlBlock } from "./HtmlBlock.tsx";
|
|
9
9
|
import { HtmlPlaceholder } from "./HtmlPlaceholder.tsx";
|
|
10
10
|
import { MermaidBlock } from "./MermaidBlock.tsx";
|
|
11
|
+
import { ServiceDiagramBlock } from "./ServiceDiagramBlock.tsx";
|
|
11
12
|
import { useViewerMode } from "./viewerMode.ts";
|
|
12
13
|
|
|
13
14
|
export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
@@ -18,6 +19,17 @@ export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
|
18
19
|
<MermaidBlock
|
|
19
20
|
source={fence.source}
|
|
20
21
|
annotations={fence.annotations}
|
|
22
|
+
sides={fence.sides}
|
|
23
|
+
title={fence.title}
|
|
24
|
+
selectedAnnotation={props.selectedAnnotation}
|
|
25
|
+
onSelectAnnotation={props.onSelectAnnotation}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
if (fence.kind === "services")
|
|
29
|
+
return (
|
|
30
|
+
<ServiceDiagramBlock
|
|
31
|
+
source={fence.source}
|
|
32
|
+
parsed={fence.parsed}
|
|
21
33
|
selectedAnnotation={props.selectedAnnotation}
|
|
22
34
|
onSelectAnnotation={props.onSelectAnnotation}
|
|
23
35
|
/>
|
|
@@ -30,6 +42,7 @@ export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
|
30
42
|
return (
|
|
31
43
|
<ChangesCard
|
|
32
44
|
rev={fence.rev}
|
|
45
|
+
review={fence.review}
|
|
33
46
|
selectedAnnotation={props.selectedAnnotation}
|
|
34
47
|
onSelectAnnotation={props.onSelectAnnotation}
|
|
35
48
|
/>
|
|
@@ -38,6 +51,9 @@ export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
|
38
51
|
return (
|
|
39
52
|
<CallStackDiff
|
|
40
53
|
lines={fence.lines}
|
|
54
|
+
highlights={fence.highlights}
|
|
55
|
+
notes={fence.notes}
|
|
56
|
+
title={fence.title}
|
|
41
57
|
selectedAnnotation={props.selectedAnnotation}
|
|
42
58
|
onSelectAnnotation={props.onSelectAnnotation}
|
|
43
59
|
/>
|
|
@@ -55,5 +71,13 @@ export function Fence(props: { fence: FenceModel } & SourceNavigation) {
|
|
|
55
71
|
/>
|
|
56
72
|
);
|
|
57
73
|
}
|
|
58
|
-
return
|
|
74
|
+
return (
|
|
75
|
+
<CodeBlock
|
|
76
|
+
lang={fence.lang}
|
|
77
|
+
source={fence.source}
|
|
78
|
+
highlights={fence.highlights}
|
|
79
|
+
notes={fence.notes}
|
|
80
|
+
title={fence.title}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
59
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
|
};
|