@crvouga/atlas 0.10.2 → 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 (99) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +7 -0
  3. package/dist/server/src/settings-page.d.ts +4 -4
  4. package/dist/server/src/settings-page.js +17 -78
  5. package/docs/reference/server.md +4 -4
  6. package/docs/ui.md +142 -0
  7. package/docs/workspaces.md +130 -0
  8. package/llms-full.txt +278 -0
  9. package/llms.txt +2 -0
  10. package/package.json +17 -10
  11. package/packages/visualizer/src/admin/SettingsPage.tsx +456 -0
  12. package/packages/visualizer/src/admin/main.tsx +11 -0
  13. package/packages/visualizer/src/app/AppShell.tsx +62 -38
  14. package/packages/visualizer/src/app/gazetteer.tsx +90 -83
  15. package/packages/visualizer/src/app/router.tsx +33 -15
  16. package/packages/visualizer/src/components/ActivityIndicator.tsx +4 -5
  17. package/packages/visualizer/src/components/DataIssues.tsx +36 -54
  18. package/packages/visualizer/src/components/Header.tsx +197 -125
  19. package/packages/visualizer/src/components/Legend.tsx +13 -41
  20. package/packages/visualizer/src/components/ProjectPicker.tsx +61 -26
  21. package/packages/visualizer/src/components/Screen.tsx +80 -71
  22. package/packages/visualizer/src/components/SpecLogo.tsx +15 -0
  23. package/packages/visualizer/src/components/Status.tsx +20 -34
  24. package/packages/visualizer/src/components/labels.tsx +8 -3
  25. package/packages/visualizer/src/components/spec-logos/APACHE-2.0.txt +206 -0
  26. package/packages/visualizer/src/components/spec-logos/CAMUNDA-MIT.txt +21 -0
  27. package/packages/visualizer/src/components/spec-logos/FILE-ICONS-ISC.txt +13 -0
  28. package/packages/visualizer/src/components/spec-logos/README.md +23 -0
  29. package/packages/visualizer/src/components/spec-logos/SIMPLE-ICONS-CC0.txt +30 -0
  30. package/packages/visualizer/src/components/spec-logos/alloy.svg +1 -0
  31. package/packages/visualizer/src/components/spec-logos/dmn.svg +3 -0
  32. package/packages/visualizer/src/components/spec-logos/gherkin.svg +1 -0
  33. package/packages/visualizer/src/components/spec-logos/openslo.svg +63 -0
  34. package/packages/visualizer/src/components/spec-logos/quint.svg +1 -0
  35. package/packages/visualizer/src/evidence/artifacts.tsx +263 -110
  36. package/packages/visualizer/src/evidence/chart-source.tsx +18 -0
  37. package/packages/visualizer/src/evidence/check-detail.tsx +283 -139
  38. package/packages/visualizer/src/evidence/decision-table.tsx +35 -26
  39. package/packages/visualizer/src/evidence/document-page.tsx +51 -29
  40. package/packages/visualizer/src/evidence/feature-page.tsx +121 -53
  41. package/packages/visualizer/src/evidence/marks.tsx +112 -79
  42. package/packages/visualizer/src/evidence/model-page.tsx +77 -46
  43. package/packages/visualizer/src/evidence/model-trace.tsx +55 -35
  44. package/packages/visualizer/src/evidence/overview.tsx +137 -69
  45. package/packages/visualizer/src/evidence/recording-page.tsx +70 -31
  46. package/packages/visualizer/src/evidence/recording.tsx +135 -68
  47. package/packages/visualizer/src/evidence/snapshot.tsx +99 -43
  48. package/packages/visualizer/src/evidence/source-text.tsx +33 -0
  49. package/packages/visualizer/src/evidence/sources-panel.tsx +138 -144
  50. package/packages/visualizer/src/evidence/spec-header.tsx +52 -36
  51. package/packages/visualizer/src/evidence/spec-index.tsx +61 -32
  52. package/packages/visualizer/src/evidence/spec-page.tsx +56 -25
  53. package/packages/visualizer/src/evidence/test-filter.tsx +38 -14
  54. package/packages/visualizer/src/explorer/Explorer.tsx +688 -261
  55. package/packages/visualizer/src/explorer/ExplorerIndex.tsx +136 -68
  56. package/packages/visualizer/src/explorer/RunsView.tsx +307 -94
  57. package/packages/visualizer/src/explorer/TestDetail.tsx +393 -118
  58. package/packages/visualizer/src/explorer/parts.tsx +35 -13
  59. package/packages/visualizer/src/find/Finder.tsx +106 -90
  60. package/packages/visualizer/src/find/open.ts +17 -20
  61. package/packages/visualizer/src/live/test-progress.tsx +60 -22
  62. package/packages/visualizer/src/looks/Stage.tsx +223 -110
  63. package/packages/visualizer/src/main.tsx +9 -11
  64. package/packages/visualizer/src/map/ChartMap.tsx +125 -69
  65. package/packages/visualizer/src/map/EventEdge.tsx +59 -37
  66. package/packages/visualizer/src/map/map.module.css +12 -546
  67. package/packages/visualizer/src/map/nodes.tsx +124 -70
  68. package/packages/visualizer/src/reachable.test.ts +3 -2
  69. package/packages/visualizer/src/spec-kind.ts +2 -2
  70. package/packages/visualizer/src/ui/grammars/ALLOY-MIT.txt +21 -0
  71. package/packages/visualizer/src/ui/grammars/QUINT-APACHE-2.0.txt +206 -0
  72. package/packages/visualizer/src/ui/grammars/README.md +14 -0
  73. package/packages/visualizer/src/ui/grammars/alloy.json +71 -0
  74. package/packages/visualizer/src/ui/grammars/quint.json +152 -0
  75. package/packages/visualizer/src/ui/highlight.ts +50 -0
  76. package/packages/visualizer/src/ui/theme.tsx +109 -0
  77. package/packages/visualizer/src/views/ChartPage.tsx +46 -26
  78. package/packages/visualizer/src/views/ChartTests.tsx +59 -22
  79. package/packages/visualizer/src/views/ProductPage.tsx +16 -15
  80. package/packages/visualizer/src/views/SidePanel.tsx +241 -131
  81. package/packages/visualizer/src/app/gazetteer.module.css +0 -107
  82. package/packages/visualizer/src/components/ActivityIndicator.module.css +0 -4
  83. package/packages/visualizer/src/components/DataIssues.module.css +0 -136
  84. package/packages/visualizer/src/components/Header.module.css +0 -114
  85. package/packages/visualizer/src/components/Legend.module.css +0 -84
  86. package/packages/visualizer/src/components/Screen.module.css +0 -312
  87. package/packages/visualizer/src/components/Status.module.css +0 -116
  88. package/packages/visualizer/src/components/icons.tsx +0 -156
  89. package/packages/visualizer/src/evidence/artifacts.module.css +0 -213
  90. package/packages/visualizer/src/evidence/evidence.module.css +0 -907
  91. package/packages/visualizer/src/evidence/recording.module.css +0 -460
  92. package/packages/visualizer/src/evidence/sources.module.css +0 -168
  93. package/packages/visualizer/src/evidence/spec-snapshot.tsx +0 -26
  94. package/packages/visualizer/src/explorer/explorer.module.css +0 -1238
  95. package/packages/visualizer/src/find/Finder.module.css +0 -168
  96. package/packages/visualizer/src/looks/stage.module.css +0 -300
  97. package/packages/visualizer/src/styles/global.css +0 -105
  98. package/packages/visualizer/src/styles/tokens.css +0 -80
  99. package/packages/visualizer/src/views/views.module.css +0 -546
@@ -1,10 +1,8 @@
1
- import { Link, useLocation, useParams, useSearch } from '@tanstack/react-router';
2
-
3
- import { ScreenImage } from '../components/Screen';
1
+ import { Badge, Box, Divider, NavLink, Stack, Text, Title } from '@mantine/core';
2
+ import { Link, useParams, useSearch } from '@tanstack/react-router';
4
3
  import { STATUS_LABEL, type AtlasView, type StateView } from '../data/model';
4
+ import { statusColor } from '../ui/theme';
5
5
  import { useAtlas } from './atlas-context';
6
- import styles from './gazetteer.module.css';
7
-
8
6
  function openingScreen(view: AtlasView, steps: readonly { transitionIds: readonly string[] }[]) {
9
7
  for (const step of steps) {
10
8
  const transition = view.transitions.get(step.transitionIds[0] ?? '');
@@ -13,105 +11,114 @@ function openingScreen(view: AtlasView, steps: readonly { transitionIds: readonl
13
11
  }
14
12
  return undefined;
15
13
  }
16
-
17
14
  function screensOf(view: AtlasView, chartId: string) {
18
- const chart = view.charts.get(chartId);
19
- if (!chart) return [];
20
- return chart.states
15
+ return (view.charts.get(chartId)?.states ?? [])
21
16
  .map((key) => view.states.get(key))
22
- .filter((state): state is StateView => Boolean(state && (state.kind === 'screen' || state.kind === 'final')));
17
+ .filter((state): state is StateView => !!state && (state.kind === 'screen' || state.kind === 'final'));
23
18
  }
24
-
25
19
  export function Gazetteer() {
26
20
  const { view } = useAtlas();
27
21
  const params = useParams({ strict: false });
28
22
  const search = useSearch({ strict: false });
29
- const { pathname } = useLocation();
30
23
  const screen = typeof search.screen === 'string' ? search.screen : undefined;
31
24
  const journeyId = typeof search.journey === 'string' ? search.journey : undefined;
32
25
  const group = typeof search.group === 'string' ? search.group : undefined;
33
26
  const journeys = (view?.journeys ?? []).filter((journey) => journey.chartIds.length > 0);
34
27
  return (
35
- <nav className={styles.index} aria-label="Places">
36
- <h2>Places</h2>
37
- {!view && <p className={styles.empty}>Loading the spec…</p>}
38
- {view?.contexts.map((context) => (
39
- <section key={context.id}>
40
- <h3>{context.name}</h3>
41
- {context.chartIds.map((chartId) => {
42
- const chart = view.charts.get(chartId);
43
- if (!chart) return null;
44
- const screens = screensOf(view, chartId);
45
- return (
46
- <div key={chartId}>
47
- <Link className={styles.chart} to="/chart/$chartId" params={{ chartId }} search={(prev) => ({ run: prev.run, screen: undefined, journey: undefined, group: undefined })} activeOptions={{ includeSearch: true, explicitUndefined: true }} aria-current={params.chartId === chartId && !screen && !journeyId && !group ? 'page' : undefined}>
48
- {chart.name}
49
- </Link>
50
- <ul>
51
- {screens.map((state) => (
52
- <li key={state.key}>
28
+ <Box component="nav" aria-label="Places" p="md">
29
+ <Stack gap="lg">
30
+ <Title order={2}>Places</Title>
31
+ {!view && <Text c="dimmed">Loading the spec…</Text>}
32
+ {view?.contexts.map((context) => (
33
+ <Stack key={context.id} gap="xs">
34
+ <Text size="xs" fw={600} c="dimmed">
35
+ {context.name}
36
+ </Text>
37
+ {context.chartIds.map((chartId) => {
38
+ const chart = view.charts.get(chartId);
39
+ if (!chart) return null;
40
+ return (
41
+ <Box key={chartId}>
42
+ <NavLink
43
+ label={chart.name}
44
+ fw={600}
45
+ active={params.chartId === chartId && !screen && !journeyId && !group}
46
+ renderRoot={(props) => (
53
47
  <Link
54
- className={styles.row}
55
- data-status={state.status}
48
+ {...props}
56
49
  to="/chart/$chartId"
57
50
  params={{ chartId }}
58
- search={(prev) => ({ run: prev.run, screen: state.name, journey: prev.journey })}
59
- aria-current={params.chartId === chartId && screen === state.name ? 'page' : undefined}
60
- >
61
- <span className={styles.thumb} aria-hidden="true">
62
- <ScreenImage state={state} size="thumb" description={false} lines={false} caption={false} />
63
- </span>
64
- <span className={styles.name}>{state.name}</span>
65
- <span className={styles.status}>{STATUS_LABEL[state.status]}</span>
66
- </Link>
67
- </li>
51
+ search={(prev) => ({ run: prev.run, screen: undefined, journey: undefined, group: undefined })}
52
+ />
53
+ )}
54
+ />
55
+ {screensOf(view, chartId).map((state) => (
56
+ <NavLink
57
+ key={state.key}
58
+ label={state.name}
59
+ active={params.chartId === chartId && screen === state.name}
60
+ aria-current={params.chartId === chartId && screen === state.name ? 'page' : undefined}
61
+ rightSection={
62
+ <Badge color={statusColor(state.status)} size="xs">
63
+ {STATUS_LABEL[state.status]}
64
+ </Badge>
65
+ }
66
+ renderRoot={(props) => (
67
+ <Link
68
+ {...props}
69
+ to="/chart/$chartId"
70
+ params={{ chartId }}
71
+ search={(prev) => ({ run: prev.run, screen: state.name, journey: prev.journey })}
72
+ />
73
+ )}
74
+ />
68
75
  ))}
69
- </ul>
70
- </div>
71
- );
72
- })}
73
- </section>
74
- ))}
75
- {journeys.length > 0 && (
76
- <section>
77
- <h3>Journeys</h3>
78
- <ul>
76
+ </Box>
77
+ );
78
+ })}
79
+ </Stack>
80
+ ))}
81
+ {journeys.length > 0 && (
82
+ <Stack gap="xs">
83
+ <Divider />
84
+ <Title order={3}>Journeys</Title>
79
85
  {journeys.map((journey) => {
80
- const opening = view ? openingScreen(view, journey.steps) : undefined;
86
+ const chartId = journey.chartIds[0]!;
87
+ const start = view && openingScreen(view, journey.steps);
81
88
  return (
82
- <li key={journey.id}>
83
- <Link
84
- className={styles.row}
85
- data-status={journey.status}
86
- to="/chart/$chartId"
87
- params={{ chartId: journey.chartIds[0] ?? '' }}
88
- search={(prev) => ({ run: prev.run, journey: journey.id })}
89
- aria-current={journeyId === journey.id && pathname.startsWith('/chart') ? 'page' : undefined}
90
- >
91
- {opening && (
92
- <span className={styles.thumb} aria-hidden="true">
93
- <ScreenImage state={opening} size="thumb" description={false} lines={false} caption={false} />
94
- </span>
95
- )}
96
- <span className={styles.name}>{journey.name}</span>
97
- <span className={styles.status}>{STATUS_LABEL[journey.status]}</span>
98
- </Link>
99
- </li>
89
+ <NavLink
90
+ key={journey.id}
91
+ label={journey.name}
92
+ active={journeyId === journey.id}
93
+ rightSection={
94
+ <Badge size="xs" color={statusColor(journey.status)}>
95
+ {STATUS_LABEL[journey.status]}
96
+ </Badge>
97
+ }
98
+ renderRoot={(props) => (
99
+ <Link
100
+ {...props}
101
+ to="/chart/$chartId"
102
+ params={{ chartId }}
103
+ search={(prev) => ({ run: prev.run, journey: journey.id, screen: start?.name })}
104
+ />
105
+ )}
106
+ />
100
107
  );
101
108
  })}
102
- </ul>
103
- </section>
104
- )}
105
- {view && view.excluded.length > 0 && (
106
- <section>
107
- <h2>Not in the spec yet</h2>
108
- <ul>
109
+ </Stack>
110
+ )}
111
+ {!!view?.excluded.length && (
112
+ <Stack gap="xs">
113
+ <Title order={3}>Not in the spec yet</Title>
109
114
  {view.excluded.map((item) => (
110
- <li key={item} className={styles.excluded}>{item}</li>
115
+ <Text key={item} c="dimmed">
116
+ {item}
117
+ </Text>
111
118
  ))}
112
- </ul>
113
- </section>
114
- )}
115
- </nav>
119
+ </Stack>
120
+ )}
121
+ </Stack>
122
+ </Box>
116
123
  );
117
124
  }
@@ -1,13 +1,20 @@
1
- import { createBrowserHistory, createMemoryHistory, createRootRoute, createRoute, createRouter, lazyRouteComponent, type RouterHistory } from '@tanstack/react-router';
1
+ import { Alert, Anchor, Button, Container, Stack, Text, Title } from '@mantine/core';
2
+ import {
3
+ Link,
4
+ createBrowserHistory,
5
+ createMemoryHistory,
6
+ createRootRoute,
7
+ createRoute,
8
+ createRouter,
9
+ lazyRouteComponent,
10
+ type RouterHistory
11
+ } from '@tanstack/react-router';
2
12
  import { z } from 'zod/v4';
3
-
4
13
  import { SpecPage } from '../evidence/spec-page';
14
+ import { ExplorerSearchFields } from '../explorer/search';
5
15
  import { ProductPage } from '../views/ProductPage';
6
16
  import { AppShell } from './AppShell';
7
- import { ExplorerSearchFields } from '../explorer/search';
8
-
9
17
  const RootSearch = z.object({ run: z.string().min(1).optional().catch(undefined) });
10
-
11
18
  const ChartSearch = RootSearch.extend({
12
19
  screen: z.string().min(1).optional().catch(undefined),
13
20
  event: z.string().min(1).optional().catch(undefined),
@@ -15,12 +22,9 @@ const ChartSearch = RootSearch.extend({
15
22
  group: z.string().min(1).optional().catch(undefined),
16
23
  t: z.coerce.number().nonnegative().optional().catch(undefined)
17
24
  });
18
-
19
25
  export type ChartSearch = z.infer<typeof ChartSearch>;
20
-
21
26
  /** The project home: the test explorer's filters, grouping, order, open test and open run (see `explorer/search.ts`). */
22
27
  const HomeSearch = RootSearch.extend(ExplorerSearchFields);
23
-
24
28
  /** A spec document, the check opened in it, one layer's record of that check, and a recorded moment. */
25
29
  const SpecSearch = RootSearch.extend({
26
30
  spec: z.string().min(1).optional().catch(undefined),
@@ -29,14 +33,32 @@ const SpecSearch = RootSearch.extend({
29
33
  view: z.enum(['recording']).optional().catch(undefined),
30
34
  moment: z.coerce.number().int().positive().optional().catch(undefined)
31
35
  });
32
-
33
36
  export type SpecSearch = z.infer<typeof SpecSearch>;
34
-
35
37
  /** A fresh route tree per router, so a test can run its own viewer beside the page's. */
36
38
  function routeTree() {
37
39
  const rootRoute = createRootRoute({
38
40
  validateSearch: (search) => RootSearch.parse(search),
39
- component: AppShell
41
+ component: AppShell,
42
+ errorComponent: ({ error, reset }) => (
43
+ <Container py="xl">
44
+ <Alert color="red" title="The page couldn’t be loaded">
45
+ <Stack>
46
+ <Text>{error instanceof Error ? error.message : String(error)}</Text>
47
+ <Button onClick={reset}>Try again</Button>
48
+ <Anchor renderRoot={(props) => <Link {...props} to="/" search={{}} />}>Back to reports</Anchor>
49
+ </Stack>
50
+ </Alert>
51
+ </Container>
52
+ ),
53
+ notFoundComponent: () => (
54
+ <Container py="xl">
55
+ <Stack>
56
+ <Title order={1}>Page not found</Title>
57
+ <Text c="dimmed">This address is not part of the report viewer.</Text>
58
+ <Anchor renderRoot={(props) => <Link {...props} to="/" search={{}} />}>Back to reports</Anchor>
59
+ </Stack>
60
+ </Container>
61
+ )
40
62
  });
41
63
  const productRoute = createRoute({
42
64
  getParentRoute: () => rootRoute,
@@ -59,11 +81,9 @@ function routeTree() {
59
81
  });
60
82
  return rootRoute.addChildren([productRoute, chartRoute, specRoute]);
61
83
  }
62
-
63
84
  export function createAtlasRouter(history: RouterHistory) {
64
85
  return createRouter({ routeTree: routeTree(), history, defaultPreload: false, scrollRestoration: true });
65
86
  }
66
-
67
87
  /**
68
88
  * Addresses in the hash, so a built site deep-links without server rewrites. The page's own query (`?project=geviti`)
69
89
  * selects the project and is not part of the address: TanStack's hash history appends it to the hash, which turned
@@ -82,9 +102,7 @@ export function createProjectHashHistory(win: Window = window): RouterHistory {
82
102
  createHref: (href) => `${win.location.pathname}${win.location.search}#${href}`
83
103
  });
84
104
  }
85
-
86
105
  export const router = createAtlasRouter(createProjectHashHistory());
87
-
88
106
  declare module '@tanstack/react-router' {
89
107
  interface Register {
90
108
  router: typeof router;
@@ -1,6 +1,5 @@
1
- import styles from './ActivityIndicator.module.css';
2
-
3
- /** A quiet indication of active work; reduced motion keeps a static ring. */
4
- export function ActivityIndicator({ label = 'Working' }: { label?: string }) {
5
- return <span className={styles.activity} role="status" aria-label={label} title={label}><span className={styles.ring} aria-hidden="true" /></span>;
1
+ import { Loader } from '@mantine/core';
2
+ /** A named, reduced-motion-aware indicator for work in progress. */
3
+ export function ActivityIndicator({ label }: { label: string }) {
4
+ return <Loader size="xs" aria-label={label} role="status" />;
6
5
  }
@@ -1,70 +1,52 @@
1
1
  import type { DataIssue } from '@crvouga/atlas/schema';
2
- import * as Dialog from '@radix-ui/react-dialog';
2
+ import { Alert, Button, Code, Drawer, Stack, Text, Title } from '@mantine/core';
3
3
  import { useMemo } from 'react';
4
-
5
4
  import { useUiStore } from '../state/ui-store';
6
- import styles from './DataIssues.module.css';
7
- import { CloseIcon, NoteIcon, WarnIcon } from './icons';
8
-
9
5
  const SEVERITY_LABEL = { error: 'Problem', warning: 'Warning', info: 'Note' } as const;
10
-
11
- /**
12
- * A quiet count of what went wrong while reading the data. It never interrupts; the list opens
13
- * only when asked for.
14
- */
6
+ /** File-by-file diagnostics; unreadable data never appears as a passing result. */
15
7
  export function DataIssues({ issues }: { issues: DataIssue[] }) {
16
8
  const open = useUiStore((s) => s.issuesOpen);
17
9
  const setOpen = useUiStore((s) => s.setIssuesOpen);
18
10
  const problems = issues.filter((i) => i.severity !== 'info').length;
19
- const byFile = useMemo(() => {
20
- const groups = new Map<string, DataIssue[]>();
21
- for (const issue of issues) groups.set(issue.file, [...(groups.get(issue.file) ?? []), issue]);
22
- return [...groups.entries()].sort(([, a], [, b]) => severityRank(b) - severityRank(a));
11
+ const groups = useMemo(() => {
12
+ const byFile = new Map<string, DataIssue[]>();
13
+ for (const issue of issues) byFile.set(issue.file, [...(byFile.get(issue.file) ?? []), issue]);
14
+ return [...byFile].sort(([, a], [, b]) => severityRank(b) - severityRank(a));
23
15
  }, [issues]);
24
- if (issues.length === 0) return null;
25
- const label = problems ? `${problems} data ${problems === 1 ? 'problem' : 'problems'}` : `${issues.length} data ${issues.length === 1 ? 'note' : 'notes'}`;
16
+ if (!issues.length) return null;
17
+ const label = problems
18
+ ? `${problems} data ${problems === 1 ? 'problem' : 'problems'}`
19
+ : `${issues.length} data ${issues.length === 1 ? 'note' : 'notes'}`;
26
20
  return (
27
- <Dialog.Root open={open} onOpenChange={setOpen}>
28
- <Dialog.Trigger className={styles.trigger} data-has-problems={problems > 0}>
29
- {problems ? <WarnIcon size={13} /> : <NoteIcon size={13} />}
21
+ <>
22
+ <Button color={problems ? 'orange' : 'gray'} onClick={() => setOpen(true)}>
30
23
  {label}
31
- </Dialog.Trigger>
32
- <Dialog.Portal>
33
- <Dialog.Overlay className={styles.overlay} />
34
- <Dialog.Content className={styles.sheet}>
35
- <div className={styles.head}>
36
- <Dialog.Title className={styles.title}>What we couldn’t read</Dialog.Title>
37
- <Dialog.Close className={styles.close} aria-label="Close">
38
- <CloseIcon size={16} />
39
- </Dialog.Close>
40
- </div>
41
- <Dialog.Description className={styles.lead}>
42
- Everything else on the map is shown as usual. These are the parts of the spec and the runs that were skipped or guessed, file by file.
43
- </Dialog.Description>
44
- <div className={styles.list}>
45
- {byFile.map(([file, list]) => (
46
- <section key={file} className={styles.file}>
47
- <h3 className={styles.fileName}>{file || 'General'}</h3>
48
- <ul className={styles.items}>
49
- {list.map((issue, i) => (
50
- <li key={i} className={styles.item} data-severity={issue.severity}>
51
- <span className={styles.severity}>{SEVERITY_LABEL[issue.severity]}</span>
52
- <span>
53
- {issue.path && <span className={styles.path}>{issue.path}: </span>}
54
- {issue.message}
55
- </span>
56
- </li>
57
- ))}
58
- </ul>
59
- </section>
60
- ))}
61
- </div>
62
- </Dialog.Content>
63
- </Dialog.Portal>
64
- </Dialog.Root>
24
+ </Button>
25
+ <Drawer opened={open} onClose={() => setOpen(false)} title="What we couldn’t read">
26
+ <Stack>
27
+ <Text c="dimmed">
28
+ Everything else on the map is shown as usual. These are the parts of the spec and the runs that were skipped or guessed, file by
29
+ file.
30
+ </Text>
31
+ {groups.map(([file, list]) => (
32
+ <Stack key={file} gap="xs">
33
+ <Title order={3}>{file || 'General'}</Title>
34
+ {list.map((issue, i) => (
35
+ <Alert
36
+ key={i}
37
+ color={issue.severity === 'error' ? 'red' : issue.severity === 'warning' ? 'orange' : 'blue'}
38
+ title={SEVERITY_LABEL[issue.severity]}
39
+ >
40
+ {issue.path && <Code>{issue.path}</Code>} {issue.message}
41
+ </Alert>
42
+ ))}
43
+ </Stack>
44
+ ))}
45
+ </Stack>
46
+ </Drawer>
47
+ </>
65
48
  );
66
49
  }
67
-
68
50
  function severityRank(list: DataIssue[]) {
69
51
  return Math.max(...list.map((i) => (i.severity === 'error' ? 2 : i.severity === 'warning' ? 1 : 0)));
70
52
  }