@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,94 +1,112 @@
1
- import * as Collapsible from '@radix-ui/react-collapsible';
1
+ import { IconCheck, IconX, IconCircle } from '@tabler/icons-react';
2
+ import { Accordion, Alert, Anchor, Box, Container, Flex, List, Stack, Text, Title } from '@mantine/core';
3
+
2
4
  import { Link } from '@tanstack/react-router';
3
5
  import type { ReactNode } from 'react';
4
-
5
- import { statusNote, type AtlasView, type StateView, type TransitionView } from '../data/model';
6
- import { ChevronIcon } from '../components/icons';
6
+ import { IconChevronRight as ChevronIcon } from '@tabler/icons-react';
7
7
  import { KIND_LABEL } from '../components/labels';
8
8
  import { ScreenImage } from '../components/Screen';
9
9
  import { StatusBadge, StatusDot } from '../components/Status';
10
+ import { statusNote, type AtlasView, type StateView, type TransitionView } from '../data/model';
11
+ import { mapScope } from '../layout/graph';
10
12
  import { eventLooks, journeyLooks, stateLooks } from '../looks/collect';
11
13
  import { Stage } from '../looks/Stage';
12
- import { mapScope } from '../layout/graph';
13
14
  import type { MapSelection } from '../map/ChartMap';
14
- import styles from './views.module.css';
15
15
  import { ChartTests } from './ChartTests';
16
-
16
+ import { ChartSource } from '../evidence/chart-source';
17
17
  function Section({ title, children }: { title: string; children: ReactNode }) {
18
18
  return (
19
- <section className={styles.section}>
20
- <h3 className={styles.sectionTitle}>{title}</h3>
19
+ <Box component="section" mb="lg">
20
+ <Title order={2} mb="sm">
21
+ {title}
22
+ </Title>
21
23
  {children}
22
- </section>
24
+ </Box>
23
25
  );
24
26
  }
25
-
26
27
  function EventLink({ t, view, direction }: { t: TransitionView; view: AtlasView; direction: 'in' | 'out' }) {
27
28
  const other = direction === 'in' ? t.source : t.target;
28
29
  const otherState = other ? view.states.get(other) : undefined;
29
30
  return (
30
- <Link
31
- to="/chart/$chartId"
32
- params={{ chartId: view.states.get(direction === 'in' ? t.target ?? t.source : t.source)?.chartId ?? t.chartId }}
33
- search={(prev) => ({ run: prev.run, event: t.id, journey: prev.journey })}
34
- className={styles.eventRow}
31
+ <Anchor
35
32
  data-status={t.status}
33
+ underline="never"
34
+ renderRoot={(props) => (
35
+ <Link
36
+ {...props}
37
+ to="/chart/$chartId"
38
+ params={{ chartId: view.states.get(direction === 'in' ? (t.target ?? t.source) : t.source)?.chartId ?? t.chartId }}
39
+ search={(prev) => ({ run: prev.run, event: t.id, journey: prev.journey })}
40
+ />
41
+ )}
36
42
  >
37
- <span>{t.event}</span>
43
+ <Text span inherit>
44
+ {t.event}
45
+ </Text>
38
46
  {otherState && (
39
- <span className={styles.eventTarget}>
47
+ <Text span inherit>
40
48
  {direction === 'in' ? 'from' : 'to'} {otherState.name}
41
- </span>
49
+ </Text>
42
50
  )}
43
- </Link>
51
+ </Anchor>
44
52
  );
45
53
  }
46
-
47
54
  function Developer({ rows }: { rows: [string, ReactNode][] }) {
48
55
  return (
49
- <Collapsible.Root className={styles.dev}>
50
- <Collapsible.Trigger className={styles.devTrigger}>
51
- <ChevronIcon size={12} className={styles.devChevron} />
52
- For developers
53
- </Collapsible.Trigger>
54
- <Collapsible.Content>
55
- <dl className={styles.devList}>
56
- {rows
57
- .filter(([, v]) => v !== null && v !== undefined && v !== '')
58
- .map(([k, v]) => (
59
- <div key={k}>
60
- <dt>{k}</dt>
61
- <dd>{v}</dd>
62
- </div>
63
- ))}
64
- </dl>
65
- </Collapsible.Content>
66
- </Collapsible.Root>
56
+ <Accordion variant="separated" my="lg">
57
+ <Accordion.Item value="developer">
58
+ <Accordion.Control>
59
+ <ChevronIcon aria-hidden="true" size={12} />
60
+ For developers
61
+ </Accordion.Control>
62
+ <Accordion.Panel>
63
+ <Box component="dl">
64
+ {rows
65
+ .filter(([, v]) => v !== null && v !== undefined && v !== '')
66
+ .map(([k, v]) => (
67
+ <Stack key={k} gap="sm">
68
+ <Box component="dt">{k}</Box>
69
+ <Box component="dd" m={0}>
70
+ {v}
71
+ </Box>
72
+ </Stack>
73
+ ))}
74
+ </Box>
75
+ </Accordion.Panel>
76
+ </Accordion.Item>
77
+ </Accordion>
67
78
  );
68
79
  }
69
-
70
80
  function Questions({ view, ids }: { view: AtlasView; ids: string[] }) {
71
81
  if (ids.length === 0) return null;
72
82
  return (
73
83
  <Section title="Open questions">
74
- <ul className={styles.notes}>
84
+ <List spacing="xs" listStyleType="none">
75
85
  {ids.map((id) => {
76
86
  const q = view.questions.get(id);
77
87
  if (!q) return null;
78
88
  return (
79
- <li key={id} className={styles.note}>
80
- <strong>{q.title}</strong> {q.body}
81
- {q.specToday && <span className={styles.noteMeta}>Spec today: {q.specToday}</span>}
82
- </li>
89
+ <Alert key={id} color={'blue'} my="sm">
90
+ <Text span inherit fw={600}>
91
+ {q.title}
92
+ </Text>{' '}
93
+ {q.body}
94
+ {q.specToday && (
95
+ <Text span inherit c="dimmed">
96
+ Spec today: {q.specToday}
97
+ </Text>
98
+ )}
99
+ </Alert>
83
100
  );
84
101
  })}
85
- </ul>
102
+ </List>
86
103
  </Section>
87
104
  );
88
105
  }
89
-
90
106
  function ScreenDetails({ view, state }: { view: AtlasView; state: StateView }) {
91
- const checks = state.result?.checks.length ? state.result.checks : state.specChecks.map((check) => ({ check, passed: null as boolean | null, expected: undefined, actual: undefined }));
107
+ const checks = state.result?.checks.length
108
+ ? state.result.checks
109
+ : state.specChecks.map((check) => ({ check, passed: null as boolean | null, expected: undefined, actual: undefined }));
92
110
  const note = statusNote(state.status);
93
111
  const events = state.result?.businessEvents;
94
112
  const screen = state.kind === 'screen' || state.kind === 'final';
@@ -96,111 +114,152 @@ function ScreenDetails({ view, state }: { view: AtlasView; state: StateView }) {
96
114
  const outgoing = state.outgoing.map((id) => view.transitions.get(id)).filter((t): t is TransitionView => Boolean(t));
97
115
  return (
98
116
  <>
99
- <div className={styles.plateHead}>
100
- <h2 className={styles.plateTitle}>{state.name}</h2>
117
+ <Flex gap="xs" align="center" wrap="wrap">
118
+ <Title order={1} mb="sm">
119
+ {state.name}
120
+ </Title>
101
121
  <StatusBadge status={state.status} />
102
- {state.confidence === 'assumed' && <span className={styles.assumedBadge}>Assumed, not confirmed</span>}
103
- </div>
104
- {state.description && <p className={styles.lead}>{state.description}</p>}
105
- {note && <p className={styles.statusNote}>{note}</p>}
122
+ {state.confidence === 'assumed' && (
123
+ <Text span inherit>
124
+ Assumed, not confirmed
125
+ </Text>
126
+ )}
127
+ </Flex>
128
+ {state.description && (
129
+ <Text component="div" c="dimmed">
130
+ {state.description}
131
+ </Text>
132
+ )}
133
+ {note && (
134
+ <Text component="div" c="dimmed">
135
+ {note}
136
+ </Text>
137
+ )}
106
138
  <Stage
107
139
  looks={stateLooks(state)}
108
140
  prefer="pictures"
109
141
  empty={
110
142
  screen ? (
111
- <div className={styles.bigShot}>
143
+ <Stack>
112
144
  <ScreenImage key={state.name} state={state} size="full" description={false} lines={state.hints.length > 0} />
113
- </div>
145
+ </Stack>
114
146
  ) : (
115
- <p className={styles.lead}>This part of the chart has no screen of its own.</p>
147
+ <Text c="dimmed">This part of the chart has no screen of its own.</Text>
116
148
  )
117
149
  }
118
150
  />
119
- <div>
120
- <h3 className={styles.sectionTitle}>Where they go next</h3>
151
+ <Stack gap="sm">
152
+ <Title order={2} mb="sm">
153
+ Where they go next
154
+ </Title>
121
155
  {outgoing.length > 0 ? (
122
- <div className={styles.eventList}>
156
+ <Stack gap="sm" miw={0}>
123
157
  {outgoing.map((transition) => (
124
158
  <EventLink key={transition.id} t={transition} view={view} direction="out" />
125
159
  ))}
126
- </div>
160
+ </Stack>
127
161
  ) : (
128
- <p className={styles.lead}>{state.deadEnd ? `This is where the journey stops: ${state.deadEnd}` : 'Nothing leaves this screen.'}</p>
162
+ <Text component="div" c="dimmed">
163
+ {state.deadEnd ? `This is where the journey stops: ${state.deadEnd}` : 'Nothing leaves this screen.'}
164
+ </Text>
129
165
  )}
130
- </div>
166
+ </Stack>
131
167
  {checks.length > 0 && (
132
168
  <Section title="What should be on the screen">
133
- <ul className={styles.checks}>
169
+ <List spacing="xs" listStyleType="none">
134
170
  {checks.map((c) => (
135
- <li key={c.check} data-passed={c.passed === null ? 'unknown' : c.passed}>
136
- <span className={styles.checkMark} aria-hidden="true">
137
- {c.passed === null ? '○' : c.passed ? '✓' : '✗'}
138
- </span>
139
- <span>
171
+ <List.Item key={c.check} data-passed={c.passed === null ? 'unknown' : c.passed}>
172
+ <Text aria-hidden="true" span inherit>
173
+ {c.passed === null ? <IconCircle size={16} /> : c.passed ? <IconCheck size={16} /> : <IconX size={16} />}
174
+ </Text>
175
+ <Text span inherit>
140
176
  {c.check}
141
177
  {c.passed === false && (c.expected || c.actual) && (
142
- <span className={styles.expected}>
178
+ <Text span inherit c="dimmed">
143
179
  Expected {c.expected ?? 'something else'}, saw {c.actual ?? 'nothing'}
144
- </span>
180
+ </Text>
145
181
  )}
146
- </span>
147
- </li>
182
+ </Text>
183
+ </List.Item>
148
184
  ))}
149
- </ul>
185
+ </List>
150
186
  </Section>
151
187
  )}
152
188
  {(state.specBusinessEvents.length > 0 || events) && (
153
189
  <Section title="What happens behind the scenes">
154
- <ul className={styles.checks}>
190
+ <List spacing="xs" listStyleType="none">
155
191
  {(events?.expected ?? state.specBusinessEvents).map((e) => {
156
192
  const missing = events?.missing.includes(e);
157
193
  const noSignal = events?.noSignal.includes(e);
158
194
  const seen = events?.observed.includes(e);
159
195
  return (
160
- <li key={e} data-passed={missing ? false : seen ? true : 'unknown'}>
161
- <span className={styles.checkMark} aria-hidden="true">
162
- {missing ? '✗' : seen ? '✓' : '○'}
163
- </span>
164
- <span>
196
+ <List.Item key={e} data-passed={missing ? false : seen ? true : 'unknown'}>
197
+ <Text aria-hidden="true" span inherit>
198
+ {missing ? <IconX size={16} /> : seen ? <IconCheck size={16} /> : <IconCircle size={16} />}
199
+ </Text>
200
+ <Text span inherit>
165
201
  {e}
166
- {missing && <span className={styles.expected}>Didn’t happen in this run</span>}
167
- {noSignal && <span className={styles.expected}>Can’t be checked yet</span>}
168
- </span>
169
- </li>
202
+ {missing && (
203
+ <Text span inherit c="dimmed">
204
+ Didn’t happen in this run
205
+ </Text>
206
+ )}
207
+ {noSignal && (
208
+ <Text span inherit c="dimmed">
209
+ Can’t be checked yet
210
+ </Text>
211
+ )}
212
+ </Text>
213
+ </List.Item>
170
214
  );
171
215
  })}
172
- </ul>
216
+ </List>
173
217
  </Section>
174
218
  )}
175
219
  {state.knownIssues.length > 0 && (
176
220
  <Section title="Known issues">
177
- <ul className={styles.notes}>
221
+ <List spacing="xs" listStyleType="none">
178
222
  {state.knownIssues.map((n) => (
179
- <li key={n.body} className={styles.note}>
223
+ <Alert key={n.body} color={'blue'} my="sm">
180
224
  {n.body}
181
- </li>
225
+ </Alert>
182
226
  ))}
183
- </ul>
227
+ </List>
184
228
  </Section>
185
229
  )}
186
230
  <Questions view={view} ids={state.questionIds} />
187
231
  {incoming.length > 0 && (
188
232
  <Section title="How users get here">
189
- <div className={styles.eventList}>
233
+ <Stack gap="sm" miw={0}>
190
234
  {incoming.map((t) => (
191
235
  <EventLink key={t.id} t={t} view={view} direction="in" />
192
236
  ))}
193
- </div>
237
+ </Stack>
194
238
  </Section>
195
239
  )}
196
240
 
197
- {state.deadEnd && outgoing.length > 0 && <p className={styles.note}>This is where the journey stops: {state.deadEnd}</p>}
241
+ {state.deadEnd && outgoing.length > 0 && (
242
+ <Alert color={'blue'} my="sm">
243
+ This is where the journey stops: {state.deadEnd}
244
+ </Alert>
245
+ )}
198
246
  <Developer
199
247
  rows={[
200
248
  ['State ID', <code key="id">{state.name}</code>],
201
249
  ['Spec file', <code key="f">{state.file}</code>],
202
250
  ['Snapshot name', state.snapshot ? <code key="s">{state.snapshot}</code> : null],
203
- ['Sources', state.sources.length ? <ul key="src">{state.sources.map((s) => <li key={s}><code>{s}</code></li>)}</ul> : null],
251
+ [
252
+ 'Sources',
253
+ state.sources.length ? (
254
+ <ul key="src">
255
+ {state.sources.map((s) => (
256
+ <li key={s}>
257
+ <code>{s}</code>
258
+ </li>
259
+ ))}
260
+ </ul>
261
+ ) : null
262
+ ],
204
263
  ['Owner', state.owner],
205
264
  ['Tier', state.tier],
206
265
  [
@@ -220,27 +279,42 @@ function ScreenDetails({ view, state }: { view: AtlasView; state: StateView }) {
220
279
  </>
221
280
  );
222
281
  }
223
-
224
282
  function EventDetails({ view, t }: { view: AtlasView; t: TransitionView }) {
225
283
  const from = view.states.get(t.source);
226
284
  const to = t.target ? view.states.get(t.target) : undefined;
227
285
  const note = statusNote(t.status, t.result?.reason);
228
286
  return (
229
287
  <>
230
- <div className={styles.plateHead}>
231
- <h2 className={styles.plateTitle}>{t.event}</h2>
288
+ <Flex gap="xs" align="center" wrap="wrap">
289
+ <Title order={1} mb="sm">
290
+ {t.event}
291
+ </Title>
232
292
  <StatusBadge status={t.status} />
233
- </div>
234
- <p className={styles.lead}>{KIND_LABEL[t.kind]}</p>
235
- <p className={styles.lead}>
236
- From <strong>{from?.name ?? t.source}</strong> to <strong>{to?.name ?? 'a screen not in the spec'}</strong>.
237
- </p>
238
- {note && <p className={styles.statusNote}>{note}</p>}
293
+ </Flex>
294
+ <Text component="div" c="dimmed">
295
+ {KIND_LABEL[t.kind]}
296
+ </Text>
297
+ <Text component="div" c="dimmed">
298
+ From{' '}
299
+ <Text span inherit fw={600}>
300
+ {from?.name ?? t.source}
301
+ </Text>{' '}
302
+ to{' '}
303
+ <Text span inherit fw={600}>
304
+ {to?.name ?? 'a screen not in the spec'}
305
+ </Text>
306
+ .
307
+ </Text>
308
+ {note && (
309
+ <Text component="div" c="dimmed">
310
+ {note}
311
+ </Text>
312
+ )}
239
313
  <Stage
240
314
  looks={eventLooks(view, t)}
241
315
  prefer="film"
242
316
  notice={t.result?.clip.state === 'processing' ? 'The video of this event is still processing.' : null}
243
- empty={<p className={styles.lead}>This event has no picture, video, or timeline in this run.</p>}
317
+ empty={<Text c="dimmed">This event has no picture, video, or timeline in this run.</Text>}
244
318
  />
245
319
  <Questions view={view} ids={t.questionIds} />
246
320
  <Developer
@@ -253,45 +327,72 @@ function EventDetails({ view, t }: { view: AtlasView; t: TransitionView }) {
253
327
  </>
254
328
  );
255
329
  }
256
-
257
330
  function JourneyLooks({ view, journeyId }: { view: AtlasView; journeyId: string }) {
258
331
  const looks = journeyLooks(view, journeyId);
259
332
  if (looks.pictures.length === 0 && looks.films.length === 0 && !looks.moments.some((moment) => moment.thumb)) return null;
260
333
  return <Stage looks={looks} prefer="timeline" />;
261
334
  }
262
-
263
335
  function JourneyDetails({ view, journeyId, chartId }: { view: AtlasView; journeyId: string; chartId: string }) {
264
336
  const j = view.journeys.find((x) => x.id === journeyId);
265
- if (!j) return <p className={styles.lead}>This journey isn’t in the spec.</p>;
337
+ if (!j)
338
+ return (
339
+ <Text component="div" c="dimmed">
340
+ This journey isn’t in the spec.
341
+ </Text>
342
+ );
266
343
  return (
267
344
  <>
268
- <div className={styles.plateHead}>
269
- <h2 className={styles.plateTitle}>{j.name}</h2>
345
+ <Flex gap="xs" align="center" wrap="wrap">
346
+ <Title order={1} mb="sm">
347
+ {j.name}
348
+ </Title>
270
349
  <StatusBadge status={j.status} />
271
- </div>
272
- {j.description && <p className={styles.lead}>{j.description}</p>}
273
- {!j.replay.ok && <p className={styles.warn}>The chart can’t follow this journey past “{j.replay.failedAt ?? j.replay.missingEnds.join(', ')}”.</p>}
350
+ </Flex>
351
+ {j.description && (
352
+ <Text component="div" c="dimmed">
353
+ {j.description}
354
+ </Text>
355
+ )}
356
+ {!j.replay.ok && (
357
+ <Text component="div" c="red.7">
358
+ The chart can’t follow this journey past “{j.replay.failedAt ?? j.replay.missingEnds.join(', ')}”.
359
+ </Text>
360
+ )}
274
361
  <JourneyLooks view={view} journeyId={j.id} />
275
- <ol className={styles.steps}>
362
+ <List spacing="xs" listStyleType="none" type="ordered">
276
363
  {j.steps.map((step, i) => {
277
364
  const t = view.transitions.get(step.transitionIds[0] ?? '');
278
365
  if (!t) return null;
279
- const here = view.transitions.get(t.id)?.chartId === chartId || [...view.transitions.values()].some((x) => x.carriedBy === t.id && x.chartId === chartId);
366
+ const here =
367
+ view.transitions.get(t.id)?.chartId === chartId ||
368
+ [...view.transitions.values()].some((x) => x.carriedBy === t.id && x.chartId === chartId);
280
369
  return (
281
- <li key={i} data-here={here}>
282
- <Link to="/chart/$chartId" params={{ chartId: t.chartId }} search={(prev) => ({ run: prev.run, journey: j.id, event: t.id })} className={styles.step}>
283
- <span className={styles.stepNo}>{i + 1}</span>
284
- <span className={styles.stepEvent} data-status={t.status}>{t.event}</span>
370
+ <List.Item key={i} data-here={here}>
371
+ <Anchor
372
+ renderRoot={(props) => (
373
+ <Link
374
+ {...props}
375
+ to="/chart/$chartId"
376
+ params={{ chartId: t.chartId }}
377
+ search={(prev) => ({ run: prev.run, journey: j.id, event: t.id })}
378
+ />
379
+ )}
380
+ >
381
+ <Text span inherit>
382
+ {i + 1}
383
+ </Text>
384
+ <Text data-status={t.status} span inherit>
385
+ {t.event}
386
+ </Text>
285
387
  <StatusDot status={t.status} />
286
- </Link>
287
- </li>
388
+ </Anchor>
389
+ </List.Item>
288
390
  );
289
391
  })}
290
- </ol>
392
+ </List>
291
393
  </>
292
394
  );
293
395
  }
294
-
295
396
  export function SidePanel({ view, chartId, selection }: { view: AtlasView; chartId: string; selection: MapSelection }) {
296
397
  const state = selection.screen ? view.states.get(selection.screen) : undefined;
297
398
  const transition = selection.event ? view.transitions.get(selection.event) : undefined;
@@ -307,25 +408,34 @@ export function SidePanel({ view, chartId, selection }: { view: AtlasView; chart
307
408
  body = <JourneyDetails view={view} journeyId={selection.journey} chartId={chartId} />;
308
409
  label = 'Journey';
309
410
  } else {
310
- body = <p className={styles.lead}>This item isn’t in the spec any more. It may have been renamed or removed.</p>;
411
+ body = (
412
+ <Text component="div" c="dimmed">
413
+ This item isn’t in the spec any more. It may have been renamed or removed.
414
+ </Text>
415
+ );
311
416
  label = 'Not found';
312
417
  }
313
418
  const scopeName = state?.name ?? transition?.source;
314
419
  const scope = scopeName ? (mapScope(view, chartId, scopeName) ?? undefined) : undefined;
315
420
  return (
316
- <article className={styles.plateScroll} aria-label={label}>
421
+ <Container aria-label={label} component="article" size="xl" w="100%" py="xl">
317
422
  {body}
318
423
  <ChartTests view={view} chartId={chartId} />
319
- <Link
320
- to="/chart/$chartId"
321
- params={{ chartId }}
322
- search={(prev) => ({ run: prev.run, screen: undefined, event: undefined, journey: undefined, group: scope })}
323
- activeOptions={{ includeSearch: true, explicitUndefined: true }}
424
+ {view.charts.get(chartId)?.file && <ChartSource file={view.charts.get(chartId)!.file} />}
425
+ <Anchor
324
426
  aria-current={undefined}
325
- className={styles.mapLink}
427
+ renderRoot={(props) => (
428
+ <Link
429
+ {...props}
430
+ to="/chart/$chartId"
431
+ params={{ chartId }}
432
+ search={(prev) => ({ run: prev.run, screen: undefined, event: undefined, journey: undefined, group: scope })}
433
+ activeOptions={{ includeSearch: true, explicitUndefined: true }}
434
+ />
435
+ )}
326
436
  >
327
437
  Show the map
328
- </Link>
329
- </article>
438
+ </Anchor>
439
+ </Container>
330
440
  );
331
441
  }
@@ -1,107 +0,0 @@
1
- .index {
2
- background: var(--bg);
3
- border-right: 1px solid var(--line);
4
- overflow: auto;
5
- padding: 12px 8px 32px;
6
- min-height: 0;
7
- }
8
-
9
- .index h2 {
10
- margin: 14px 10px 6px;
11
- font-size: 0.9375rem;
12
- font-weight: 600;
13
- }
14
-
15
- .index h3 {
16
- margin: 16px 10px 4px;
17
- color: var(--ink-3);
18
- font-size: 0.75rem;
19
- font-weight: 600;
20
- }
21
-
22
- .index ul {
23
- list-style: none;
24
- margin: 0;
25
- padding: 0;
26
- }
27
-
28
- .chart,
29
- .row {
30
- display: flex;
31
- justify-content: space-between;
32
- align-items: center;
33
- gap: 10px;
34
- padding: 6px 10px;
35
- border-radius: 8px;
36
- color: inherit;
37
- font-size: 0.9rem;
38
- text-decoration: none;
39
- }
40
-
41
- .thumb {
42
- flex: none;
43
- width: 28px;
44
- }
45
-
46
- .name {
47
- flex: 1;
48
- min-width: 0;
49
- }
50
-
51
- .chart {
52
- color: var(--ink-2);
53
- font-weight: 600;
54
- }
55
-
56
- .chart:hover,
57
- .row:hover {
58
- background: var(--bg-2);
59
- }
60
-
61
- .chart[aria-current='page'],
62
- .row[aria-current='page'] {
63
- background: var(--place-soft);
64
- color: var(--ink);
65
- font-weight: 600;
66
- }
67
-
68
- .status {
69
- flex: none;
70
- color: var(--ink-3);
71
- font-size: 0.75rem;
72
- font-weight: 400;
73
- text-align: right;
74
- white-space: nowrap;
75
- }
76
-
77
- .row[data-status='failed'] .status {
78
- color: var(--failed);
79
- font-weight: 600;
80
- }
81
-
82
- .row[data-status='flaky'] .status {
83
- color: var(--flaky-ink);
84
- }
85
-
86
- .excluded {
87
- padding: 6px 10px;
88
- color: var(--ink-3);
89
- font-size: 0.9rem;
90
- }
91
-
92
- .empty {
93
- margin: 4px 10px;
94
- color: var(--ink-3);
95
- font-size: 0.875rem;
96
- }
97
-
98
- @media (max-width: 760px) {
99
- .index {
100
- flex: none;
101
- max-height: none;
102
- min-height: auto;
103
- overflow: visible;
104
- border-right: 0;
105
- border-top: 1px solid var(--line);
106
- }
107
- }
@@ -1,4 +0,0 @@
1
- .activity { display: inline-flex; align-items: center; justify-content: center; width: 12px; height: 12px; flex: none; color: var(--place); }
2
- .ring { box-sizing: border-box; width: 10px; height: 10px; border: 1.5px solid currentColor; border-right-color: transparent; border-bottom-color: transparent; border-radius: 50%; opacity: .65; animation: turn 1.8s linear infinite; }
3
- @keyframes turn { to { transform: rotate(360deg); } }
4
- @media (prefers-reduced-motion: reduce) { .ring { animation: none; } }