@crvouga/atlas 0.10.2 → 0.12.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 (114) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -0
  3. package/dist/reports/src/catalog-health.d.ts +142 -0
  4. package/dist/reports/src/catalog-health.js +257 -0
  5. package/dist/reports/src/test-catalog.d.ts +8 -0
  6. package/dist/reports/src/test-catalog.js +5 -1
  7. package/dist/schema/src/api.d.ts +79 -0
  8. package/dist/schema/src/api.js +50 -0
  9. package/dist/server/src/app.js +9 -0
  10. package/dist/server/src/llms.js +1 -0
  11. package/dist/server/src/openapi.js +18 -2
  12. package/dist/server/src/settings-page.d.ts +4 -4
  13. package/dist/server/src/settings-page.js +17 -78
  14. package/docs/reference/http-api.md +34 -0
  15. package/docs/reference/reports.md +224 -0
  16. package/docs/reference/schema.md +10 -0
  17. package/docs/reference/server.md +4 -4
  18. package/docs/server.md +10 -0
  19. package/docs/ui.md +142 -0
  20. package/docs/workspaces.md +130 -0
  21. package/llms-full.txt +322 -0
  22. package/llms.txt +2 -0
  23. package/package.json +17 -10
  24. package/packages/schema/schemas/catalog-health.schema.json +400 -0
  25. package/packages/server/openapi.json +596 -0
  26. package/packages/visualizer/src/admin/SettingsPage.tsx +456 -0
  27. package/packages/visualizer/src/admin/main.tsx +11 -0
  28. package/packages/visualizer/src/app/AppShell.tsx +62 -38
  29. package/packages/visualizer/src/app/gazetteer.tsx +90 -83
  30. package/packages/visualizer/src/app/router.tsx +33 -15
  31. package/packages/visualizer/src/components/ActivityIndicator.tsx +4 -5
  32. package/packages/visualizer/src/components/DataIssues.tsx +36 -54
  33. package/packages/visualizer/src/components/Header.tsx +197 -125
  34. package/packages/visualizer/src/components/Legend.tsx +13 -41
  35. package/packages/visualizer/src/components/ProjectPicker.tsx +61 -26
  36. package/packages/visualizer/src/components/Screen.tsx +80 -71
  37. package/packages/visualizer/src/components/SpecLogo.tsx +15 -0
  38. package/packages/visualizer/src/components/Status.tsx +20 -34
  39. package/packages/visualizer/src/components/labels.tsx +8 -3
  40. package/packages/visualizer/src/components/spec-logos/APACHE-2.0.txt +206 -0
  41. package/packages/visualizer/src/components/spec-logos/CAMUNDA-MIT.txt +21 -0
  42. package/packages/visualizer/src/components/spec-logos/FILE-ICONS-ISC.txt +13 -0
  43. package/packages/visualizer/src/components/spec-logos/README.md +23 -0
  44. package/packages/visualizer/src/components/spec-logos/SIMPLE-ICONS-CC0.txt +30 -0
  45. package/packages/visualizer/src/components/spec-logos/alloy.svg +1 -0
  46. package/packages/visualizer/src/components/spec-logos/dmn.svg +3 -0
  47. package/packages/visualizer/src/components/spec-logos/gherkin.svg +1 -0
  48. package/packages/visualizer/src/components/spec-logos/openslo.svg +63 -0
  49. package/packages/visualizer/src/components/spec-logos/quint.svg +1 -0
  50. package/packages/visualizer/src/evidence/artifacts.tsx +263 -110
  51. package/packages/visualizer/src/evidence/chart-source.tsx +18 -0
  52. package/packages/visualizer/src/evidence/check-detail.tsx +283 -139
  53. package/packages/visualizer/src/evidence/decision-table.tsx +35 -26
  54. package/packages/visualizer/src/evidence/document-page.tsx +51 -29
  55. package/packages/visualizer/src/evidence/feature-page.tsx +121 -53
  56. package/packages/visualizer/src/evidence/marks.tsx +112 -79
  57. package/packages/visualizer/src/evidence/model-page.tsx +77 -46
  58. package/packages/visualizer/src/evidence/model-trace.tsx +55 -35
  59. package/packages/visualizer/src/evidence/overview.tsx +137 -69
  60. package/packages/visualizer/src/evidence/recording-page.tsx +70 -31
  61. package/packages/visualizer/src/evidence/recording.tsx +135 -68
  62. package/packages/visualizer/src/evidence/snapshot.tsx +99 -43
  63. package/packages/visualizer/src/evidence/source-text.tsx +33 -0
  64. package/packages/visualizer/src/evidence/sources-panel.tsx +138 -144
  65. package/packages/visualizer/src/evidence/spec-header.tsx +52 -36
  66. package/packages/visualizer/src/evidence/spec-index.tsx +61 -32
  67. package/packages/visualizer/src/evidence/spec-page.tsx +56 -25
  68. package/packages/visualizer/src/evidence/test-filter.tsx +38 -14
  69. package/packages/visualizer/src/explorer/Explorer.tsx +688 -261
  70. package/packages/visualizer/src/explorer/ExplorerIndex.tsx +136 -68
  71. package/packages/visualizer/src/explorer/RunsView.tsx +307 -94
  72. package/packages/visualizer/src/explorer/TestDetail.tsx +393 -118
  73. package/packages/visualizer/src/explorer/parts.tsx +35 -13
  74. package/packages/visualizer/src/find/Finder.tsx +106 -90
  75. package/packages/visualizer/src/find/open.ts +17 -20
  76. package/packages/visualizer/src/live/test-progress.tsx +60 -22
  77. package/packages/visualizer/src/looks/Stage.tsx +223 -110
  78. package/packages/visualizer/src/main.tsx +9 -11
  79. package/packages/visualizer/src/map/ChartMap.tsx +125 -69
  80. package/packages/visualizer/src/map/EventEdge.tsx +59 -37
  81. package/packages/visualizer/src/map/map.module.css +12 -546
  82. package/packages/visualizer/src/map/nodes.tsx +124 -70
  83. package/packages/visualizer/src/reachable.test.ts +3 -2
  84. package/packages/visualizer/src/spec-kind.ts +2 -2
  85. package/packages/visualizer/src/ui/grammars/ALLOY-MIT.txt +21 -0
  86. package/packages/visualizer/src/ui/grammars/QUINT-APACHE-2.0.txt +206 -0
  87. package/packages/visualizer/src/ui/grammars/README.md +14 -0
  88. package/packages/visualizer/src/ui/grammars/alloy.json +71 -0
  89. package/packages/visualizer/src/ui/grammars/quint.json +152 -0
  90. package/packages/visualizer/src/ui/highlight.ts +50 -0
  91. package/packages/visualizer/src/ui/theme.tsx +109 -0
  92. package/packages/visualizer/src/views/ChartPage.tsx +46 -26
  93. package/packages/visualizer/src/views/ChartTests.tsx +59 -22
  94. package/packages/visualizer/src/views/ProductPage.tsx +16 -15
  95. package/packages/visualizer/src/views/SidePanel.tsx +241 -131
  96. package/packages/visualizer/src/app/gazetteer.module.css +0 -107
  97. package/packages/visualizer/src/components/ActivityIndicator.module.css +0 -4
  98. package/packages/visualizer/src/components/DataIssues.module.css +0 -136
  99. package/packages/visualizer/src/components/Header.module.css +0 -114
  100. package/packages/visualizer/src/components/Legend.module.css +0 -84
  101. package/packages/visualizer/src/components/Screen.module.css +0 -312
  102. package/packages/visualizer/src/components/Status.module.css +0 -116
  103. package/packages/visualizer/src/components/icons.tsx +0 -156
  104. package/packages/visualizer/src/evidence/artifacts.module.css +0 -213
  105. package/packages/visualizer/src/evidence/evidence.module.css +0 -907
  106. package/packages/visualizer/src/evidence/recording.module.css +0 -460
  107. package/packages/visualizer/src/evidence/sources.module.css +0 -168
  108. package/packages/visualizer/src/evidence/spec-snapshot.tsx +0 -26
  109. package/packages/visualizer/src/explorer/explorer.module.css +0 -1238
  110. package/packages/visualizer/src/find/Finder.module.css +0 -168
  111. package/packages/visualizer/src/looks/stage.module.css +0 -300
  112. package/packages/visualizer/src/styles/global.css +0 -105
  113. package/packages/visualizer/src/styles/tokens.css +0 -80
  114. package/packages/visualizer/src/views/views.module.css +0 -546
@@ -1,17 +1,16 @@
1
+ import { IconArrowLeft } from '@tabler/icons-react';
1
2
  import { checkHistory, type CheckSlot, type HistoryEntry } from '@crvouga/atlas/reports/evidence-view';
2
3
  import type { RunReport } from '@crvouga/atlas/reports/report-types';
4
+ import { Alert, Box, Button, Container, Flex, Text, Title } from '@mantine/core';
3
5
  import { useQuery } from '@tanstack/react-query';
4
6
  import { useMemo } from 'react';
5
-
7
+ import { TestProgress, useTestProgress } from '../live/test-progress';
6
8
  import { reportText } from './api';
7
9
  import { useRecordedReport } from './check-detail';
8
10
  import { useEvidence } from './evidence-context';
9
- import styles from './evidence.module.css';
10
11
  import { duration, statusWord, useNow } from './format';
11
12
  import { Origin, StatusMark } from './marks';
12
13
  import { Timeline } from './recording';
13
- import { TestProgress, useTestProgress } from '../live/test-progress';
14
-
15
14
  type Props = {
16
15
  /** The check whose recording is open, once the combined report holds it. */
17
16
  slot: CheckSlot | undefined;
@@ -22,7 +21,6 @@ type Props = {
22
21
  onMoment: (moment: number, replace: boolean) => void;
23
22
  onClose: () => void;
24
23
  };
25
-
26
24
  /** HTML snapshots are the only pictures that paint with the run's stylesheet. */
27
25
  function needsStylesheet(report: RunReport | undefined): boolean {
28
26
  if (!report) return false;
@@ -37,7 +35,6 @@ function needsStylesheet(report: RunReport | undefined): boolean {
37
35
  };
38
36
  return visit(report.steps) || (report.artifacts ?? []).some((artifact) => artifact.media === 'html');
39
37
  }
40
-
41
38
  /** One recorded test, opened in place of its spec. */
42
39
  export function RecordingPage({ slot, layerId, label, back, moment, onMoment, onClose }: Props) {
43
40
  const progress = useTestProgress(slot ?? null);
@@ -45,45 +42,87 @@ export function RecordingPage({ slot, layerId, label, back, moment, onMoment, on
45
42
  const now = useNow();
46
43
  const history = useMemo(() => (slot ? checkHistory(evidence, slot.specPath, slot.id) : []), [evidence, slot]);
47
44
  const winner = slot?.outcomes[0];
48
- const chosen: HistoryEntry | null = (layerId ? history.find((entry) => entry.layer.id === layerId) : undefined) ?? (winner ? { layer: winner.layer, row: winner.row } : null);
45
+ const chosen: HistoryEntry | null =
46
+ (layerId ? history.find((entry) => entry.layer.id === layerId) : undefined) ??
47
+ (winner ? { layer: winner.layer, row: winner.row } : null);
49
48
  const { paths, report } = useRecordedReport(chosen);
50
49
  const wantsCss = needsStylesheet(report.data);
51
- const css = useQuery({ queryKey: ['evidence-css', paths?.css ?? ''], queryFn: ({ signal }) => reportText(paths!.css, signal), enabled: paths !== null && wantsCss, staleTime: Number.POSITIVE_INFINITY, retry: false });
50
+ const css = useQuery({
51
+ queryKey: ['evidence-css', paths?.css ?? ''],
52
+ queryFn: ({ signal }) => reportText(paths!.css, signal),
53
+ enabled: paths !== null && wantsCss,
54
+ staleTime: Number.POSITIVE_INFINITY,
55
+ retry: false
56
+ });
52
57
  const waiting = !chosen && !unavailable && (!ready || pending > 0);
53
58
  return (
54
- <article className={styles.page} data-wide="true" aria-label="Recorded test">
55
- <button type="button" className={styles.back} onClick={onClose}>
56
- <span aria-hidden="true">←</span> {back}
57
- </button>
58
- <header className={styles.specHead}>
59
- <h1 className={styles.title}>{label}</h1>
59
+ <Container data-wide="true" aria-label="Recorded test" component="article" size="xl" w="100%" py="xl">
60
+ <Button type="button" onClick={onClose} variant="subtle">
61
+ <Text aria-hidden="true" span inherit>
62
+ <IconArrowLeft size={16} aria-hidden="true" />
63
+ </Text>{' '}
64
+ {back}
65
+ </Button>
66
+ <Box component="header" mb="lg">
67
+ <Title order={1} mb="sm">
68
+ {label}
69
+ </Title>
60
70
  {chosen && (
61
- <p className={styles.detailMeta}>
71
+ <Flex gap="xs" align="center" wrap="wrap" c="dimmed">
62
72
  <StatusMark status={chosen.row.status} />
63
- <strong data-status={chosen.row.status}>{statusWord(chosen.row.status)}</strong>
64
- {chosen.row.durationMs !== null && <span>{duration(chosen.row.durationMs)}</span>}
73
+ <Text data-status={chosen.row.status} span inherit fw={600}>
74
+ {statusWord(chosen.row.status)}
75
+ </Text>
76
+ {chosen.row.durationMs !== null && (
77
+ <Text span inherit>
78
+ {duration(chosen.row.durationMs)}
79
+ </Text>
80
+ )}
65
81
  <Origin layer={chosen.layer} backends={backends} now={now} />
66
- </p>
82
+ </Flex>
67
83
  )}
68
- </header>
84
+ </Box>
69
85
  <TestProgress {...progress} />
70
- {waiting && <p className={styles.aside} role="status">Loading the recording…</p>}
71
- {!chosen && !waiting && !progress.tests.length && <p className={styles.notice}>{unavailable ?? 'This result is not in the combined report any more. A newer run may have replaced it, or its source is switched off.'}</p>}
72
- {chosen && !paths && <p className={styles.notice}>This result has an outcome and no recorded timeline.</p>}
73
- {report.isLoading && <p className={styles.aside} role="status">Loading the recording…</p>}
86
+ {waiting && (
87
+ <Text role="status" component="div" c="dimmed">
88
+ Loading the recording…
89
+ </Text>
90
+ )}
91
+ {!chosen && !waiting && !progress.tests.length && (
92
+ <Alert color={'blue'} my="sm">
93
+ {unavailable ??
94
+ 'This result is not in the combined report any more. A newer run may have replaced it, or its source is switched off.'}
95
+ </Alert>
96
+ )}
97
+ {chosen && !paths && (
98
+ <Alert color={'blue'} my="sm">
99
+ This result has an outcome and no recorded timeline.
100
+ </Alert>
101
+ )}
102
+ {report.isLoading && (
103
+ <Text role="status" component="div" c="dimmed">
104
+ Loading the recording…
105
+ </Text>
106
+ )}
74
107
  {report.isError && (
75
- <p className={styles.notice} role="alert">
108
+ <Alert role="alert" color={'red'} my="sm">
76
109
  The recording could not be read. {report.error.message}{' '}
77
- <button type="button" className={styles.link} onClick={() => void report.refetch()}>Try again</button>
78
- </p>
110
+ <Button type="button" onClick={() => void report.refetch()} variant="subtle">
111
+ Try again
112
+ </Button>
113
+ </Alert>
79
114
  )}
80
115
  {wantsCss && css.isError && (
81
- <p className={styles.notice} role="alert">
116
+ <Alert role="alert" color={'red'} my="sm">
82
117
  The recorded stylesheet could not be read, so pictures appear unstyled.{' '}
83
- <button type="button" className={styles.link} onClick={() => void css.refetch()}>Try again</button>
84
- </p>
118
+ <Button type="button" onClick={() => void css.refetch()} variant="subtle">
119
+ Try again
120
+ </Button>
121
+ </Alert>
122
+ )}
123
+ {report.data && paths && (
124
+ <Timeline report={report.data} fileUrl={paths.file} css={css.data ?? ''} moment={moment} onMoment={onMoment} />
85
125
  )}
86
- {report.data && paths && <Timeline report={report.data} fileUrl={paths.file} css={css.data ?? ''} moment={moment} onMoment={onMoment} />}
87
- </article>
126
+ </Container>
88
127
  );
89
128
  }
@@ -1,16 +1,14 @@
1
1
  import { flattenSteps, neighborFailure, orderedUiBlobs, selectedFrame, testFrames } from '@crvouga/atlas/reports/frames';
2
2
  import { performanceFrameIndex, performanceRows, type PerformanceRow } from '@crvouga/atlas/reports/performance';
3
3
  import type { ReportStep, RunReport } from '@crvouga/atlas/reports/report-types';
4
+ import { Accordion, Box, Button, Code, List, NativeSelect, Progress, Stack, Tabs, Text, VisuallyHidden } from '@mantine/core';
4
5
  import { useMemo, useState } from 'react';
5
-
6
6
  import { reportLooks } from '../looks/collect';
7
7
  import { Stage, type LookMode } from '../looks/Stage';
8
8
  import { reportAssetUrl } from './api';
9
9
  import { duration } from './format';
10
10
  import { StatusMark } from './marks';
11
- import styles from './recording.module.css';
12
11
  import { Snapshot, SnapshotData } from './snapshot';
13
-
14
12
  type Props = {
15
13
  report: RunReport;
16
14
  /** Where a captured file of this test is read from. */
@@ -20,7 +18,6 @@ type Props = {
20
18
  moment: number | null;
21
19
  onMoment: (moment: number, replace: boolean) => void;
22
20
  };
23
-
24
21
  /** A recorded test, one captured moment at a time. */
25
22
  export function Timeline({ report, fileUrl, css, moment, onMoment }: Props) {
26
23
  const [actor, setActor] = useState('all');
@@ -30,7 +27,11 @@ export function Timeline({ report, fileUrl, css, moment, onMoment }: Props) {
30
27
  const frames = useMemo(() => testFrames(test), [test]);
31
28
  const looks = useMemo(() => reportLooks(report, (file) => reportAssetUrl(fileUrl(file))), [report, fileUrl]);
32
29
  const extras = (report.artifacts ?? []).filter((artifact) => artifact.media !== 'png' && artifact.media !== 'video');
33
- const frame = selectedFrame(frames, { view: 'test', testId: report.meta.testId, params: { frame: moment === null ? '' : String(moment) } });
30
+ const frame = selectedFrame(frames, {
31
+ view: 'test',
32
+ testId: report.meta.testId,
33
+ params: { frame: moment === null ? '' : String(moment) }
34
+ });
34
35
  const index = frame?.index ?? 0;
35
36
  const last = frames.length - 1;
36
37
  const go = (target: number) => {
@@ -53,53 +54,67 @@ export function Timeline({ report, fileUrl, css, moment, onMoment }: Props) {
53
54
  go(failure.index);
54
55
  }
55
56
  };
56
-
57
57
  return (
58
- <div className={styles.recording}>
58
+ <Stack gap="sm">
59
59
  {report.meta.error && (
60
- <div className={styles.error} role="alert">
60
+ <Stack role="alert" gap="sm" c="red.7">
61
61
  {report.meta.error}
62
62
  {report.meta.errorStack && (
63
- <details>
64
- <summary>Where it failed</summary>
65
- {report.meta.errorStack}
66
- </details>
63
+ <Accordion variant="separated" my="sm">
64
+ <Accordion.Item value="content">
65
+ <Accordion.Control>Where it failed</Accordion.Control>
66
+ <Accordion.Panel>{report.meta.errorStack}</Accordion.Panel>
67
+ </Accordion.Item>
68
+ </Accordion>
67
69
  )}
68
- </div>
70
+ </Stack>
69
71
  )}
70
- <div className={styles.tabs} role="tablist" aria-label="Recording">
71
- <button type="button" role="tab" aria-selected={tab === 'moments'} onClick={() => setTab('moments')}>Moments</button>
72
- <button type="button" role="tab" aria-selected={tab === 'timing'} onClick={() => setTab('timing')}>Timing</button>
73
- </div>
72
+ <Tabs value={tab} onChange={(value) => setTab(value as typeof tab)} mb="md">
73
+ <Tabs.List aria-label="Recording">
74
+ <Tabs.Tab value="moments">Moments</Tabs.Tab>
75
+ <Tabs.Tab value="timing">Timing</Tabs.Tab>
76
+ </Tabs.List>
77
+ </Tabs>
74
78
  {tab === 'timing' ? (
75
79
  <Timing rows={rows} canOpen={(step) => byStep.has(step)} open={jump} />
76
80
  ) : (
77
- <div className={styles.split}>
78
- <ol className={styles.steps} aria-label="Recorded steps">
81
+ <Stack gap="sm">
82
+ <List aria-label="Recorded steps" spacing="xs" listStyleType="none" type="ordered">
79
83
  {steps.map((entry) => {
80
84
  const current = frame?.step === entry.step;
81
85
  return (
82
- <li key={entry.index} style={{ paddingLeft: entry.depth * 14 }}>
83
- <button type="button" data-status={entry.step.status} aria-current={current ? 'step' : undefined} disabled={!byStep.has(entry.step)} onClick={() => jump(entry.step)}>
86
+ <List.Item key={entry.index} pl={entry.depth ? 'md' : 0}>
87
+ <Button
88
+ type="button"
89
+ data-status={entry.step.status}
90
+ aria-current={current ? 'step' : undefined}
91
+ disabled={!byStep.has(entry.step)}
92
+ onClick={() => jump(entry.step)}
93
+ variant="light"
94
+ >
84
95
  <StatusMark status={entry.step.status} size={14} />
85
- <span className={styles.stepTitle}>{entry.step.title}</span>
86
- <span className={styles.stepTime}>{duration(entry.step.durationMs)}</span>
87
- </button>
88
- </li>
96
+ <Text span inherit>
97
+ {entry.step.title}
98
+ </Text>
99
+ <Text span inherit c="dimmed">
100
+ {duration(entry.step.durationMs)}
101
+ </Text>
102
+ </Button>
103
+ </List.Item>
89
104
  );
90
105
  })}
91
- </ol>
92
- <section className={styles.stage} aria-label="Recorded moment">
106
+ </List>
107
+ <Box aria-label="Recorded moment" component="section" mb="lg">
93
108
  {report.meta.actorIds.length > 1 && (
94
- <label className={styles.actor}>
95
- <span className="sr-only">Person</span>
96
- <select value={actor} onChange={(event) => setActor(event.target.value)}>
109
+ <Box component="label">
110
+ <VisuallyHidden>Person</VisuallyHidden>
111
+ <NativeSelect value={actor} onChange={(event) => setActor(event.target.value)}>
97
112
  <option value="all">Everyone</option>
98
113
  {report.meta.actorIds.map((id) => (
99
114
  <option key={id}>{id}</option>
100
115
  ))}
101
- </select>
102
- </label>
116
+ </NativeSelect>
117
+ </Box>
103
118
  )}
104
119
  <Stage
105
120
  looks={looks}
@@ -110,79 +125,131 @@ export function Timeline({ report, fileUrl, css, moment, onMoment }: Props) {
110
125
  onMoment={go}
111
126
  failure={frames.some((entry) => entry.step.status === 'failed')}
112
127
  onFailure={nextFailure}
113
- empty={extras.length > 0
114
- ? <p className={styles.quietLine}>This runner attached files instead of a step-by-step timeline. They are below.</p>
115
- : <p className={styles.quietLine}>This test recorded no picture, video, or timeline. Its steps and outcome are on the left.</p>}
128
+ empty={
129
+ extras.length > 0 ? (
130
+ <Text c="dimmed">This runner attached files instead of a step-by-step timeline. They are below.</Text>
131
+ ) : (
132
+ <Text c="dimmed">This test recorded no picture, video, or timeline. Its steps and outcome are on the left.</Text>
133
+ )
134
+ }
116
135
  renderMoment={(at) => {
117
136
  const current = frames[at];
118
137
  if (!current) return null;
119
138
  const ui = orderedUiBlobs(current, test);
120
- const pictures = ui.filter((entry) => entry.blob && entry.blob.media !== 'video' && (actor === 'all' || entry.nodeId === actor));
139
+ const pictures = ui.filter(
140
+ (entry) => entry.blob && entry.blob.media !== 'video' && (actor === 'all' || entry.nodeId === actor)
141
+ );
121
142
  const absent = ui.filter((entry) => !entry.blob && (actor === 'all' || !entry.nodeId || entry.nodeId === actor));
122
- const observations = current.frame.snapshots.filter((blob) => blob.media !== 'html' && blob.media !== 'png' && blob.media !== 'video' && (actor === 'all' || !blob.nodeId || blob.nodeId === actor));
143
+ const observations = current.frame.snapshots.filter(
144
+ (blob) =>
145
+ blob.media !== 'html' &&
146
+ blob.media !== 'png' &&
147
+ blob.media !== 'video' &&
148
+ (actor === 'all' || !blob.nodeId || blob.nodeId === actor)
149
+ );
123
150
  return (
124
151
  <>
125
- <p className={styles.quietLine}>
126
- {current.step.status === 'failed' ? 'Failed' : 'Passed'} · at {duration(Math.max(0, current.frame.capturedAtMs - report.meta.startedAtMs)) || '0 ms'}
152
+ <Text c="dimmed">
153
+ {current.step.status === 'failed' ? 'Failed' : 'Passed'} · at{' '}
154
+ {duration(Math.max(0, current.frame.capturedAtMs - report.meta.startedAtMs)) || '0 ms'}
127
155
  {current.frame.hit && <> · acted on {current.frame.hit.path.join(' › ')}</>}
128
- </p>
129
- {current.step.error && <pre className={styles.error} role="alert">{current.step.error}</pre>}
156
+ </Text>
157
+ {current.step.error && (
158
+ <Code role="alert" c="dimmed">
159
+ {current.step.error}
160
+ </Code>
161
+ )}
130
162
  {current.step.detail && (
131
- <div className={styles.observed}>
163
+ <Stack>
132
164
  <SnapshotData text={current.step.detail} />
133
- </div>
165
+ </Stack>
134
166
  )}
135
- <div className={styles.pictures} aria-label="Recorded snapshots">
167
+ <Stack aria-label="Recorded snapshots">
136
168
  {pictures.map((entry) => (
137
- <Snapshot key={entry.blob!.file} url={fileUrl(entry.blob!.file)} blob={entry.blob!} css={css} hit={entry.stale ? null : current.frame.hit ?? null} earlier={entry.stale} />
169
+ <Snapshot
170
+ key={entry.blob!.file}
171
+ url={fileUrl(entry.blob!.file)}
172
+ blob={entry.blob!}
173
+ css={css}
174
+ hit={entry.stale ? null : (current.frame.hit ?? null)}
175
+ earlier={entry.stale}
176
+ />
138
177
  ))}
139
178
  {absent.map((entry) => (
140
- <p key={entry.nodeId} className={styles.quietLine}>No picture of {entry.nodeId} had been recorded by this moment.</p>
179
+ <Text key={entry.nodeId} c="dimmed">
180
+ No picture of {entry.nodeId} had been recorded by this moment.
181
+ </Text>
141
182
  ))}
142
183
  {observations.map((blob) => (
143
184
  <Snapshot key={blob.file} url={fileUrl(blob.file)} blob={blob} css={css} hit={null} />
144
185
  ))}
145
- </div>
186
+ </Stack>
146
187
  </>
147
188
  );
148
189
  }}
149
190
  />
150
191
  {extras.length > 0 && (
151
- <div className={styles.pictures} aria-label="Other attachments">
192
+ <Stack aria-label="Other attachments" gap="sm">
152
193
  {extras.map((artifact) => (
153
- <Snapshot key={artifact.file} url={fileUrl(artifact.file)} blob={{ name: artifact.name, media: artifact.media, file: artifact.file }} css={css} hit={null} />
194
+ <Snapshot
195
+ key={artifact.file}
196
+ url={fileUrl(artifact.file)}
197
+ blob={{ name: artifact.name, media: artifact.media, file: artifact.file }}
198
+ css={css}
199
+ hit={null}
200
+ />
154
201
  ))}
155
- </div>
202
+ </Stack>
156
203
  )}
157
- </section>
158
- </div>
204
+ </Box>
205
+ </Stack>
159
206
  )}
160
- </div>
207
+ </Stack>
161
208
  );
162
209
  }
163
-
164
210
  /** Where the recorded time went. A step without a measurement is never shown as quick. */
165
- function Timing({ rows, canOpen, open }: { rows: readonly PerformanceRow[]; canOpen: (step: ReportStep) => boolean; open: (step: ReportStep) => void }) {
211
+ function Timing({
212
+ rows,
213
+ canOpen,
214
+ open
215
+ }: {
216
+ rows: readonly PerformanceRow[];
217
+ canOpen: (step: ReportStep) => boolean;
218
+ open: (step: ReportStep) => void;
219
+ }) {
166
220
  const measured = rows.filter((row) => row.band !== 'unmeasured');
167
- if (!measured.length) return <p className={styles.quietLine}>Measurements are unavailable for this recording.</p>;
221
+ if (!measured.length)
222
+ return (
223
+ <Text component="div" c="dimmed">
224
+ Measurements are unavailable for this recording.
225
+ </Text>
226
+ );
168
227
  const longest = Math.max(...measured.map((row) => row.step.performance?.selfMs ?? 0), 1);
169
228
  return (
170
- <ol className={styles.timing} aria-label="Recorded time per step">
229
+ <List aria-label="Recorded time per step" spacing="xs" listStyleType="none" type="ordered">
171
230
  {rows.map((row) => {
172
231
  const own = row.step.performance?.selfMs;
173
232
  return (
174
- <li key={row.index} data-band={row.band}>
175
- <button type="button" disabled={!canOpen(row.step)} onClick={() => open(row.step)} style={{ paddingLeft: 8 + row.depth * 14 }}>
176
- <span className={styles.stepTitle}>{row.step.title}</span>
177
- {row.band === 'slow' && <span className={styles.slow}>Slow</span>}
178
- <span className={styles.bar} aria-hidden="true">
179
- <span style={{ width: `${own === undefined ? 0 : Math.max(2, (own / longest) * 100)}%` }} />
180
- </span>
181
- <span className={styles.stepTime}>{own === undefined ? 'Not measured' : `${own.toFixed(1)} ms`}</span>
182
- </button>
183
- </li>
233
+ <List.Item key={row.index} data-band={row.band}>
234
+ <Button type="button" disabled={!canOpen(row.step)} onClick={() => open(row.step)} pl={row.depth ? 'lg' : 'sm'} variant="light">
235
+ <Text span inherit>
236
+ {row.step.title}
237
+ </Text>
238
+ {row.band === 'slow' && (
239
+ <Text span inherit c="red.7">
240
+ Slow
241
+ </Text>
242
+ )}
243
+ <Text aria-hidden="true" span inherit>
244
+ <Progress value={own === undefined ? 0 : (own / longest) * 100} w="6rem" size="sm" />
245
+ </Text>
246
+ <Text span inherit c="dimmed">
247
+ {own === undefined ? 'Not measured' : `${own.toFixed(1)} ms`}
248
+ </Text>
249
+ </Button>
250
+ </List.Item>
184
251
  );
185
252
  })}
186
- </ol>
253
+ </List>
187
254
  );
188
255
  }