@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
@@ -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
  }