@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
@@ -0,0 +1,152 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3
+ "name": "quint",
4
+ "patterns": [
5
+ {
6
+ "include": "#hashbangLine"
7
+ },
8
+ {
9
+ "include": "#docComments"
10
+ },
11
+ {
12
+ "include": "#lineComments"
13
+ },
14
+ {
15
+ "include": "#blockComments"
16
+ },
17
+ {
18
+ "include": "#keywords"
19
+ },
20
+ {
21
+ "include": "#storage"
22
+ },
23
+ {
24
+ "include": "#constants"
25
+ },
26
+ {
27
+ "include": "#punctuation"
28
+ },
29
+ {
30
+ "include": "#operators"
31
+ },
32
+ {
33
+ "include": "#identifiers"
34
+ },
35
+ {
36
+ "include": "#strings"
37
+ },
38
+ {
39
+ "include": "#numbers"
40
+ }
41
+ ],
42
+ "repository": {
43
+ "identifiers": {
44
+ "patterns": [
45
+ {
46
+ "name": "meta.quint",
47
+ "match": "[a-zA-Z_]([a-zA-Z0-9_])*"
48
+ }
49
+ ]
50
+ },
51
+ "keywords": {
52
+ "patterns": [
53
+ {
54
+ "name": "keyword.control.quint",
55
+ "match": "\\b(module|import|from|export|as|if|else|match|not|or|and|implies|iff|all|any|leadsTo|strongFair|weakFair)\\b"
56
+ }
57
+ ]
58
+ },
59
+ "storage": {
60
+ "patterns": [
61
+ {
62
+ "name": "storage.modifier.quint",
63
+ "match": "\\b(type|assume|const|var|val|nondet|def|pure|action|temporal|run)\\b"
64
+ },
65
+ {
66
+ "name": "storage.type.quint",
67
+ "match": "\\b(Set|List|int|str|bool)\\b"
68
+ },
69
+ {
70
+ "name": "support.type.quint",
71
+ "match": "\\b(Map|Tup|Rec)\\b"
72
+ }
73
+ ]
74
+ },
75
+ "operators": {
76
+ "patterns": [
77
+ {
78
+ "name": "keyword.operator.quint",
79
+ "match": "(=>|->|<=|>|<|=|!=|'|\\.|\\*|\\+|-|/|%|\\^)"
80
+ }
81
+ ]
82
+ },
83
+ "strings": {
84
+ "name": "string.quoted.double.quint",
85
+ "begin": "\"",
86
+ "end": "\""
87
+ },
88
+ "numbers": {
89
+ "patterns": [
90
+ {
91
+ "name": "constant.numeric.quint",
92
+ "match": "-?(0x[0-9a-fA-F]([0-9a-fA-F]|_[0-9a-fA-F])*|0|[1-9]([0-9]|_[0-9])*)"
93
+ }
94
+ ]
95
+ },
96
+ "constants": {
97
+ "patterns": [
98
+ {
99
+ "name": "constant.language.quint",
100
+ "match": "\\b(false|true|Bool|Int|Nat)\\b"
101
+ }
102
+ ]
103
+ },
104
+ "hashbangLine": {
105
+ "patterns": [
106
+ {
107
+ "name": "comment.line.hashbang.quint",
108
+ "match": "\\A#!.*$"
109
+ }
110
+ ]
111
+ },
112
+ "docComments": {
113
+ "patterns": [
114
+ {
115
+ "name": "comment.block.documentation.quint",
116
+ "match": "///.*$"
117
+ }
118
+ ]
119
+ },
120
+ "punctuation": {
121
+ "patterns": [
122
+ {
123
+ "name": "keyword.operator.spread.quint",
124
+ "match": "\\.\\.\\."
125
+ },
126
+ {
127
+ "name": "punctuation.accessor.quint",
128
+ "match": "::"
129
+ },
130
+ {
131
+ "name": "punctuation.separator.quint",
132
+ "match": "\\|"
133
+ }
134
+ ]
135
+ },
136
+ "lineComments": {
137
+ "patterns": [
138
+ {
139
+ "name": "comment.line.double-slash.quint",
140
+ "match": "//.*$"
141
+ }
142
+ ]
143
+ },
144
+ "blockComments": {
145
+ "name": "comment.block.quint",
146
+ "begin": "/\\*",
147
+ "end": "\\*/",
148
+ "patterns": []
149
+ }
150
+ },
151
+ "scopeName": "source.quint"
152
+ }
@@ -0,0 +1,50 @@
1
+ import { createShikiAdapter } from '@mantine/code-highlight';
2
+ import type { HighlighterCore, LanguageInput, LanguageRegistration } from 'shiki/core';
3
+
4
+ const grammars: Record<string, () => Promise<LanguageInput>> = {
5
+ gherkin: () => import('shiki/langs/gherkin.mjs').then((module) => module.default),
6
+ yaml: () => import('shiki/langs/yaml.mjs').then((module) => module.default),
7
+ xml: () => import('shiki/langs/xml.mjs').then((module) => module.default),
8
+ json: () => import('shiki/langs/json.mjs').then((module) => module.default),
9
+ quint: () => import('./grammars/quint.json').then((module) => module.default as unknown as LanguageRegistration),
10
+ alloy: () => import('./grammars/alloy.json').then((module) => module.default as unknown as LanguageRegistration)
11
+ };
12
+
13
+ async function createContext() {
14
+ const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] = await Promise.all([
15
+ import('shiki/core'),
16
+ import('shiki/engine/javascript')
17
+ ]);
18
+ const highlighter = await createHighlighterCore({ langs: [], themes: [], engine: createJavaScriptRegexEngine() });
19
+ return {
20
+ getLoadedLanguages: () => highlighter.getLoadedLanguages(),
21
+ loadLanguage: (...languages: Parameters<HighlighterCore['loadLanguage']>) => highlighter.loadLanguage(...languages),
22
+ codeToHtml: (code: string, options: Parameters<HighlighterCore['codeToHtml']>[1]) =>
23
+ highlighter.codeToHtml(code, {
24
+ ...options,
25
+ transformers: [
26
+ {
27
+ line(node, line) {
28
+ node.properties.id = `line-${line}`;
29
+ }
30
+ }
31
+ ]
32
+ })
33
+ };
34
+ }
35
+ let context: ReturnType<typeof createContext> | undefined;
36
+ /** One lazy, cached syntax engine; only a viewed file's grammar is downloaded. */
37
+ export const specHighlightAdapter = createShikiAdapter(() => (context ??= createContext()), {
38
+ resolveLanguage: (language) => grammars[language]
39
+ });
40
+
41
+ /** Choose the actual notation, never a visually similar language. */
42
+ export function specLanguage(file: string): string {
43
+ if (/\.feature$/i.test(file)) return 'gherkin';
44
+ if (/\.qnt$/i.test(file)) return 'quint';
45
+ if (/\.als$/i.test(file)) return 'alloy';
46
+ if (/\.(dmn|scxml|xml)$/i.test(file)) return 'xml';
47
+ if (/\.ya?ml$/i.test(file)) return 'yaml';
48
+ if (/\.json$/i.test(file)) return 'json';
49
+ return 'text';
50
+ }
@@ -0,0 +1,109 @@
1
+ /** @module Viewer design system
2
+ * The sole Mantine theme and provider for the report viewer and server settings.
3
+ */
4
+ import { CodeHighlight, CodeHighlightTabs } from '@mantine/code-highlight';
5
+ import {
6
+ ActionIcon,
7
+ DEFAULT_THEME,
8
+ darken,
9
+ type MantineColorsTuple,
10
+ type CSSVariablesResolver,
11
+ Anchor,
12
+ Badge,
13
+ Button,
14
+ Card,
15
+ createTheme,
16
+ Drawer,
17
+ InputWrapper,
18
+ List,
19
+ MantineProvider,
20
+ Modal,
21
+ NativeSelect,
22
+ Paper,
23
+ Table,
24
+ Text,
25
+ Title,
26
+ Tooltip
27
+ } from '@mantine/core';
28
+ import type { ReactNode } from 'react';
29
+ // Retain Mantine's scales while making small outcome labels readable on light surfaces.
30
+ const readableScale = (scale: MantineColorsTuple) =>
31
+ scale.map((color, index) => (index >= 6 ? darken(color, 0.25) : color)) as unknown as MantineColorsTuple;
32
+ const cssVariablesResolver: CSSVariablesResolver = () => ({
33
+ variables: {},
34
+ light: { '--mantine-color-dimmed': 'var(--mantine-color-gray-7)' },
35
+ dark: {}
36
+ });
37
+
38
+ /** Atlas uses Mantine's blue scale for navigation and standard outcome colors for evidence. */
39
+ export const theme = createTheme({
40
+ primaryColor: 'blue',
41
+ primaryShade: 8,
42
+ autoContrast: true,
43
+ colors: {
44
+ green: readableScale(DEFAULT_THEME.colors.green),
45
+ red: readableScale(DEFAULT_THEME.colors.red),
46
+ orange: readableScale(DEFAULT_THEME.colors.orange)
47
+ },
48
+ defaultRadius: 'md',
49
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
50
+ fontFamilyMonospace: 'ui-monospace, SFMono-Regular, Menlo, monospace',
51
+ headings: { fontWeight: '650', sizes: { h1: { fontSize: '1.75rem' }, h2: { fontSize: '1.25rem' }, h3: { fontSize: '1rem' } } },
52
+ respectReducedMotion: true,
53
+ focusRing: 'auto',
54
+ components: {
55
+ CodeHighlight: CodeHighlight.extend({
56
+ defaultProps: { withLineNumbers: true, withFirstLineIndentation: true, defaultExpanded: true, withBorder: true, radius: 'md' }
57
+ }),
58
+ CodeHighlightTabs: CodeHighlightTabs.extend({
59
+ defaultProps: {
60
+ withLineNumbers: true,
61
+ withFirstLineIndentation: true,
62
+ defaultExpanded: true,
63
+ withBorder: true,
64
+ radius: 'md',
65
+ withExpandButton: true,
66
+ maxCollapsedHeight: '70vh'
67
+ }
68
+ }),
69
+ // Mantine List wraps labels in inline-flex; report rows need the available width.
70
+ // Keep this single supported Styles API adjustment here, not in each feature.
71
+ List: List.extend({ styles: { itemWrapper: { display: 'flex' }, itemLabel: { flex: 1, minWidth: 0, width: '100%' } } }),
72
+ Button: Button.extend({ defaultProps: { size: 'sm', variant: 'light' } }),
73
+ ActionIcon: ActionIcon.extend({ defaultProps: { size: 'lg', variant: 'subtle' } }),
74
+ Anchor: Anchor.extend({ defaultProps: { size: 'sm', underline: 'hover' } }),
75
+ Badge: Badge.extend({ defaultProps: { variant: 'light', tt: 'none' } }),
76
+ Paper: Paper.extend({ defaultProps: { p: 'md' } }),
77
+ Card: Card.extend({ defaultProps: { withBorder: true, padding: 'md' } }),
78
+ Text: Text.extend({ defaultProps: { size: 'sm' } }),
79
+ Title: Title.extend({ defaultProps: { textWrap: 'pretty' } }),
80
+ InputWrapper: InputWrapper.extend({ defaultProps: { size: 'sm' } }),
81
+ NativeSelect: NativeSelect.extend({ defaultProps: { size: 'sm' } }),
82
+ Modal: Modal.extend({
83
+ defaultProps: { centered: true, size: 'lg', overlayProps: { backgroundOpacity: 0.35 }, transitionProps: { duration: 120 } }
84
+ }),
85
+ Drawer: Drawer.extend({ defaultProps: { position: 'right', size: 'lg', padding: 'lg', transitionProps: { duration: 120 } } }),
86
+ Table: Table.extend({ defaultProps: { verticalSpacing: 'sm', horizontalSpacing: 'sm', highlightOnHover: true, tabularNums: true } }),
87
+ Tooltip: Tooltip.extend({ defaultProps: { openDelay: 300, withArrow: true } })
88
+ }
89
+ });
90
+ /** Use this provider at every UI root; the viewer currently has a light visual identity. */
91
+ export function AtlasThemeProvider({ children, env }: { children: ReactNode; env?: 'test' }) {
92
+ return (
93
+ <MantineProvider env={env} theme={theme} cssVariablesResolver={cssVariablesResolver} forceColorScheme="light">
94
+ {children}
95
+ </MantineProvider>
96
+ );
97
+ }
98
+ /** Outcome colors shared by report badges, progress bars, and the graph. Unknown outcomes stay gray. */
99
+ export function statusColor(status: string): string {
100
+ return status === 'passed'
101
+ ? 'green'
102
+ : status === 'failed'
103
+ ? 'red'
104
+ : status === 'flaky'
105
+ ? 'orange'
106
+ : status === 'running'
107
+ ? 'blue'
108
+ : 'gray';
109
+ }
@@ -1,26 +1,32 @@
1
+ import { Anchor, Container, Flex, Stack, Text, Title } from '@mantine/core';
1
2
  import { Link, useParams, useSearch } from '@tanstack/react-router';
2
-
3
3
  import { useAtlas } from '../app/atlas-context';
4
4
  import { mapFrame } from '../layout/graph';
5
5
  import { ChartMap } from '../map/ChartMap';
6
- import { SidePanel } from './SidePanel';
7
- import styles from './views.module.css';
8
6
  import { ChartTests } from './ChartTests';
9
-
7
+ import { ChartSource } from '../evidence/chart-source';
8
+ import { SidePanel } from './SidePanel';
10
9
  export function ChartPage() {
11
10
  const { chartId } = useParams({ from: '/chart/$chartId' });
12
11
  const search = useSearch({ from: '/chart/$chartId' });
13
12
  const { view } = useAtlas();
14
- if (!view) return <div className={styles.plateScroll} role="status">Loading the spec…</div>;
13
+ if (!view)
14
+ return (
15
+ <Container role="status" component="article" size="xl" w="100%" py="xl">
16
+ Loading the spec…
17
+ </Container>
18
+ );
15
19
  if (!view.charts.has(chartId)) {
16
20
  return (
17
- <div className={styles.plateScroll}>
18
- <h1 className={styles.plateTitle}>This chart isn’t in the spec</h1>
19
- <p className={styles.lead}>It may have been renamed or removed since this link was shared.</p>
20
- <Link to="/" search={(prev) => ({ run: prev.run })} className={styles.button}>
21
- Back to the product map
22
- </Link>
23
- </div>
21
+ <Container component="article" size="xl" w="100%" py="xl">
22
+ <Title order={1} mb="sm">
23
+ This chart isn’t in the spec
24
+ </Title>
25
+ <Text component="div" c="dimmed">
26
+ It may have been renamed or removed since this link was shared.
27
+ </Text>
28
+ <Anchor renderRoot={(props) => <Link {...props} to="/" search={(prev) => ({ run: prev.run })} />}>Back to the product map</Anchor>
29
+ </Container>
24
30
  );
25
31
  }
26
32
  const selection = { screen: search.screen, event: search.event, journey: search.journey };
@@ -30,25 +36,39 @@ export function ChartPage() {
30
36
  const frame = mapFrame(view, chartId, search.group ?? null);
31
37
  const homeFocus = mapFrame(view, chartId, null).focus;
32
38
  return (
33
- <div className={styles.mapPlate}>
34
- <div className={styles.mapHeading}>
35
- <h1>{chart.name}</h1>
36
- {chart.description && <p>{chart.description}</p>}
39
+ <Stack gap="sm" p="md">
40
+ <Flex gap="xs" align="center" wrap="wrap">
41
+ <Title order={1} mb="sm">
42
+ {chart.name}
43
+ </Title>
44
+ {chart.description && <Text component="div">{chart.description}</Text>}
37
45
  {frame.focus && (
38
- <p className={styles.mapScope}>
39
- <span>{frame.focus}</span>
46
+ <Text component="div">
47
+ <Text span inherit>
48
+ {frame.focus}
49
+ </Text>
40
50
  {frame.wider && (
41
- <Link to="/chart/$chartId" params={{ chartId }} search={(prev) => ({ run: prev.run, group: frame.wider && frame.wider !== homeFocus ? frame.wider : undefined })}>
51
+ <Anchor
52
+ renderRoot={(props) => (
53
+ <Link
54
+ {...props}
55
+ to="/chart/$chartId"
56
+ params={{ chartId }}
57
+ search={(prev) => ({ run: prev.run, group: frame.wider && frame.wider !== homeFocus ? frame.wider : undefined })}
58
+ />
59
+ )}
60
+ >
42
61
  Wider map
43
- </Link>
62
+ </Anchor>
44
63
  )}
45
- </p>
64
+ </Text>
46
65
  )}
47
- </div>
48
- <div className={styles.mapArea}>
66
+ </Flex>
67
+ <ChartSource file={chart.file} />
68
+ <Stack gap="sm">
49
69
  <ChartMap key={`${chartId}:${search.group ?? ''}`} view={view} chartId={chartId} selection={selection} group={search.group} />
50
- </div>
51
- <ChartTests view={view} chartId={chartId} compact />
52
- </div>
70
+ </Stack>
71
+ <ChartTests view={view} chartId={chartId} />
72
+ </Stack>
53
73
  );
54
74
  }
@@ -1,14 +1,12 @@
1
+ import { Box, List, Title } from '@mantine/core';
1
2
  import { lazy, Suspense, useState } from 'react';
2
3
  import type { AtlasView } from '../data/model';
3
- import { useEvidence } from '../evidence/evidence-context';
4
4
  import { CheckDetail, ResultRow, type CheckSelection } from '../evidence/check-detail';
5
- import { TestFilter, useRunningFilter } from '../evidence/test-filter';
5
+ import { useEvidence } from '../evidence/evidence-context';
6
6
  import { Bounded } from '../evidence/marks';
7
+ import { TestFilter, useRunningFilter } from '../evidence/test-filter';
7
8
  import { outlinePath } from '../spec-kind';
8
- import styles from '../evidence/evidence.module.css';
9
-
10
- const RecordingPage = lazy(() => import('../evidence/recording-page').then(module => ({ default: module.RecordingPage })));
11
-
9
+ const RecordingPage = lazy(() => import('../evidence/recording-page').then((module) => ({ default: module.RecordingPage })));
12
10
  /** Statechart test reports stay in the chart, including while their recordings are still arriving. */
13
11
  export function ChartTests({ view, chartId, compact = false }: { view: AtlasView; chartId: string; compact?: boolean }) {
14
12
  const evidence = useEvidence();
@@ -23,21 +21,60 @@ export function ChartTests({ view, chartId, compact = false }: { view: AtlasView
23
21
  files.add(outlinePath(chart.file));
24
22
  chart = chart.parent ? view.charts.get(chart.parent.chartId) : undefined;
25
23
  }
26
- const slots = [...files].flatMap(file => evidence.view.specs.get(file)?.slots ?? []);
27
- const visible = slots.filter(slot => !runningOnly || slot.status === 'running');
24
+ const slots = [...files].flatMap((file) => evidence.view.specs.get(file)?.slots ?? []);
25
+ const visible = slots.filter((slot) => !runningOnly || slot.status === 'running');
28
26
  if (!slots.length) return null;
29
- return <section aria-label="Chart tests" className={styles.block} style={compact ? { maxHeight: '40vh', overflowY: 'auto', flexShrink: 0 } : undefined}>
30
- <h3 className={styles.blockTitle}>Tests</h3>
31
- <TestFilter slots={slots} />
32
- <ul className={styles.results}><Bounded items={visible} size={40} more="More tests">{slot =>
33
- <ResultRow key={slot.key} label={slot.title} slot={slot} open={selectedKey === slot.key}
34
- onToggle={() => { setSelectedKey(selectedKey === slot.key ? null : slot.key); setSelection({ check: slot.id }); setRecording(false); setMoment(null); }}>
35
- {recording ? <Suspense fallback={<p role="status">Loading the recording…</p>}><RecordingPage slot={slot} layerId={selection.layer} label={slot.title} back="Test report" moment={moment} onMoment={setMoment} onClose={() => setRecording(false)} /></Suspense>
36
- : <CheckDetail slot={slot} layerId={selection.layer} reads="steps" select={patch => {
37
- setSelection({ check: patch.check ?? slot.id, layer: patch.layer ?? undefined });
38
- if (patch.view === 'recording') { setRecording(true); setMoment(null); }
39
- }} />}
40
- </ResultRow>
41
- }</Bounded></ul>
42
- </section>;
27
+ return (
28
+ <Box aria-label="Chart tests" mah={compact ? '40vh' : undefined} p="md" component="section" mb="lg">
29
+ <Title order={3} mb="sm">
30
+ Tests
31
+ </Title>
32
+ <TestFilter slots={slots} />
33
+ <List spacing="xs" listStyleType="none">
34
+ <Bounded items={visible} size={40} more="More tests">
35
+ {(slot) => (
36
+ <ResultRow
37
+ key={slot.key}
38
+ label={slot.title}
39
+ slot={slot}
40
+ open={selectedKey === slot.key}
41
+ onToggle={() => {
42
+ setSelectedKey(selectedKey === slot.key ? null : slot.key);
43
+ setSelection({ check: slot.id });
44
+ setRecording(false);
45
+ setMoment(null);
46
+ }}
47
+ >
48
+ {recording ? (
49
+ <Suspense fallback={<p role="status">Loading the recording…</p>}>
50
+ <RecordingPage
51
+ slot={slot}
52
+ layerId={selection.layer}
53
+ label={slot.title}
54
+ back="Test report"
55
+ moment={moment}
56
+ onMoment={setMoment}
57
+ onClose={() => setRecording(false)}
58
+ />
59
+ </Suspense>
60
+ ) : (
61
+ <CheckDetail
62
+ slot={slot}
63
+ layerId={selection.layer}
64
+ reads="steps"
65
+ select={(patch) => {
66
+ setSelection({ check: patch.check ?? slot.id, layer: patch.layer ?? undefined });
67
+ if (patch.view === 'recording') {
68
+ setRecording(true);
69
+ setMoment(null);
70
+ }
71
+ }}
72
+ />
73
+ )}
74
+ </ResultRow>
75
+ )}
76
+ </Bounded>
77
+ </List>
78
+ </Box>
79
+ );
43
80
  }
@@ -1,35 +1,36 @@
1
+ import { Container, Text, Title } from '@mantine/core';
1
2
  import { Navigate, useSearch } from '@tanstack/react-router';
2
-
3
3
  import { useAtlas } from '../app/atlas-context';
4
4
  import type { AtlasView } from '../data/model';
5
- import styles from './views.module.css';
6
-
7
5
  function openingPlace(view: AtlasView) {
8
6
  const screens = [...view.states.values()].filter((state) => state.kind === 'screen' || state.kind === 'final');
9
7
  const withFailedEvent = screens.find((state) => state.outgoing.some((id) => view.transitions.get(id)?.status === 'failed'));
10
8
  return withFailedEvent ?? screens.find((state) => state.status === 'failed') ?? screens[0] ?? null;
11
9
  }
12
-
13
10
  export function ProductPage() {
14
11
  const { view } = useAtlas();
15
12
  const search = useSearch({ strict: false });
16
13
  const run = typeof search.run === 'string' ? search.run : undefined;
17
- if (!view) return <div className={styles.plateScroll} role="status">Loading the spec…</div>;
14
+ if (!view)
15
+ return (
16
+ <Container role="status" component="article" size="xl" w="100%" py="xl">
17
+ Loading the spec…
18
+ </Container>
19
+ );
18
20
  const place = openingPlace(view);
19
21
  if (!place) {
20
22
  return (
21
- <article className={styles.plateScroll}>
22
- <h1 className={styles.plateTitle}>{view.title}</h1>
23
- <p className={styles.lead}>This spec has no screens yet.</p>
24
- </article>
23
+ <Container component="article" size="xl" w="100%" py="xl">
24
+ <Title order={1} mb="sm">
25
+ {view.title}
26
+ </Title>
27
+ <Text component="div" c="dimmed">
28
+ This spec has no screens yet.
29
+ </Text>
30
+ </Container>
25
31
  );
26
32
  }
27
33
  return (
28
- <Navigate
29
- to="/chart/$chartId"
30
- params={{ chartId: place.chartId }}
31
- search={{ ...(run ? { run } : {}), screen: place.name }}
32
- replace
33
- />
34
+ <Navigate to="/chart/$chartId" params={{ chartId: place.chartId }} search={{ ...(run ? { run } : {}), screen: place.name }} replace />
34
35
  );
35
36
  }