@echothink-ui/developer 0.1.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.
Files changed (56) hide show
  1. package/README.md +5 -0
  2. package/dist/components/APIExplorer.d.ts +2 -0
  3. package/dist/components/BranchSelector.d.ts +2 -0
  4. package/dist/components/CodeBlock.d.ts +2 -0
  5. package/dist/components/CodeEditor.d.ts +2 -0
  6. package/dist/components/CommitList.d.ts +2 -0
  7. package/dist/components/DiffTable.d.ts +2 -0
  8. package/dist/components/DiffViewer.d.ts +2 -0
  9. package/dist/components/EventPayloadViewer.d.ts +2 -0
  10. package/dist/components/GitRepositoryPanel.d.ts +2 -0
  11. package/dist/components/JSONViewer.d.ts +2 -0
  12. package/dist/components/LogConsole.d.ts +2 -0
  13. package/dist/components/PullRequestPanel.d.ts +2 -0
  14. package/dist/components/RequestResponseViewer.d.ts +2 -0
  15. package/dist/components/SchemaViewer.d.ts +2 -0
  16. package/dist/components/TerminalPanel.d.ts +2 -0
  17. package/dist/components/TraceTimeline.d.ts +2 -0
  18. package/dist/components/WebhookEventViewer.d.ts +2 -0
  19. package/dist/components/YAMLViewer.d.ts +2 -0
  20. package/dist/components/devUtils.d.ts +10 -0
  21. package/dist/components/types.d.ts +196 -0
  22. package/dist/index.cjs +2627 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.css +3651 -0
  25. package/dist/index.css.map +1 -0
  26. package/dist/index.d.ts +22 -0
  27. package/dist/index.js +2572 -0
  28. package/dist/index.js.map +1 -0
  29. package/package.json +43 -0
  30. package/src/components/APIExplorer.tsx +205 -0
  31. package/src/components/BranchSelector.tsx +54 -0
  32. package/src/components/CodeBlock.tsx +127 -0
  33. package/src/components/CodeEditor.tsx +95 -0
  34. package/src/components/CommitList.tsx +100 -0
  35. package/src/components/DiffTable.tsx +288 -0
  36. package/src/components/DiffViewer.tsx +145 -0
  37. package/src/components/EventPayloadViewer.tsx +91 -0
  38. package/src/components/GitRepositoryPanel.tsx +73 -0
  39. package/src/components/JSONViewer.tsx +189 -0
  40. package/src/components/LogConsole.tsx +160 -0
  41. package/src/components/PullRequestPanel.test.tsx +52 -0
  42. package/src/components/PullRequestPanel.tsx +215 -0
  43. package/src/components/RequestResponseViewer.test.tsx +45 -0
  44. package/src/components/RequestResponseViewer.tsx +169 -0
  45. package/src/components/SchemaViewer.tsx +157 -0
  46. package/src/components/TerminalPanel.test.tsx +33 -0
  47. package/src/components/TerminalPanel.tsx +134 -0
  48. package/src/components/TraceTimeline.test.tsx +63 -0
  49. package/src/components/TraceTimeline.tsx +207 -0
  50. package/src/components/WebhookEventViewer.test.tsx +57 -0
  51. package/src/components/WebhookEventViewer.tsx +184 -0
  52. package/src/components/YAMLViewer.tsx +207 -0
  53. package/src/components/devUtils.ts +81 -0
  54. package/src/components/types.ts +230 -0
  55. package/src/index.tsx +72 -0
  56. package/src/styles.css +4296 -0
@@ -0,0 +1,288 @@
1
+ import * as React from "react";
2
+ import { EmptyState } from "@echothink-ui/core";
3
+ import type { DiffTableChangeType, DiffTableProps, DiffTableRow } from "./types";
4
+
5
+ const changeTypes = new Set<DiffTableChangeType>([
6
+ "added",
7
+ "modified",
8
+ "removed",
9
+ "renamed",
10
+ "unchanged",
11
+ "conflict"
12
+ ]);
13
+
14
+ function getNumeric(row: DiffTableRow, keys: string[]) {
15
+ for (const key of keys) {
16
+ const value = row[key];
17
+ if (typeof value === "number" && Number.isFinite(value)) return value;
18
+ if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
19
+ return Number(value);
20
+ }
21
+ }
22
+ return 0;
23
+ }
24
+
25
+ function getPath(row: DiffTableRow) {
26
+ const value = row.afterPath ?? row.path ?? row.file ?? row.name;
27
+ return typeof value === "string" && value.trim() ? value : "Unknown file";
28
+ }
29
+
30
+ function getPreviousPath(row: DiffTableRow) {
31
+ const value = row.beforePath ?? row.previousPath;
32
+ return typeof value === "string" && value.trim() ? value : undefined;
33
+ }
34
+
35
+ function normalizeChangeType(value: unknown): DiffTableChangeType | undefined {
36
+ if (typeof value !== "string") return undefined;
37
+ const normalized = value
38
+ .trim()
39
+ .toLowerCase()
40
+ .replace(/[_\s]+/g, "-");
41
+ if (normalized === "delete" || normalized === "deleted" || normalized === "deletion") {
42
+ return "removed";
43
+ }
44
+ if (normalized === "add" || normalized === "created" || normalized === "create") {
45
+ return "added";
46
+ }
47
+ if (normalized === "rename" || normalized === "moved") {
48
+ return "renamed";
49
+ }
50
+ return changeTypes.has(normalized as DiffTableChangeType)
51
+ ? (normalized as DiffTableChangeType)
52
+ : undefined;
53
+ }
54
+
55
+ function inferChangeType(row: DiffTableRow): DiffTableChangeType {
56
+ const explicit = normalizeChangeType(row.changeType ?? row.status ?? row.kind ?? row.type);
57
+ if (explicit) return explicit;
58
+ if (getPreviousPath(row) && getPreviousPath(row) !== getPath(row)) return "renamed";
59
+
60
+ const added = getAdded(row);
61
+ const removed = getRemoved(row);
62
+ if (added > 0 && removed > 0) return "modified";
63
+ if (added > 0) return "added";
64
+ if (removed > 0) return "removed";
65
+ return "unchanged";
66
+ }
67
+
68
+ function getAdded(row: DiffTableRow) {
69
+ return getNumeric(row, ["added", "additions", "linesAdded", "insertions"]);
70
+ }
71
+
72
+ function getRemoved(row: DiffTableRow) {
73
+ return getNumeric(row, ["removed", "deletions", "deleted", "linesRemoved"]);
74
+ }
75
+
76
+ function changeLabel(type: DiffTableChangeType) {
77
+ if (type === "added") return "Added";
78
+ if (type === "removed") return "Removed";
79
+ if (type === "renamed") return "Renamed";
80
+ if (type === "unchanged") return "Unchanged";
81
+ if (type === "conflict") return "Conflict";
82
+ return "Modified";
83
+ }
84
+
85
+ function formatAdded(value: number) {
86
+ return value > 0 ? `+${value}` : "0";
87
+ }
88
+
89
+ function formatRemoved(value: number) {
90
+ return value > 0 ? `-${value}` : "0";
91
+ }
92
+
93
+ function formatNet(value: number) {
94
+ if (value > 0) return `+${value}`;
95
+ if (value < 0) return String(value);
96
+ return "0";
97
+ }
98
+
99
+ function defaultSummary(row: DiffTableRow, type: DiffTableChangeType) {
100
+ const hunks = getNumeric(row, ["hunks", "sections"]);
101
+ if (typeof row.summary !== "undefined") return row.summary;
102
+ if (type === "renamed") return "Path changed";
103
+ if (type === "unchanged") return "No line changes";
104
+ if (type === "conflict") return "Review required";
105
+ if (hunks > 0) return `${hunks} ${hunks === 1 ? "hunk" : "hunks"}`;
106
+ return "Line changes";
107
+ }
108
+
109
+ export function DiffTable({
110
+ rows,
111
+ mode = "side-by-side",
112
+ title = "Diff summary",
113
+ subtitle,
114
+ showSummary = true,
115
+ emptyState,
116
+ className,
117
+ "aria-label": ariaLabel,
118
+ ...props
119
+ }: DiffTableProps) {
120
+ const totals = React.useMemo(
121
+ () =>
122
+ rows.reduce(
123
+ (acc, row) => {
124
+ const type = inferChangeType(row);
125
+ acc.added += getAdded(row);
126
+ acc.removed += getRemoved(row);
127
+ if (type !== "unchanged") acc.changed += 1;
128
+ return acc;
129
+ },
130
+ { added: 0, removed: 0, changed: 0 }
131
+ ),
132
+ [rows]
133
+ );
134
+ const effectiveSubtitle =
135
+ subtitle ??
136
+ `${totals.changed} of ${rows.length} files changed, ${totals.added} additions, ${totals.removed} deletions`;
137
+ const label = ariaLabel ?? (typeof title === "string" ? title : "Diff summary");
138
+
139
+ return (
140
+ <section
141
+ {...props}
142
+ className={[
143
+ "eth-dev-diff-table",
144
+ `eth-dev-diff-table--${mode}`,
145
+ rows.length ? undefined : "eth-dev-diff-table--empty",
146
+ className
147
+ ]
148
+ .filter(Boolean)
149
+ .join(" ")}
150
+ data-eth-component="DiffTable"
151
+ aria-label={label}
152
+ >
153
+ <header className="eth-dev-diff-table__header">
154
+ <div className="eth-dev-diff-table__heading">
155
+ <h3>{title}</h3>
156
+ {effectiveSubtitle ? <p>{effectiveSubtitle}</p> : null}
157
+ </div>
158
+ {showSummary ? (
159
+ <dl className="eth-dev-diff-table__summary" aria-label="Diff totals">
160
+ <div>
161
+ <dt>Files</dt>
162
+ <dd>{totals.changed}</dd>
163
+ </div>
164
+ <div>
165
+ <dt>Added</dt>
166
+ <dd className="eth-dev-diff-table__summary-value--added">
167
+ {formatAdded(totals.added)}
168
+ </dd>
169
+ </div>
170
+ <div>
171
+ <dt>Removed</dt>
172
+ <dd className="eth-dev-diff-table__summary-value--removed">
173
+ {formatRemoved(totals.removed)}
174
+ </dd>
175
+ </div>
176
+ </dl>
177
+ ) : null}
178
+ </header>
179
+ {rows.length ? (
180
+ <div className="eth-dev-diff-table__viewport">
181
+ <table className="eth-dev-diff-table__table">
182
+ <caption className="eth-dev-diff-table__caption">{label}</caption>
183
+ <thead>
184
+ <tr>
185
+ <th scope="col">File</th>
186
+ <th scope="col">State</th>
187
+ {mode === "inline" ? (
188
+ <th scope="col">Changes</th>
189
+ ) : (
190
+ <>
191
+ <th scope="col">Added</th>
192
+ <th scope="col">Removed</th>
193
+ <th scope="col">Net</th>
194
+ </>
195
+ )}
196
+ <th scope="col">Summary</th>
197
+ </tr>
198
+ </thead>
199
+ <tbody>
200
+ {rows.map((row, index) => {
201
+ const added = getAdded(row);
202
+ const removed = getRemoved(row);
203
+ const type = inferChangeType(row);
204
+ const path = getPath(row);
205
+ const previousPath = getPreviousPath(row);
206
+ const key = row.id ?? `${path}-${index}`;
207
+
208
+ return (
209
+ <tr key={key} className={`eth-dev-diff-table__row--${type}`}>
210
+ <th scope="row">
211
+ <span className="eth-dev-diff-table__file">
212
+ <code>{path}</code>
213
+ {previousPath && previousPath !== path ? (
214
+ <span>was {previousPath}</span>
215
+ ) : null}
216
+ </span>
217
+ </th>
218
+ <td>
219
+ <span
220
+ className={`eth-dev-diff-table__state eth-dev-diff-table__state--${type}`}
221
+ >
222
+ {changeLabel(type)}
223
+ </span>
224
+ </td>
225
+ {mode === "inline" ? (
226
+ <td>
227
+ <span className="eth-dev-diff-table__inline-delta">
228
+ <span
229
+ className="eth-dev-diff-table__count eth-dev-diff-table__count--added"
230
+ aria-label={`${added} added lines`}
231
+ >
232
+ {formatAdded(added)}
233
+ </span>
234
+ <span
235
+ className="eth-dev-diff-table__count eth-dev-diff-table__count--removed"
236
+ aria-label={`${removed} removed lines`}
237
+ >
238
+ {formatRemoved(removed)}
239
+ </span>
240
+ </span>
241
+ </td>
242
+ ) : (
243
+ <>
244
+ <td>
245
+ <span
246
+ className="eth-dev-diff-table__count eth-dev-diff-table__count--added"
247
+ aria-label={`${added} added lines`}
248
+ >
249
+ {formatAdded(added)}
250
+ </span>
251
+ </td>
252
+ <td>
253
+ <span
254
+ className="eth-dev-diff-table__count eth-dev-diff-table__count--removed"
255
+ aria-label={`${removed} removed lines`}
256
+ >
257
+ {formatRemoved(removed)}
258
+ </span>
259
+ </td>
260
+ <td>
261
+ <span className="eth-dev-diff-table__count">
262
+ {formatNet(added - removed)}
263
+ </span>
264
+ </td>
265
+ </>
266
+ )}
267
+ <td>
268
+ <span className="eth-dev-diff-table__row-summary">
269
+ {defaultSummary(row, type)}
270
+ </span>
271
+ </td>
272
+ </tr>
273
+ );
274
+ })}
275
+ </tbody>
276
+ </table>
277
+ </div>
278
+ ) : (
279
+ (emptyState ?? (
280
+ <EmptyState
281
+ title="No diff rows"
282
+ description="No files have line-level changes in this comparison."
283
+ />
284
+ ))
285
+ )}
286
+ </section>
287
+ );
288
+ }
@@ -0,0 +1,145 @@
1
+ import * as React from "react";
2
+ import { Button } from "@echothink-ui/core";
3
+ import type { DiffViewerProps } from "./types";
4
+ import { buildLineDiff, type DiffLine } from "./devUtils";
5
+
6
+ type DiffViewerMode = NonNullable<DiffViewerProps["mode"]>;
7
+
8
+ export function DiffViewer({
9
+ before,
10
+ after,
11
+ mode = "inline",
12
+ language,
13
+ className,
14
+ ...props
15
+ }: DiffViewerProps) {
16
+ const [viewMode, setViewMode] = React.useState<DiffViewerMode>(mode);
17
+ const diff = React.useMemo(() => buildLineDiff(before, after), [before, after]);
18
+ const counts = React.useMemo(
19
+ () => ({
20
+ additions: diff.filter((line) => line.type === "add").length,
21
+ deletions: diff.filter((line) => line.type === "remove").length
22
+ }),
23
+ [diff]
24
+ );
25
+ const languageLabel = language?.trim() || "diff";
26
+
27
+ React.useEffect(() => setViewMode(mode), [mode]);
28
+
29
+ return (
30
+ <section
31
+ {...props}
32
+ className={`eth-dev-diff-viewer eth-dev-diff-viewer--${viewMode} ${className ?? ""}`.trim()}
33
+ aria-label={props["aria-label"] ?? `${languageLabel} diff viewer`}
34
+ data-eth-component="DiffViewer"
35
+ data-language={languageLabel}
36
+ >
37
+ <header className="eth-dev-diff-viewer__header">
38
+ <div className="eth-dev-diff-viewer__heading">
39
+ <span className="eth-dev-diff-viewer__language">{languageLabel}</span>
40
+ <span className="eth-dev-diff-viewer__mode">
41
+ {viewMode === "inline" ? "Inline" : "Side by side"}
42
+ </span>
43
+ </div>
44
+ <div className="eth-dev-diff-viewer__summary" aria-label="Diff summary">
45
+ <span className="eth-dev-diff-viewer__stat eth-dev-diff-viewer__stat--add">
46
+ <strong>{counts.additions}</strong> additions
47
+ </span>
48
+ <span className="eth-dev-diff-viewer__stat eth-dev-diff-viewer__stat--remove">
49
+ <strong>{counts.deletions}</strong> deletions
50
+ </span>
51
+ </div>
52
+ <div className="eth-dev-diff-viewer__view-switch" role="group" aria-label="Diff view mode">
53
+ <Button
54
+ intent={viewMode === "inline" ? "primary" : "secondary"}
55
+ density="compact"
56
+ aria-pressed={viewMode === "inline"}
57
+ onClick={() => setViewMode("inline")}
58
+ >
59
+ Inline
60
+ </Button>
61
+ <Button
62
+ intent={viewMode === "side-by-side" ? "primary" : "secondary"}
63
+ density="compact"
64
+ aria-pressed={viewMode === "side-by-side"}
65
+ onClick={() => setViewMode("side-by-side")}
66
+ >
67
+ Side by side
68
+ </Button>
69
+ </div>
70
+ </header>
71
+ {viewMode === "inline" ? (
72
+ <pre
73
+ className={`eth-dev-diff-viewer__inline language-${toLanguageClass(languageLabel)}`}
74
+ aria-label="Inline diff"
75
+ >
76
+ <code className="eth-dev-diff-viewer__code">
77
+ {diff.map((line, index) => (
78
+ <DiffLineView key={index} line={line} />
79
+ ))}
80
+ </code>
81
+ </pre>
82
+ ) : (
83
+ <div className="eth-dev-diff-viewer__split" role="table" aria-label="Side-by-side diff">
84
+ <div role="rowgroup">
85
+ <div className="eth-dev-diff-viewer__split-header" role="row">
86
+ <div role="columnheader">Before</div>
87
+ <div role="columnheader">After</div>
88
+ </div>
89
+ </div>
90
+ <div className="eth-dev-diff-viewer__split-body" role="rowgroup">
91
+ {diff.map((line, index) => (
92
+ <div
93
+ key={index}
94
+ className={`eth-dev-diff-viewer__split-row eth-dev-diff-viewer__split-row--${line.type}`}
95
+ role="row"
96
+ >
97
+ <DiffCell side="before" line={line} />
98
+ <DiffCell side="after" line={line} />
99
+ </div>
100
+ ))}
101
+ </div>
102
+ </div>
103
+ )}
104
+ </section>
105
+ );
106
+ }
107
+
108
+ function DiffLineView({ line }: { line: DiffLine }) {
109
+ return (
110
+ <span className={`eth-dev-diff-viewer__line eth-dev-diff-viewer__line--${line.type}`}>
111
+ <span className="eth-dev-diff-viewer__gutter">{line.beforeLine ?? line.afterLine ?? ""}</span>
112
+ <span className="eth-dev-diff-viewer__marker">{lineMarker(line.type)}</span>
113
+ <span className="eth-dev-diff-viewer__content">{line.text || " "}</span>
114
+ </span>
115
+ );
116
+ }
117
+
118
+ function DiffCell({ side, line }: { side: "before" | "after"; line: DiffLine }) {
119
+ const empty =
120
+ (side === "before" && line.type === "add") || (side === "after" && line.type === "remove");
121
+ const lineNumber = side === "before" ? line.beforeLine : line.afterLine;
122
+
123
+ return (
124
+ <pre
125
+ className={`eth-dev-diff-viewer__cell eth-dev-diff-viewer__cell--${empty ? "empty" : line.type}`}
126
+ role="cell"
127
+ >
128
+ <code className="eth-dev-diff-viewer__cell-code">
129
+ <span className="eth-dev-diff-viewer__gutter">{empty ? "" : (lineNumber ?? "")}</span>
130
+ <span className="eth-dev-diff-viewer__marker">{empty ? " " : lineMarker(line.type)}</span>
131
+ <span className="eth-dev-diff-viewer__content">{empty ? " " : line.text || " "}</span>
132
+ </code>
133
+ </pre>
134
+ );
135
+ }
136
+
137
+ function lineMarker(type: DiffLine["type"]) {
138
+ if (type === "add") return "+";
139
+ if (type === "remove") return "-";
140
+ return " ";
141
+ }
142
+
143
+ function toLanguageClass(language: string) {
144
+ return language.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
145
+ }
@@ -0,0 +1,91 @@
1
+ import * as React from "react";
2
+ import type { EventPayloadViewerProps } from "./types";
3
+ import { JSONViewer } from "./JSONViewer";
4
+
5
+ export function EventPayloadViewer({
6
+ eventId,
7
+ payload,
8
+ className,
9
+ "aria-label": ariaLabel,
10
+ ...props
11
+ }: EventPayloadViewerProps) {
12
+ const summary = describePayload(payload);
13
+
14
+ return (
15
+ <section
16
+ {...props}
17
+ className={`eth-dev-event-payload-viewer ${className ?? ""}`}
18
+ aria-label={ariaLabel ?? (eventId ? `Event payload ${eventId}` : "Event payload")}
19
+ data-eth-component="EventPayloadViewer"
20
+ >
21
+ <header className="eth-dev-event-payload-viewer__header">
22
+ <div className="eth-dev-event-payload-viewer__heading">
23
+ <span>Event payload</span>
24
+ <h3>{eventId ? <code>{eventId}</code> : "Payload"}</h3>
25
+ </div>
26
+ <dl className="eth-dev-event-payload-viewer__summary" aria-label="Payload summary">
27
+ <div>
28
+ <dt>Root</dt>
29
+ <dd>{summary.root}</dd>
30
+ </div>
31
+ <div>
32
+ <dt>{summary.countLabel}</dt>
33
+ <dd>{summary.countValue}</dd>
34
+ </div>
35
+ <div>
36
+ <dt>Size</dt>
37
+ <dd>{summary.size}</dd>
38
+ </div>
39
+ </dl>
40
+ </header>
41
+ <JSONViewer className="eth-dev-event-payload-viewer__json" value={payload} defaultExpandedDepth={3} />
42
+ </section>
43
+ );
44
+ }
45
+
46
+ function describePayload(value: unknown) {
47
+ const text = serializePayload(value);
48
+
49
+ if (Array.isArray(value)) {
50
+ return {
51
+ root: "array",
52
+ countLabel: "Items",
53
+ countValue: String(value.length),
54
+ size: formatPayloadSize(text.length)
55
+ };
56
+ }
57
+
58
+ if (value !== null && typeof value === "object") {
59
+ return {
60
+ root: "object",
61
+ countLabel: "Fields",
62
+ countValue: String(Object.keys(value as Record<string, unknown>).length),
63
+ size: formatPayloadSize(text.length)
64
+ };
65
+ }
66
+
67
+ return {
68
+ root: value === null ? "null" : typeof value,
69
+ countLabel: "Shape",
70
+ countValue: "scalar",
71
+ size: formatPayloadSize(text.length)
72
+ };
73
+ }
74
+
75
+ function serializePayload(value: unknown) {
76
+ if (typeof value === "string") return value;
77
+ if (typeof value === "undefined") return "";
78
+
79
+ try {
80
+ return JSON.stringify(value, null, 2) ?? "";
81
+ } catch {
82
+ return String(value);
83
+ }
84
+ }
85
+
86
+ function formatPayloadSize(size: number) {
87
+ if (size < 1000) return `${size} B`;
88
+ if (size < 1000 * 1000) return `${(size / 1000).toFixed(1)} KB`;
89
+
90
+ return `${(size / (1000 * 1000)).toFixed(1)} MB`;
91
+ }
@@ -0,0 +1,73 @@
1
+ import * as React from "react";
2
+ import { Branch, Commit, GitRepo, Launch, PullRequest, Time } from "@carbon/icons-react";
3
+ import type { GitRepositoryPanelProps } from "./types";
4
+
5
+ export function GitRepositoryPanel({ repo, className, ...props }: GitRepositoryPanelProps) {
6
+ const summaryItems = [
7
+ {
8
+ label: "Default branch",
9
+ value: repo.defaultBranch,
10
+ icon: <Branch size={16} />,
11
+ valueClassName: "eth-dev-git-repository-panel__metric-value--code"
12
+ },
13
+ {
14
+ label: "Open PRs",
15
+ value: String(repo.openPrCount ?? 0),
16
+ icon: <PullRequest size={16} />
17
+ },
18
+ {
19
+ label: "Commits",
20
+ value: String(repo.commitCount ?? 0),
21
+ icon: <Commit size={16} />
22
+ },
23
+ {
24
+ label: "Last commit",
25
+ value: repo.lastCommitAt ?? "No activity",
26
+ icon: <Time size={16} />,
27
+ valueClassName: "eth-dev-git-repository-panel__metric-value--code"
28
+ }
29
+ ];
30
+
31
+ return (
32
+ <section
33
+ {...props}
34
+ aria-label={props["aria-label"] ?? `${repo.name} repository summary`}
35
+ className={`eth-dev-git-repository-panel ${className ?? ""}`}
36
+ data-eth-component="GitRepositoryPanel"
37
+ >
38
+ <header className="eth-dev-git-repository-panel__header">
39
+ <div className="eth-dev-git-repository-panel__identity">
40
+ <span className="eth-dev-git-repository-panel__icon" aria-hidden="true">
41
+ <GitRepo size={20} />
42
+ </span>
43
+ <div className="eth-dev-git-repository-panel__heading">
44
+ <p>Repository</p>
45
+ <h3>{repo.name}</h3>
46
+ <a className="eth-dev-git-repository-panel__url" href={repo.url} title={repo.url}>
47
+ {repo.url}
48
+ </a>
49
+ </div>
50
+ </div>
51
+ <a
52
+ className="eth-dev-git-repository-panel__action"
53
+ href={repo.url}
54
+ aria-label={`Open ${repo.name} repository`}
55
+ >
56
+ <span>Open repository</span>
57
+ <Launch size={16} aria-hidden="true" />
58
+ </a>
59
+ </header>
60
+ <dl className="eth-dev-git-repository-panel__metrics" aria-label="Repository metrics">
61
+ {summaryItems.map((item) => (
62
+ <div key={item.label} className="eth-dev-git-repository-panel__metric">
63
+ <dt>
64
+ <span aria-hidden="true">{item.icon}</span>
65
+ {item.label}
66
+ </dt>
67
+ <dd className={item.valueClassName}>{item.value}</dd>
68
+ </div>
69
+ ))}
70
+ </dl>
71
+ </section>
72
+ );
73
+ }