@crvouga/atlas 0.10.1 → 0.11.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 (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +7 -0
  3. package/dist/reports/src/layer-merge.d.ts +5 -1
  4. package/dist/reports/src/layer-merge.js +17 -2
  5. package/dist/server/src/settings-page.d.ts +4 -4
  6. package/dist/server/src/settings-page.js +17 -78
  7. package/docs/agents.md +25 -0
  8. package/docs/reference/reports.md +2 -0
  9. package/docs/reference/server.md +4 -4
  10. package/docs/report-contract.md +3 -0
  11. package/docs/ui.md +142 -0
  12. package/docs/workspaces.md +130 -0
  13. package/llms-full.txt +306 -0
  14. package/llms.txt +2 -0
  15. package/package.json +17 -10
  16. package/packages/visualizer/src/admin/SettingsPage.tsx +456 -0
  17. package/packages/visualizer/src/admin/main.tsx +11 -0
  18. package/packages/visualizer/src/app/AppShell.tsx +62 -38
  19. package/packages/visualizer/src/app/gazetteer.tsx +90 -83
  20. package/packages/visualizer/src/app/router.tsx +33 -15
  21. package/packages/visualizer/src/components/ActivityIndicator.tsx +4 -5
  22. package/packages/visualizer/src/components/DataIssues.tsx +36 -54
  23. package/packages/visualizer/src/components/Header.tsx +197 -125
  24. package/packages/visualizer/src/components/Legend.tsx +13 -41
  25. package/packages/visualizer/src/components/ProjectPicker.tsx +61 -26
  26. package/packages/visualizer/src/components/Screen.tsx +80 -71
  27. package/packages/visualizer/src/components/SpecLogo.tsx +15 -0
  28. package/packages/visualizer/src/components/Status.tsx +20 -34
  29. package/packages/visualizer/src/components/labels.tsx +8 -3
  30. package/packages/visualizer/src/components/spec-logos/APACHE-2.0.txt +206 -0
  31. package/packages/visualizer/src/components/spec-logos/CAMUNDA-MIT.txt +21 -0
  32. package/packages/visualizer/src/components/spec-logos/FILE-ICONS-ISC.txt +13 -0
  33. package/packages/visualizer/src/components/spec-logos/README.md +23 -0
  34. package/packages/visualizer/src/components/spec-logos/SIMPLE-ICONS-CC0.txt +30 -0
  35. package/packages/visualizer/src/components/spec-logos/alloy.svg +1 -0
  36. package/packages/visualizer/src/components/spec-logos/dmn.svg +3 -0
  37. package/packages/visualizer/src/components/spec-logos/gherkin.svg +1 -0
  38. package/packages/visualizer/src/components/spec-logos/openslo.svg +63 -0
  39. package/packages/visualizer/src/components/spec-logos/quint.svg +1 -0
  40. package/packages/visualizer/src/evidence/artifacts.tsx +263 -110
  41. package/packages/visualizer/src/evidence/chart-source.tsx +18 -0
  42. package/packages/visualizer/src/evidence/check-detail.tsx +283 -139
  43. package/packages/visualizer/src/evidence/decision-table.tsx +35 -26
  44. package/packages/visualizer/src/evidence/document-page.tsx +51 -29
  45. package/packages/visualizer/src/evidence/feature-page.tsx +121 -53
  46. package/packages/visualizer/src/evidence/marks.tsx +112 -79
  47. package/packages/visualizer/src/evidence/model-page.tsx +77 -46
  48. package/packages/visualizer/src/evidence/model-trace.tsx +55 -35
  49. package/packages/visualizer/src/evidence/overview.tsx +137 -69
  50. package/packages/visualizer/src/evidence/recording-page.tsx +70 -31
  51. package/packages/visualizer/src/evidence/recording.tsx +135 -68
  52. package/packages/visualizer/src/evidence/snapshot.tsx +99 -43
  53. package/packages/visualizer/src/evidence/source-text.tsx +33 -0
  54. package/packages/visualizer/src/evidence/sources-panel.tsx +138 -144
  55. package/packages/visualizer/src/evidence/spec-header.tsx +52 -36
  56. package/packages/visualizer/src/evidence/spec-index.tsx +61 -32
  57. package/packages/visualizer/src/evidence/spec-page.tsx +56 -25
  58. package/packages/visualizer/src/evidence/test-filter.tsx +38 -14
  59. package/packages/visualizer/src/explorer/Explorer.tsx +688 -261
  60. package/packages/visualizer/src/explorer/ExplorerIndex.tsx +136 -68
  61. package/packages/visualizer/src/explorer/RunsView.tsx +307 -94
  62. package/packages/visualizer/src/explorer/TestDetail.tsx +393 -118
  63. package/packages/visualizer/src/explorer/parts.tsx +35 -13
  64. package/packages/visualizer/src/find/Finder.tsx +106 -90
  65. package/packages/visualizer/src/find/open.ts +17 -20
  66. package/packages/visualizer/src/live/test-progress.tsx +60 -22
  67. package/packages/visualizer/src/looks/Stage.tsx +223 -110
  68. package/packages/visualizer/src/main.tsx +9 -11
  69. package/packages/visualizer/src/map/ChartMap.tsx +125 -69
  70. package/packages/visualizer/src/map/EventEdge.tsx +59 -37
  71. package/packages/visualizer/src/map/map.module.css +12 -546
  72. package/packages/visualizer/src/map/nodes.tsx +124 -70
  73. package/packages/visualizer/src/reachable.test.ts +3 -2
  74. package/packages/visualizer/src/spec-kind.ts +2 -2
  75. package/packages/visualizer/src/ui/grammars/ALLOY-MIT.txt +21 -0
  76. package/packages/visualizer/src/ui/grammars/QUINT-APACHE-2.0.txt +206 -0
  77. package/packages/visualizer/src/ui/grammars/README.md +14 -0
  78. package/packages/visualizer/src/ui/grammars/alloy.json +71 -0
  79. package/packages/visualizer/src/ui/grammars/quint.json +152 -0
  80. package/packages/visualizer/src/ui/highlight.ts +50 -0
  81. package/packages/visualizer/src/ui/theme.tsx +109 -0
  82. package/packages/visualizer/src/views/ChartPage.tsx +46 -26
  83. package/packages/visualizer/src/views/ChartTests.tsx +59 -22
  84. package/packages/visualizer/src/views/ProductPage.tsx +16 -15
  85. package/packages/visualizer/src/views/SidePanel.tsx +241 -131
  86. package/packages/visualizer/src/app/gazetteer.module.css +0 -107
  87. package/packages/visualizer/src/components/ActivityIndicator.module.css +0 -4
  88. package/packages/visualizer/src/components/DataIssues.module.css +0 -136
  89. package/packages/visualizer/src/components/Header.module.css +0 -114
  90. package/packages/visualizer/src/components/Legend.module.css +0 -84
  91. package/packages/visualizer/src/components/Screen.module.css +0 -312
  92. package/packages/visualizer/src/components/Status.module.css +0 -116
  93. package/packages/visualizer/src/components/icons.tsx +0 -156
  94. package/packages/visualizer/src/evidence/artifacts.module.css +0 -213
  95. package/packages/visualizer/src/evidence/evidence.module.css +0 -907
  96. package/packages/visualizer/src/evidence/recording.module.css +0 -460
  97. package/packages/visualizer/src/evidence/sources.module.css +0 -168
  98. package/packages/visualizer/src/evidence/spec-snapshot.tsx +0 -26
  99. package/packages/visualizer/src/explorer/explorer.module.css +0 -1238
  100. package/packages/visualizer/src/find/Finder.module.css +0 -168
  101. package/packages/visualizer/src/looks/stage.module.css +0 -300
  102. package/packages/visualizer/src/styles/global.css +0 -105
  103. package/packages/visualizer/src/styles/tokens.css +0 -80
  104. package/packages/visualizer/src/views/views.module.css +0 -546
@@ -1,64 +1,95 @@
1
1
  import type { SpecEvidence } from '@crvouga/atlas/reports/evidence-view';
2
+ import { Anchor, Box, Code, Container, List, Title } from '@mantine/core';
2
3
  import { useMemo } from 'react';
3
-
4
4
  import type { SpecKind } from '../spec-kind';
5
5
  import { CheckDetail, ResultRow, type CheckSelection, type SelectCheck } from './check-detail';
6
- import styles from './evidence.module.css';
7
6
  import { Bounded } from './marks';
8
7
  import { QUINT_GROUPS, quintOutline } from './quint';
9
- import { SourceText, SpecHeader } from './spec-header';
10
- import { SpecSnapshot } from './spec-snapshot';
8
+ import { SpecHeader } from './spec-header';
9
+ import { SourceText } from './source-text';
11
10
  import { TestFilter, useRunningFilter } from './test-filter';
12
-
13
- type Props = { file: string; text: string; kind: SpecKind; entry: SpecEvidence | undefined; selection: CheckSelection; select: SelectCheck };
14
-
11
+ type Props = {
12
+ file: string;
13
+ text: string;
14
+ kind: SpecKind;
15
+ entry: SpecEvidence | undefined;
16
+ selection: CheckSelection;
17
+ select: SelectCheck;
18
+ };
15
19
  /** A model read as its state and actions, with every trace that was replayed against the app. */
16
20
  export function ModelPage({ file, text, kind, entry, selection, select }: Props) {
17
21
  const items = useMemo(() => quintOutline(text), [text]);
18
22
  const moduleName = items.find((item) => item.kind === 'module')?.name;
19
- const slots = useMemo(() => [...(entry?.slots ?? [])].sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true })), [entry]);
23
+ const slots = useMemo(
24
+ () => [...(entry?.slots ?? [])].sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true })),
25
+ [entry]
26
+ );
20
27
  const runningOnly = useRunningFilter();
21
- const visible = slots.filter(slot => !runningOnly || slot.status === 'running');
28
+ const visible = slots.filter((slot) => !runningOnly || slot.status === 'running');
22
29
  const toggle = (id: string) => select({ check: selection.check === id ? null : id, layer: null });
23
30
  const label = (title: string) => (moduleName && title.startsWith(`${moduleName} · `) ? title.slice(moduleName.length + 3) : title);
24
31
  return (
25
- <article className={styles.page}>
32
+ <Container component="article" size="xl" w="100%" py="xl">
26
33
  <SpecHeader title={moduleName ?? file.split('/').at(-1)!} file={file} kind={kind} entry={entry} />
27
- <SpecSnapshot title={moduleName ?? file.split('/').at(-1)!} text={text} kind={kind.one} />
28
- <TestFilter slots={slots} />
29
- {visible.length > 0 && (
30
- <section className={styles.block}>
31
- <h2 className={styles.blockTitle}>Traces</h2>
32
- <ul className={styles.results}>
33
- <Bounded items={visible} size={40} more="More traces">
34
- {(slot) => (
35
- <ResultRow key={slot.key} label={label(slot.title)} slot={slot} open={selection.check === slot.id} onToggle={() => toggle(slot.id)}>
36
- <CheckDetail slot={slot} layerId={selection.check === slot.id ? selection.layer : undefined} select={(patch) => select({ check: slot.id, ...patch })} reads="trace" />
37
- </ResultRow>
38
- )}
39
- </Bounded>
40
- </ul>
41
- </section>
42
- )}
43
- {QUINT_GROUPS.map((group) => {
44
- const declared = items.filter((item) => item.kind === group.kind);
45
- if (!declared.length) return null;
46
- return (
47
- <section key={group.kind} className={styles.block}>
48
- <h2 className={styles.blockTitle}>{group.label}</h2>
49
- <ul className={styles.declarations}>
50
- {declared.map((item) => (
51
- <li key={item.line}>
52
- <a href={`#line-${item.line}`} onClick={(event) => { event.preventDefault(); const line = document.getElementById(`line-${item.line}`); line?.closest('details')?.setAttribute('open', ''); line?.scrollIntoView({ block: 'center' }); }}>
53
- <code>{item.text}</code>
54
- </a>
55
- </li>
56
- ))}
57
- </ul>
58
- </section>
59
- );
60
- })}
61
- <SourceText text={text} label="Model source" open={items.length === 0} />
62
- </article>
34
+ <SourceText text={text} file={file} withResults />
35
+ <Box component="section" id="spec-results" aria-label="Recorded results" tabIndex={-1}>
36
+ <TestFilter slots={slots} />
37
+ {visible.length > 0 && (
38
+ <Box component="section" mb="lg">
39
+ <Title order={2} mb="sm">
40
+ Traces
41
+ </Title>
42
+ <List spacing="xs" listStyleType="none">
43
+ <Bounded items={visible} size={40} more="More traces">
44
+ {(slot) => (
45
+ <ResultRow
46
+ key={slot.key}
47
+ label={label(slot.title)}
48
+ slot={slot}
49
+ open={selection.check === slot.id}
50
+ onToggle={() => toggle(slot.id)}
51
+ >
52
+ <CheckDetail
53
+ slot={slot}
54
+ layerId={selection.check === slot.id ? selection.layer : undefined}
55
+ select={(patch) => select({ check: slot.id, ...patch })}
56
+ reads="trace"
57
+ />
58
+ </ResultRow>
59
+ )}
60
+ </Bounded>
61
+ </List>
62
+ </Box>
63
+ )}
64
+ {QUINT_GROUPS.map((group) => {
65
+ const declared = items.filter((item) => item.kind === group.kind);
66
+ if (!declared.length) return null;
67
+ return (
68
+ <Box key={group.kind} component="section" mb="lg">
69
+ <Title order={2} mb="sm">
70
+ {group.label}
71
+ </Title>
72
+ <List spacing="xs" listStyleType="none">
73
+ {declared.map((item) => (
74
+ <List.Item key={item.line}>
75
+ <Anchor
76
+ href={`#line-${item.line}`}
77
+ onClick={(event) => {
78
+ event.preventDefault();
79
+ const leading = (text.match(/^(?:[^\S\r\n]*\r?\n)+/)?.[0].match(/\n/g) ?? []).length;
80
+ const line = document.getElementById(`line-${item.line - leading}`) ?? document.querySelector('[data-source]');
81
+ line?.scrollIntoView({ block: 'center' });
82
+ }}
83
+ >
84
+ <Code>{item.text}</Code>
85
+ </Anchor>
86
+ </List.Item>
87
+ ))}
88
+ </List>
89
+ </Box>
90
+ );
91
+ })}
92
+ </Box>
93
+ </Container>
63
94
  );
64
95
  }
@@ -1,57 +1,77 @@
1
+ import { Button, Code, List, Stack, Table, Text } from '@mantine/core';
1
2
  import { useState } from 'react';
2
-
3
3
  import type { ModelState } from './quint';
4
- import styles from './recording.module.css';
5
-
6
4
  function shown(value: unknown) {
7
5
  if (value === undefined) return '—';
8
6
  return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
9
7
  }
10
-
11
8
  /** The states a model run went through, one at a time, with what each step changed. */
12
9
  export function ModelTrace({ states }: { states: readonly ModelState[] }) {
13
10
  const [selected, setSelected] = useState(0);
14
- if (!states.length) return <p className={styles.quietLine}>No model states were recorded.</p>;
11
+ if (!states.length)
12
+ return (
13
+ <Text component="div" c="dimmed">
14
+ No model states were recorded.
15
+ </Text>
16
+ );
15
17
  const at = Math.min(selected, states.length - 1);
16
18
  const state = states[at]!;
17
19
  const previous = states[at - 1];
18
20
  const variables = [...new Set([...Object.keys(previous ?? {}), ...Object.keys(state)])].filter((name) => !name.startsWith('#'));
19
21
  const changed = (name: string) => previous !== undefined && JSON.stringify(previous[name]) !== JSON.stringify(state[name]);
20
22
  return (
21
- <div className={styles.trace}>
22
- <ol className={styles.traceStates} aria-label="Model trace states">
23
+ <Stack gap="sm" miw={0}>
24
+ <List aria-label="Model trace states" spacing="xs" listStyleType="none" type="ordered">
23
25
  {states.map((entry, index) => (
24
- <li key={index}>
25
- <button type="button" aria-label={`State ${index + 1}`} aria-pressed={at === index} onClick={() => setSelected(index)}>
26
- <span className={styles.traceNo}>{index + 1}</span>
27
- <span>{typeof entry['#action'] === 'string' ? entry['#action'] : index === 0 ? 'Start' : 'Step'}</span>
28
- </button>
29
- </li>
26
+ <List.Item key={index}>
27
+ <Button
28
+ type="button"
29
+ aria-label={`State ${index + 1}`}
30
+ aria-pressed={at === index}
31
+ onClick={() => setSelected(index)}
32
+ variant="light"
33
+ >
34
+ <Text span inherit>
35
+ {index + 1}
36
+ </Text>
37
+ <Text span inherit>
38
+ {typeof entry['#action'] === 'string' ? entry['#action'] : index === 0 ? 'Start' : 'Step'}
39
+ </Text>
40
+ </Button>
41
+ </List.Item>
30
42
  ))}
31
- </ol>
32
- <div className={styles.tableScroll}>
33
- <table className={styles.traceTable}>
34
- <thead>
35
- <tr>
36
- <th scope="col">Variable</th>
37
- <th scope="col">Before</th>
38
- <th scope="col">State {at + 1}</th>
39
- </tr>
40
- </thead>
41
- <tbody>
43
+ </List>
44
+ <Table.ScrollContainer minWidth={600}>
45
+ <Table>
46
+ <Table.Thead>
47
+ <Table.Tr>
48
+ <Table.Th scope="col">Variable</Table.Th>
49
+ <Table.Th scope="col">Before</Table.Th>
50
+ <Table.Th scope="col">State {at + 1}</Table.Th>
51
+ </Table.Tr>
52
+ </Table.Thead>
53
+ <Table.Tbody>
42
54
  {variables.map((name) => (
43
- <tr key={name} data-changed={changed(name) || undefined}>
44
- <th scope="row">
55
+ <Table.Tr key={name} data-changed={changed(name) || undefined}>
56
+ <Table.Th scope="row">
45
57
  {name}
46
- {changed(name) && <span className={styles.changed}>Changed</span>}
47
- </th>
48
- <td><pre>{previous ? shown(previous[name]) : '—'}</pre></td>
49
- <td><pre>{shown(state[name])}</pre></td>
50
- </tr>
58
+ {changed(name) && (
59
+ <Text span inherit>
60
+ Changed
61
+ </Text>
62
+ )}
63
+ </Table.Th>
64
+ <Table.Td>
65
+ <Code block>{previous ? shown(previous[name]) : '—'}</Code>
66
+ </Table.Td>
67
+ <Table.Td>
68
+ <Code block>{shown(state[name])}</Code>
69
+ </Table.Td>
70
+ </Table.Tr>
51
71
  ))}
52
- </tbody>
53
- </table>
54
- </div>
55
- </div>
72
+ </Table.Tbody>
73
+ </Table>
74
+ </Table.ScrollContainer>
75
+ </Stack>
56
76
  );
57
77
  }
@@ -1,14 +1,13 @@
1
+ import { SpecLogo } from '../components/SpecLogo';
1
2
  import { addSummaries, NO_EVIDENCE, type SpecSummary } from '@crvouga/atlas/reports/evidence-view';
3
+ import { Alert, Anchor, Box, Button, Container, Flex, Group, List, Stack, Text, Title } from '@mantine/core';
2
4
  import { Link } from '@tanstack/react-router';
3
5
  import { useMemo } from 'react';
4
-
5
6
  import { folderOf, outlinePath, type SpecKind } from '../spec-kind';
6
7
  import { useUiStore } from '../state/ui-store';
7
8
  import { useEvidence } from './evidence-context';
8
- import styles from './evidence.module.css';
9
9
  import { ago, plural, useNow } from './format';
10
10
  import { backendLabel, Bounded, CountLine, Meter, StatusMark, Tally } from './marks';
11
-
12
11
  /** A kind's specs at a glance: how many have results, which are failing, and what just ran. */
13
12
  export function Overview({ kind }: { kind: SpecKind }) {
14
13
  const evidence = useEvidence();
@@ -16,119 +15,188 @@ export function Overview({ kind }: { kind: SpecKind }) {
16
15
  const setFilter = useUiStore((s) => s.setResultFilter);
17
16
  const setSourcesOpen = useUiStore((s) => s.setSourcesOpen);
18
17
  const files = useMemo(() => evidence.files.filter(kind.accepts), [evidence.files, kind]);
19
- const rows = useMemo(() => files.map((file) => ({ file, summary: evidence.view.specs.get(outlinePath(file))?.summary ?? NO_EVIDENCE })), [files, evidence.view]);
18
+ const rows = useMemo(
19
+ () => files.map((file) => ({ file, summary: evidence.view.specs.get(outlinePath(file))?.summary ?? NO_EVIDENCE })),
20
+ [files, evidence.view]
21
+ );
20
22
  const total = useMemo(() => addSummaries(rows.map((row) => row.summary)), [rows]);
21
23
  const withResults = rows.filter((row) => row.summary.status !== 'none');
22
24
  const failing = rows.filter((row) => row.summary.counts.failed > 0).sort((a, b) => b.summary.counts.failed - a.summary.counts.failed);
23
25
  const recent = [...withResults].sort((a, b) => (b.summary.latestAtMs ?? 0) - (a.summary.latestAtMs ?? 0)).slice(0, 8);
24
26
  const empty = rows.length - withResults.length;
25
27
  const specRow = (row: { file: string; summary: SpecSummary }) => (
26
- <li key={row.file}>
27
- <Link to="/specs/$kind" params={{ kind: kind.id }} search={{ spec: row.file }} className={styles.listRow}>
28
- <StatusMark status={row.summary.status} />
29
- <span className={styles.listName}>
30
- {evidence.titleOf(row.file)}
31
- <span className={styles.listPath}>{folderOf(row.file)}</span>
32
- </span>
33
- <Tally summary={row.summary} />
34
- {row.summary.latestAtMs !== null && <span className={styles.listTime}>{ago(row.summary.latestAtMs, now)}</span>}
35
- </Link>
36
- </li>
28
+ <List.Item key={row.file}>
29
+ <Anchor
30
+ display="block"
31
+ p="sm"
32
+ underline="never"
33
+ renderRoot={(props) => <Link {...props} to="/specs/$kind" params={{ kind: kind.id }} search={{ spec: row.file }} />}
34
+ >
35
+ <Group gap="md" align="center" wrap="nowrap">
36
+ <StatusMark status={row.summary.status} />
37
+ <Stack gap={0} flex={1} miw={0}>
38
+ <Text span inherit fw={500} truncate title={evidence.titleOf(row.file)}>
39
+ {evidence.titleOf(row.file)}
40
+ </Text>
41
+ {folderOf(row.file) && (
42
+ <Text span inherit c="dimmed" truncate title={folderOf(row.file)}>
43
+ {folderOf(row.file)}
44
+ </Text>
45
+ )}
46
+ </Stack>
47
+ <Flex direction={{ base: 'column', sm: 'row' }} align={{ base: 'flex-end', sm: 'center' }} gap={{ base: 'xs', sm: 'md' }}>
48
+ <Tally summary={row.summary} />
49
+ {row.summary.latestAtMs !== null && (
50
+ <Text span inherit c="dimmed" textWrap="nowrap">
51
+ {ago(row.summary.latestAtMs, now)}
52
+ </Text>
53
+ )}
54
+ </Flex>
55
+ </Group>
56
+ </Anchor>
57
+ </List.Item>
37
58
  );
38
59
  return (
39
- <article className={styles.page}>
40
- <header className={styles.specHead}>
41
- <h1 className={styles.title}>{kind.label}</h1>
42
- <div className={styles.summary}>
60
+ <Container component="article" size="xl" w="100%" py="xl">
61
+ <Box component="header" mb="lg">
62
+ <Flex gap="sm" align="center" mb="sm">
63
+ <SpecLogo kind={kind} />
64
+ <Title order={1}>{kind.label}</Title>
65
+ </Flex>
66
+ <Stack gap="sm" miw={0}>
43
67
  <Meter counts={total.counts} />
44
- <p className={styles.summaryLine}>
45
- <span>{plural(files.length, kind.one, kind.many)}</span>
46
- {evidence.ready && <span>{withResults.length} with results</span>}
47
- {evidence.ready && empty > 0 && <span>{empty} without</span>}
48
- </p>
68
+ <Flex gap="xs" align="center" wrap="wrap">
69
+ <Text span inherit>
70
+ {plural(files.length, kind.one, kind.many)}
71
+ </Text>
72
+ {evidence.ready && (
73
+ <Text span inherit>
74
+ {withResults.length} with results
75
+ </Text>
76
+ )}
77
+ {evidence.ready && empty > 0 && (
78
+ <Text span inherit>
79
+ {empty} without
80
+ </Text>
81
+ )}
82
+ </Flex>
49
83
  {total.status !== 'none' && (
50
- <p className={styles.summaryLine}>
84
+ <Flex gap="xs" align="center" wrap="wrap">
51
85
  <CountLine summary={total} noun={kind.check} nouns={`${kind.check}s`} />
52
- </p>
86
+ </Flex>
53
87
  )}
54
- </div>
55
- </header>
88
+ </Stack>
89
+ </Box>
56
90
 
57
91
  {evidence.unavailable && (
58
- <p className={styles.notice} role="alert">
92
+ <Alert role="alert" color={'red'} my="sm">
59
93
  Recorded results are unavailable. {evidence.unavailable}{' '}
60
- <button type="button" className={styles.link} onClick={evidence.retry}>Try again</button>
61
- </p>
94
+ <Button type="button" onClick={evidence.retry} variant="subtle">
95
+ Try again
96
+ </Button>
97
+ </Alert>
62
98
  )}
63
99
  {evidence.notices.map((notice) => (
64
- <p key={notice} className={styles.notice} role="status">
100
+ <Alert key={notice} role="status" color={'blue'} my="sm">
65
101
  {notice}{' '}
66
- <button type="button" className={styles.link} onClick={evidence.retry}>Try again</button>
67
- </p>
102
+ <Button type="button" onClick={evidence.retry} variant="subtle">
103
+ Try again
104
+ </Button>
105
+ </Alert>
68
106
  ))}
69
- {!evidence.ready && !evidence.unavailable && <p className={styles.aside} role="status">Loading recorded results…</p>}
70
- {evidence.ready && evidence.pending > 0 && <p className={styles.aside} role="status">Reading {plural(evidence.pending, 'more layer', 'more layers')}…</p>}
107
+ {!evidence.ready && !evidence.unavailable && (
108
+ <Text role="status" component="div" c="dimmed">
109
+ Loading recorded results…
110
+ </Text>
111
+ )}
112
+ {evidence.ready && evidence.pending > 0 && (
113
+ <Text role="status" component="div" c="dimmed">
114
+ Reading {plural(evidence.pending, 'more layer', 'more layers')}…
115
+ </Text>
116
+ )}
71
117
 
72
118
  {evidence.active.length > 0 && (
73
- <section className={styles.block}>
74
- <h2 className={styles.blockTitle}>Running now</h2>
75
- <ul className={styles.list}>
119
+ <Box component="section" mb="lg">
120
+ <Title order={2} mb="sm">
121
+ Running now
122
+ </Title>
123
+ <List spacing="xs" listStyleType="none">
76
124
  {evidence.active.map((run) => (
77
- <li key={run.source} className={styles.listRow}>
78
- <StatusMark status="running" />
79
- <span className={styles.listName}>{run.packageDir}</span>
80
- <span className={styles.tally} data-status={run.failed ? 'failed' : 'running'}>
81
- {plural(run.completed, 'result', 'results')} so far{run.failed ? `, ${run.failed} failed` : ''}
82
- </span>
83
- <span className={styles.listTime}>started {ago(run.startedAtMs, now)}</span>
84
- </li>
125
+ <List.Item key={run.source}>
126
+ <Group gap="md" align="center">
127
+ <StatusMark status="running" />
128
+ <Text span inherit>
129
+ {run.packageDir}
130
+ </Text>
131
+ <Text data-status={run.failed ? 'failed' : 'running'} span inherit>
132
+ {plural(run.completed, 'result', 'results')} so far{run.failed ? `, ${run.failed} failed` : ''}
133
+ </Text>
134
+ <Text span inherit c="dimmed">
135
+ started {ago(run.startedAtMs, now)}
136
+ </Text>
137
+ </Group>
138
+ </List.Item>
85
139
  ))}
86
- </ul>
87
- </section>
140
+ </List>
141
+ </Box>
88
142
  )}
89
143
 
90
144
  {failing.length > 0 && (
91
- <section className={styles.block}>
92
- <h2 className={styles.blockTitle}>
93
- Failing <span className={styles.aside}>{plural(failing.length, kind.one, kind.many)}</span>
94
- </h2>
95
- <ul className={styles.list}>
145
+ <Box component="section" mb="lg">
146
+ <Title order={2} mb="sm">
147
+ Failing{' '}
148
+ <Text span inherit c="dimmed">
149
+ {plural(failing.length, kind.one, kind.many)}
150
+ </Text>
151
+ </Title>
152
+ <List spacing="xs" listStyleType="none">
96
153
  <Bounded items={failing} size={12} more="More failing">
97
154
  {specRow}
98
155
  </Bounded>
99
- </ul>
100
- </section>
156
+ </List>
157
+ </Box>
101
158
  )}
102
159
 
103
160
  {recent.length > 0 && (
104
- <section className={styles.block}>
105
- <h2 className={styles.blockTitle}>Latest results</h2>
106
- <ul className={styles.list}>{recent.map(specRow)}</ul>
107
- </section>
161
+ <Box component="section" mb="lg">
162
+ <Title order={2} mb="sm">
163
+ Latest results
164
+ </Title>
165
+ <List spacing="xs" listStyleType="none">
166
+ {recent.map(specRow)}
167
+ </List>
168
+ </Box>
108
169
  )}
109
170
 
110
171
  {evidence.ready && evidence.pending === 0 && withResults.length === 0 && !evidence.unavailable && (
111
- <p className={styles.aside}>
172
+ <Text component="div" c="dimmed">
112
173
  {evidence.layers.length === 0
113
174
  ? `Nothing has been recorded yet. Results appear here as a spec run finishes each test.`
114
175
  : `None of the ${plural(evidence.layers.length, 'layer', 'layers')} in the combined report checked a ${kind.one}.`}
115
- </p>
176
+ </Text>
116
177
  )}
117
178
 
118
179
  {evidence.ready && (
119
- <p className={styles.footLine}>
120
- {plural(evidence.layers.length, 'layer', 'layers')} from {evidence.backends.filter((backend) => backend.available).map((backend) => backendLabel(evidence.backends, backend.id)).join(' and ') || 'no report store'}.{' '}
121
- <button type="button" className={styles.link} onClick={() => setSourcesOpen(true)}>Choose sources</button>
180
+ <Text component="div" c="dimmed">
181
+ {plural(evidence.layers.length, 'layer', 'layers')} from{' '}
182
+ {evidence.backends
183
+ .filter((backend) => backend.available)
184
+ .map((backend) => backendLabel(evidence.backends, backend.id))
185
+ .join(' and ') || 'no report store'}
186
+ .{' '}
187
+ <Button type="button" onClick={() => setSourcesOpen(true)} variant="subtle">
188
+ Choose sources
189
+ </Button>
122
190
  {empty > 0 && (
123
191
  <>
124
192
  {' · '}
125
- <button type="button" className={styles.link} onClick={() => setFilter('none')}>
193
+ <Button type="button" onClick={() => setFilter('none')} variant="subtle">
126
194
  Show the {empty} without results
127
- </button>
195
+ </Button>
128
196
  </>
129
197
  )}
130
- </p>
198
+ </Text>
131
199
  )}
132
- </article>
200
+ </Container>
133
201
  );
134
202
  }