@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,64 +1,80 @@
1
+ import { SpecLogo } from '../components/SpecLogo';
1
2
  import type { SpecEvidence } from '@crvouga/atlas/reports/evidence-view';
2
3
  import { NO_EVIDENCE } from '@crvouga/atlas/reports/evidence-view';
4
+ import { Box, Flex, Group, Stack, Text, Title } from '@mantine/core';
3
5
  import type { ReactNode } from 'react';
4
-
5
6
  import type { SpecKind } from '../spec-kind';
6
7
  import { useEvidence } from './evidence-context';
7
- import styles from './evidence.module.css';
8
8
  import { useNow } from './format';
9
9
  import { CountLine, Meter, Origin } from './marks';
10
-
11
10
  /** A spec's name, where it lives, and what the combined report holds for it. */
12
- export function SpecHeader({ title, file, kind, entry, planned, children }: { title: string; file: string; kind: SpecKind; entry: SpecEvidence | undefined; planned?: number; children?: ReactNode }) {
11
+ export function SpecHeader({
12
+ title,
13
+ file,
14
+ kind,
15
+ entry,
16
+ planned,
17
+ children
18
+ }: {
19
+ title: string;
20
+ file: string;
21
+ kind: SpecKind;
22
+ entry: SpecEvidence | undefined;
23
+ planned?: number;
24
+ children?: ReactNode;
25
+ }) {
13
26
  const { backends, ready, unavailable, pending } = useEvidence();
14
27
  const now = useNow();
15
28
  const summary = entry?.summary ?? NO_EVIDENCE;
16
- const newest = entry?.slots.flatMap((slot) => slot.outcomes).reduce<(typeof entry.slots)[number]['outcomes'][number] | null>((best, result) => (!best || result.layer.recordedAtMs > best.layer.recordedAtMs ? result : best), null);
29
+ const newest = entry?.slots
30
+ .flatMap((slot) => slot.outcomes)
31
+ .reduce<(typeof entry.slots)[number]['outcomes'][number] | null>(
32
+ (best, result) => (!best || result.layer.recordedAtMs > best.layer.recordedAtMs ? result : best),
33
+ null
34
+ );
17
35
  const missing = planned === undefined ? 0 : Math.max(0, planned - summary.total);
18
36
  return (
19
- <header className={styles.specHead}>
20
- <p className={styles.path}>specs/{file}</p>
21
- <h1 className={styles.title}>{title}</h1>
37
+ <Box component="header" mb="lg">
38
+ <Text component="div" c="dimmed">
39
+ specs/{file}
40
+ </Text>
41
+ <Group gap="sm" mb="sm" wrap="nowrap">
42
+ <SpecLogo kind={kind} />
43
+ <Title order={1}>{title}</Title>
44
+ </Group>
22
45
  {children}
23
- <div className={styles.summary}>
46
+ <Stack gap="sm" miw={0}>
24
47
  <Meter counts={summary.counts} missing={missing} />
25
- <p className={styles.summaryLine}>
48
+ <Flex gap="xs" align="center" wrap="wrap">
26
49
  {unavailable ? (
27
- <span>Recorded results are unavailable.</span>
50
+ <Text span inherit>
51
+ Recorded results are unavailable.
52
+ </Text>
28
53
  ) : !ready ? (
29
- <span role="status">Loading recorded results…</span>
54
+ <Text role="status" span inherit>
55
+ Loading recorded results…
56
+ </Text>
30
57
  ) : summary.status === 'none' ? (
31
- <span>{pending > 0 ? 'Loading recorded results…' : `No completed test report for this ${kind.one} yet.`}</span>
58
+ <Text span inherit>
59
+ {pending > 0 ? 'Loading recorded results…' : `No completed test report for this ${kind.one} yet.`}
60
+ </Text>
32
61
  ) : (
33
62
  <>
34
63
  <CountLine summary={summary} noun={kind.check} nouns={`${kind.check}s`} />
35
- {missing > 0 && <span>{missing} without a result</span>}
64
+ {missing > 0 && (
65
+ <Text span inherit>
66
+ {missing} without a result
67
+ </Text>
68
+ )}
36
69
  </>
37
70
  )}
38
- </p>
71
+ </Flex>
39
72
  {newest && (
40
- <p className={styles.summaryOrigin}>
73
+ <Flex gap="xs" align="center" wrap="wrap" c="dimmed">
41
74
  Latest from <Origin layer={newest.layer} backends={backends} now={now} />
42
- </p>
75
+ </Flex>
43
76
  )}
44
- </div>
45
- </header>
46
- );
47
- }
48
-
49
- /** The document as written, folded away under what was understood from it. */
50
- export function SourceText({ text, label = 'Source', open = false }: { text: string; label?: string; open?: boolean }) {
51
- return (
52
- <details className={styles.source} open={open}>
53
- <summary>{label}</summary>
54
- <pre className={styles.code}>
55
- {text.split('\n').map((line, index) => (
56
- <span id={`line-${index + 1}`} key={index}>
57
- {line}
58
- {'\n'}
59
- </span>
60
- ))}
61
- </pre>
62
- </details>
77
+ </Stack>
78
+ </Box>
63
79
  );
64
80
  }
@@ -1,21 +1,22 @@
1
1
  import { addSummaries, NO_EVIDENCE, type SpecSummary } from '@crvouga/atlas/reports/evidence-view';
2
+ import { Box, Button, Group, NavLink, Stack, Text, Title } from '@mantine/core';
2
3
  import { Link, useSearch } from '@tanstack/react-router';
3
4
  import { useMemo } from 'react';
4
-
5
5
  import { folderOf, outlinePath, specFile, type SpecKind } from '../spec-kind';
6
6
  import { useUiStore, type ResultFilter } from '../state/ui-store';
7
7
  import { useEvidence } from './evidence-context';
8
- import styles from './evidence.module.css';
9
8
  import { StatusMark, Tally } from './marks';
10
-
11
- const FILTERS: readonly { readonly id: ResultFilter; readonly label: string; readonly keeps: (summary: SpecSummary) => boolean }[] = [
9
+ const FILTERS: readonly {
10
+ readonly id: ResultFilter;
11
+ readonly label: string;
12
+ readonly keeps: (summary: SpecSummary) => boolean;
13
+ }[] = [
12
14
  { id: 'all', label: 'All', keeps: () => true },
13
15
  { id: 'results', label: 'Results', keeps: (summary) => summary.status !== 'none' },
14
16
  { id: 'running', label: 'Running', keeps: (summary) => summary.counts.running > 0 },
15
17
  { id: 'failed', label: 'Failing', keeps: (summary) => summary.counts.failed > 0 },
16
18
  { id: 'none', label: 'None', keeps: (summary) => summary.status === 'none' }
17
19
  ];
18
-
19
20
  /** Every spec of one kind, grouped by folder, each with what the combined report holds for it. */
20
21
  export function SpecIndex({ kind }: { kind: SpecKind }) {
21
22
  const evidence = useEvidence();
@@ -24,7 +25,12 @@ export function SpecIndex({ kind }: { kind: SpecKind }) {
24
25
  const setFilter = useUiStore((s) => s.setResultFilter);
25
26
  const open = typeof search.spec === 'string' ? specFile(search.spec) : '';
26
27
  const rows = useMemo(
27
- () => evidence.files.filter(kind.accepts).map((file) => ({ file, title: evidence.titleOf(file), summary: evidence.view.specs.get(outlinePath(file))?.summary ?? NO_EVIDENCE })),
28
+ () =>
29
+ evidence.files.filter(kind.accepts).map((file) => ({
30
+ file,
31
+ title: evidence.titleOf(file),
32
+ summary: evidence.view.specs.get(outlinePath(file))?.summary ?? NO_EVIDENCE
33
+ })),
28
34
  // `titleOf` reads the outline the view was built from.
29
35
  [evidence.files, evidence.view, kind]
30
36
  );
@@ -36,40 +42,63 @@ export function SpecIndex({ kind }: { kind: SpecKind }) {
36
42
  return [...byFolder];
37
43
  }, [visible]);
38
44
  return (
39
- <nav className={styles.index} aria-label={`${kind.label} ${kind.many}`}>
40
- <div className={styles.filters} role="group" aria-label="Show">
45
+ <Box aria-label={`${kind.label} ${kind.many}`} component="nav" p="md">
46
+ <Group role="group" aria-label="Show" gap="xs" mb="md">
41
47
  {FILTERS.map((entry) => (
42
- <button key={entry.id} type="button" aria-pressed={filter === entry.id} onClick={() => setFilter(entry.id)}>
43
- {entry.label} <span>{rows.filter((row) => entry.keeps(row.summary)).length}</span>
44
- </button>
48
+ <Button
49
+ key={entry.id}
50
+ type="button"
51
+ aria-pressed={filter === entry.id}
52
+ onClick={() => setFilter(entry.id)}
53
+ variant={filter === entry.id ? 'light' : 'subtle'}
54
+ >
55
+ {entry.label}{' '}
56
+ <Text span inherit>
57
+ {rows.filter((row) => entry.keeps(row.summary)).length}
58
+ </Text>
59
+ </Button>
45
60
  ))}
46
- </div>
47
- {evidence.filesPending && <p className={styles.indexNote}>Loading…</p>}
48
- {evidence.filesError && <p className={styles.indexNote}>The file list could not be loaded.</p>}
61
+ </Group>
62
+ {evidence.filesPending && <Text component="div">Loading…</Text>}
63
+ {evidence.filesError && <Text component="div">The file list could not be loaded.</Text>}
49
64
  {!evidence.filesPending && !evidence.filesError && visible.length === 0 && (
50
- <p className={styles.indexNote}>
51
- {rows.length === 0 ? `No ${kind.many} in this spec set.` : filter === 'running' ? `No running ${kind.many}.` : filter === 'failed' ? `No failing ${kind.many}.` : filter === 'results' ? `No ${kind.many} with results yet.` : `Every ${kind.one} has results.`}
52
- </p>
65
+ <Text component="div">
66
+ {rows.length === 0
67
+ ? `No ${kind.many} in this spec set.`
68
+ : filter === 'running'
69
+ ? `No running ${kind.many}.`
70
+ : filter === 'failed'
71
+ ? `No failing ${kind.many}.`
72
+ : filter === 'results'
73
+ ? `No ${kind.many} with results yet.`
74
+ : `Every ${kind.one} has results.`}
75
+ </Text>
53
76
  )}
54
77
  {groups.map(([folder, entries]) => (
55
- <section key={folder || kind.id}>
56
- <h3>
57
- <span>{folder || kind.label}</span>
78
+ <Box key={folder || kind.id} component="section" mb="lg">
79
+ <Title order={3} mb="sm">
80
+ <Text span inherit>
81
+ {folder || kind.label}
82
+ </Text>
58
83
  <Tally summary={addSummaries(entries.map((row) => row.summary))} />
59
- </h3>
60
- <ul>
84
+ </Title>
85
+ <Stack gap="xs">
61
86
  {entries.map((row) => (
62
- <li key={row.file}>
63
- <Link className={styles.indexRow} to="/specs/$kind" params={{ kind: kind.id }} search={{ spec: row.file }} title={row.file} aria-current={open === row.file ? 'page' : undefined} data-result={row.summary.status}>
64
- <StatusMark status={row.summary.status} size={14} />
65
- <span className={styles.indexName}>{row.title}</span>
66
- {row.summary.status !== 'none' && <Tally summary={row.summary} />}
67
- </Link>
68
- </li>
87
+ <NavLink
88
+ key={row.file}
89
+ label={row.title}
90
+ title={row.file}
91
+ active={open === row.file}
92
+ leftSection={<StatusMark status={row.summary.status} size={14} />}
93
+ rightSection={row.summary.status !== 'none' ? <Tally summary={row.summary} /> : undefined}
94
+ data-result={row.summary.status}
95
+ aria-current={open === row.file ? 'page' : undefined}
96
+ renderRoot={(props) => <Link {...props} to="/specs/$kind" params={{ kind: kind.id }} search={{ spec: row.file }} />}
97
+ />
69
98
  ))}
70
- </ul>
71
- </section>
99
+ </Stack>
100
+ </Box>
72
101
  ))}
73
- </nav>
102
+ </Box>
74
103
  );
75
104
  }
@@ -1,24 +1,32 @@
1
+ import { Alert, Button, Container, Text, Title } from '@mantine/core';
1
2
  import { useQuery } from '@tanstack/react-query';
2
3
  import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
3
4
  import { lazy, Suspense } from 'react';
4
-
5
5
  import { keys, specKey, useAdapter } from '../data/queries';
6
6
  import { outlinePath, specFile, specKind } from '../spec-kind';
7
7
  import type { SelectCheck } from './check-detail';
8
8
  import { useEvidence } from './evidence-context';
9
- import styles from './evidence.module.css';
10
9
  import { Overview } from './overview';
11
-
12
10
  // Each kind of spec has a page with a parser of its own (Gherkin, Quint, DMN); a page loads only the one it shows.
13
11
  const DocumentPage = lazy(() => import('./document-page').then((m) => ({ default: m.DocumentPage })));
14
12
  const FeaturePage = lazy(() => import('./feature-page').then((m) => ({ default: m.FeaturePage })));
15
13
  const ModelPage = lazy(() => import('./model-page').then((m) => ({ default: m.ModelPage })));
16
14
  const RecordingPage = lazy(() => import('./recording-page').then((m) => ({ default: m.RecordingPage })));
17
-
18
15
  function Loading({ children }: { children: React.ReactNode }) {
19
- return <Suspense fallback={<article className={styles.page}><p className={styles.aside} role="status">Loading…</p></article>}>{children}</Suspense>;
16
+ return (
17
+ <Suspense
18
+ fallback={
19
+ <Container>
20
+ <Text role="status" c="dimmed">
21
+ Loading…
22
+ </Text>
23
+ </Container>
24
+ }
25
+ >
26
+ {children}
27
+ </Suspense>
28
+ );
20
29
  }
21
-
22
30
  /** One spec tab: its overview, one of its documents, or a recording opened from that document. */
23
31
  export function SpecPage() {
24
32
  const params = useParams({ from: '/specs/$kind' });
@@ -29,26 +37,37 @@ export function SpecPage() {
29
37
  const kind = specKind(params.kind);
30
38
  const file = search.spec ? specFile(search.spec) : '';
31
39
  const readable = kind !== null && file.length > 0 && kind.accepts(file);
32
- const text = useQuery({ queryKey: keys.specFile(specKey(adapter), file), queryFn: () => adapter.specFile(file), enabled: readable, retry: false });
33
-
40
+ const text = useQuery({
41
+ queryKey: keys.specFile(specKey(adapter), file),
42
+ queryFn: () => adapter.specFile(file),
43
+ enabled: readable,
44
+ retry: false
45
+ });
34
46
  if (!kind) {
35
47
  return (
36
- <article className={styles.page}>
37
- <h1 className={styles.title}>No such kind of spec</h1>
38
- <p className={styles.aside}>Pick a tab above.</p>
39
- </article>
48
+ <Container component="article" size="xl" w="100%" py="xl">
49
+ <Title order={1} mb="sm">
50
+ No such kind of spec
51
+ </Title>
52
+ <Text component="div" c="dimmed">
53
+ Pick a tab above.
54
+ </Text>
55
+ </Container>
40
56
  );
41
57
  }
42
58
  if (!file) return <Overview kind={kind} />;
43
59
  if (!readable) {
44
60
  return (
45
- <article className={styles.page}>
46
- <h1 className={styles.title}>{kind.label}</h1>
47
- <p className={styles.aside}>This file is not a {kind.one}.</p>
48
- </article>
61
+ <Container component="article" size="xl" w="100%" py="xl">
62
+ <Title order={1} mb="sm">
63
+ {kind.label}
64
+ </Title>
65
+ <Text component="div" c="dimmed">
66
+ This file is not a {kind.one}.
67
+ </Text>
68
+ </Container>
49
69
  );
50
70
  }
51
-
52
71
  const entry = evidence.view.specs.get(outlinePath(file));
53
72
  const select: SelectCheck = (patch) => {
54
73
  const value = <T,>(next: T | null | undefined, current: T | undefined) => (next === undefined ? current : (next ?? undefined));
@@ -63,7 +82,6 @@ export function SpecPage() {
63
82
  })
64
83
  });
65
84
  };
66
-
67
85
  if (search.view === 'recording' && search.check) {
68
86
  const slot = entry?.slots.find((candidate) => candidate.id === search.check);
69
87
  // The way back already names the spec; the heading is the check itself.
@@ -83,17 +101,30 @@ export function SpecPage() {
83
101
  </Loading>
84
102
  );
85
103
  }
86
- if (text.isPending) return <article className={styles.page}><p className={styles.aside} role="status">Loading the {kind.one}…</p></article>;
104
+ if (text.isPending)
105
+ return (
106
+ <Container component="article" size="xl" w="100%" py="xl">
107
+ <Text role="status" component="div" c="dimmed">
108
+ Loading the {kind.one}…
109
+ </Text>
110
+ </Container>
111
+ );
87
112
  if (text.isError) {
88
113
  return (
89
- <article className={styles.page}>
90
- <p className={styles.notice} role="alert">
114
+ <Container component="article" size="xl" w="100%" py="xl">
115
+ <Alert role="alert" color={'red'} my="sm">
91
116
  The {kind.one} could not be read.{' '}
92
- <button type="button" className={styles.link} onClick={() => void text.refetch()}>Try again</button>
93
- </p>
94
- </article>
117
+ <Button type="button" onClick={() => void text.refetch()} variant="subtle">
118
+ Try again
119
+ </Button>
120
+ </Alert>
121
+ </Container>
95
122
  );
96
123
  }
97
124
  const page = { file, text: text.data, kind, entry, selection: { check: search.check, layer: search.layer }, select };
98
- return <Loading>{kind.id === 'gherkin' ? <FeaturePage {...page} /> : kind.id === 'quint' ? <ModelPage {...page} /> : <DocumentPage {...page} />}</Loading>;
125
+ return (
126
+ <Loading>
127
+ {kind.id === 'gherkin' ? <FeaturePage {...page} /> : kind.id === 'quint' ? <ModelPage {...page} /> : <DocumentPage {...page} />}
128
+ </Loading>
129
+ );
99
130
  }
@@ -1,21 +1,45 @@
1
1
  import type { CheckSlot } from '@crvouga/atlas/reports/evidence-view';
2
+ import { Button, Group, Text } from '@mantine/core';
2
3
  import { useUiStore } from '../state/ui-store';
3
- import styles from './evidence.module.css';
4
-
5
4
  /** Filter actual test rows as well as the spec index. Counts always describe the whole document. */
6
5
  export function TestFilter({ slots }: { slots: readonly CheckSlot[] }) {
7
- const filter = useUiStore(s => s.resultFilter);
8
- const setFilter = useUiStore(s => s.setResultFilter);
9
- const running = slots.filter(slot => slot.status === 'running').length;
10
- return <>
11
- <div className={styles.filters} role="group" aria-label="Test status">
12
- <button type="button" aria-pressed={filter !== 'running'} onClick={() => setFilter('all')}>All tests <span>{slots.length}</span></button>
13
- <button type="button" aria-pressed={filter === 'running'} onClick={() => setFilter('running')}>Running <span>{running}</span></button>
14
- </div>
15
- {filter === 'running' && running === 0 && <p className={styles.aside} role="status">No running tests.</p>}
16
- </>;
6
+ const filter = useUiStore((s) => s.resultFilter);
7
+ const setFilter = useUiStore((s) => s.setResultFilter);
8
+ const running = slots.filter((slot) => slot.status === 'running').length;
9
+ return (
10
+ <>
11
+ <Group role="group" aria-label="Test status" gap="sm" my="md">
12
+ <Button
13
+ type="button"
14
+ aria-pressed={filter !== 'running'}
15
+ onClick={() => setFilter('all')}
16
+ variant={filter !== 'running' ? 'light' : 'subtle'}
17
+ >
18
+ All tests{' '}
19
+ <Text span inherit>
20
+ {slots.length}
21
+ </Text>
22
+ </Button>
23
+ <Button
24
+ type="button"
25
+ aria-pressed={filter === 'running'}
26
+ onClick={() => setFilter('running')}
27
+ variant={filter === 'running' ? 'light' : 'subtle'}
28
+ >
29
+ Running{' '}
30
+ <Text span inherit>
31
+ {running}
32
+ </Text>
33
+ </Button>
34
+ </Group>
35
+ {filter === 'running' && running === 0 && (
36
+ <Text role="status" component="div" c="dimmed">
37
+ No running tests.
38
+ </Text>
39
+ )}
40
+ </>
41
+ );
17
42
  }
18
-
19
43
  export function useRunningFilter() {
20
- return useUiStore(s => s.resultFilter === 'running');
44
+ return useUiStore((s) => s.resultFilter === 'running');
21
45
  }