@crvouga/atlas 0.2.0 → 0.4.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.
- package/CHANGELOG.md +13 -1
- package/README.md +21 -2
- package/dist/reports/src/evidence-live.d.ts +6 -0
- package/dist/reports/src/evidence-live.js +35 -3
- package/dist/reports/src/evidence-types.d.ts +2 -0
- package/dist/reports/src/evidence-view.d.ts +15 -2
- package/dist/reports/src/evidence-view.js +31 -3
- package/dist/reports/src/export-evidence.js +2 -10
- package/dist/reports/src/frames.d.ts +3 -1
- package/dist/reports/src/frames.js +21 -0
- package/dist/reports/src/layer-axes.d.ts +18 -0
- package/dist/reports/src/layer-axes.js +51 -0
- package/dist/reports/src/layer-catalog.js +1 -1
- package/dist/reports/src/layer-merge.d.ts +1 -1
- package/dist/reports/src/layer-merge.js +9 -4
- package/dist/reports/src/layer-source.js +3 -0
- package/dist/reports/src/layer-types.d.ts +2 -0
- package/dist/reports/src/layers-route.js +2 -5
- package/dist/reports/src/protocol.d.ts +4 -0
- package/dist/reports/src/publish.d.ts +2 -0
- package/dist/reports/src/publish.js +6 -11
- package/dist/reports/src/recording-keys.js +4 -0
- package/dist/reports/src/report-parts.js +3 -0
- package/dist/reports/src/report-types.d.ts +19 -2
- package/dist/reports/src/source-entries.js +2 -12
- package/package.json +5 -4
- package/packages/reports/README.md +4 -2
- package/packages/reports/STORAGE.md +1 -1
- package/packages/visualizer/README.md +9 -6
- package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/scripts/make-fixtures.ts +2 -0
- package/packages/visualizer/scripts/publish-data.ts +1 -2
- package/packages/visualizer/src/app/AppShell.tsx +11 -12
- package/packages/visualizer/src/app/gazetteer.module.css +12 -2
- package/packages/visualizer/src/app/gazetteer.tsx +38 -26
- package/packages/visualizer/src/components/Header.module.css +20 -4
- package/packages/visualizer/src/components/Header.tsx +7 -4
- package/packages/visualizer/src/data/model/build.ts +17 -5
- package/packages/visualizer/src/data/model/types.ts +10 -1
- package/packages/visualizer/src/evidence/api.ts +6 -1
- package/packages/visualizer/src/evidence/check-detail.tsx +35 -3
- package/packages/visualizer/src/evidence/evidence.module.css +20 -0
- package/packages/visualizer/src/evidence/marks.tsx +2 -0
- package/packages/visualizer/src/evidence/overview.tsx +3 -3
- package/packages/visualizer/src/evidence/recording-page.tsx +19 -2
- package/packages/visualizer/src/evidence/recording.module.css +4 -8
- package/packages/visualizer/src/evidence/recording.tsx +74 -83
- package/packages/visualizer/src/evidence/snapshot.tsx +5 -4
- package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
- package/packages/visualizer/src/evidence/spec-index.tsx +3 -11
- package/packages/visualizer/src/find/Finder.module.css +168 -0
- package/packages/visualizer/src/find/Finder.tsx +303 -0
- package/packages/visualizer/src/find/corpus.test.ts +77 -0
- package/packages/visualizer/src/find/corpus.ts +368 -0
- package/packages/visualizer/src/find/open.ts +51 -0
- package/packages/visualizer/src/find/rank.test.ts +237 -0
- package/packages/visualizer/src/find/rank.ts +405 -0
- package/packages/visualizer/src/find/semantic.test.ts +62 -0
- package/packages/visualizer/src/find/semantic.ts +93 -0
- package/packages/visualizer/src/find/semantic.worker.ts +40 -0
- package/packages/visualizer/src/looks/Stage.tsx +322 -0
- package/packages/visualizer/src/looks/collect.test.ts +158 -0
- package/packages/visualizer/src/looks/collect.ts +245 -0
- package/packages/visualizer/src/looks/stage.module.css +300 -0
- package/packages/visualizer/src/reachable.test.ts +1 -1
- package/packages/visualizer/src/state/ui-store.ts +6 -2
- package/packages/visualizer/src/views/SidePanel.tsx +39 -49
- package/packages/visualizer/vite/example-reports.ts +475 -0
- package/packages/visualizer/vite.config.ts +9 -1
- package/packages/visualizer/src/app/search-query.tsx +0 -14
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { testFrames } from '@crvouga/atlas/reports/frames';
|
|
2
|
+
import type { RunReport } from '@crvouga/atlas/reports/report-types';
|
|
3
|
+
|
|
4
|
+
import type { AtlasView, ImageMedia, StateView, TransitionView } from '../data/model';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a run captured of the product, in the three shapes any runner can fill.
|
|
8
|
+
* A still, a video, and a timeline of moments are peers: a runner may record one of them or all three.
|
|
9
|
+
*/
|
|
10
|
+
export type PictureLook = {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly alt: string;
|
|
14
|
+
readonly src: string;
|
|
15
|
+
readonly thumb: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type FilmChapter = { readonly atMs: number; readonly label: string };
|
|
19
|
+
|
|
20
|
+
export type FilmLook = {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly src: string;
|
|
24
|
+
readonly poster: string | null;
|
|
25
|
+
readonly captions: string | null;
|
|
26
|
+
readonly durationMs: number | null;
|
|
27
|
+
readonly chapters: readonly FilmChapter[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type MomentLook = {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly label: string;
|
|
33
|
+
readonly caption: string | null;
|
|
34
|
+
readonly status: 'passed' | 'failed' | null;
|
|
35
|
+
readonly thumb: string | null;
|
|
36
|
+
/** A point in the still, in the still's own pixels, such as where someone tapped. */
|
|
37
|
+
readonly point?: { readonly x: number; readonly y: number } | null;
|
|
38
|
+
readonly pauseMs: number | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type Looks = {
|
|
42
|
+
readonly pictures: readonly PictureLook[];
|
|
43
|
+
readonly films: readonly FilmLook[];
|
|
44
|
+
readonly moments: readonly MomentLook[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const EMPTY_LOOKS: Looks = { pictures: [], films: [], moments: [] };
|
|
48
|
+
|
|
49
|
+
function ready(image: ImageMedia | null | undefined): image is ImageMedia {
|
|
50
|
+
return Boolean(image && image.state === 'available' && (image.full || image.thumb));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Every still in one capture: the client's own screen, then the other clients at that moment. */
|
|
54
|
+
export function picturesOf(image: ImageMedia | null | undefined, id: string, label: string, alt: string): PictureLook[] {
|
|
55
|
+
if (!ready(image)) return [];
|
|
56
|
+
const src = image.full ?? image.thumb!;
|
|
57
|
+
const shots: PictureLook[] = [
|
|
58
|
+
{
|
|
59
|
+
id,
|
|
60
|
+
label: image.client ? `${label} · ${image.client}` : label,
|
|
61
|
+
alt: image.client ? `${alt}, ${image.client}` : alt,
|
|
62
|
+
src,
|
|
63
|
+
thumb: image.thumb
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
image.also.forEach((extra, index) => {
|
|
67
|
+
const extraSrc = extra.full ?? extra.thumb;
|
|
68
|
+
if (!extraSrc) return;
|
|
69
|
+
const who = extra.client ?? `Screen ${index + 2}`;
|
|
70
|
+
shots.push({ id: `${id}:${who}`, label: `${label} · ${who}`, alt: `${alt}, ${who}`, src: extraSrc, thumb: extra.thumb });
|
|
71
|
+
});
|
|
72
|
+
return shots;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const MOMENT_KIND: Record<string, string> = {
|
|
76
|
+
tap: 'Tap',
|
|
77
|
+
type: 'Typing',
|
|
78
|
+
system: 'System',
|
|
79
|
+
screen: 'Screen',
|
|
80
|
+
toast: 'Toast',
|
|
81
|
+
note: 'Note',
|
|
82
|
+
time: 'Time',
|
|
83
|
+
notification: 'Notification'
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** The full still of a state. Tap points are recorded in that still's pixels, so the timeline uses it rather than a smaller preview. */
|
|
87
|
+
function stillOf(state: StateView | undefined): string | null {
|
|
88
|
+
if (!state) return null;
|
|
89
|
+
const shot = picturesOf(state.result?.screenshot, state.key, state.name, state.name)[0];
|
|
90
|
+
return shot?.src ?? shot?.thumb ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function unique(shots: readonly PictureLook[]): PictureLook[] {
|
|
94
|
+
const seen = new Set<string>();
|
|
95
|
+
return shots.filter((shot) => {
|
|
96
|
+
if (seen.has(shot.src)) return false;
|
|
97
|
+
seen.add(shot.src);
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Stills of one state: the screen, each client, and each path that captured it differently. */
|
|
103
|
+
export function stateLooks(state: StateView): Looks {
|
|
104
|
+
const pictures = unique([
|
|
105
|
+
...picturesOf(state.result?.screenshot, state.key, state.name, `Screenshot of ${state.name}`),
|
|
106
|
+
...(state.result?.screenshotsByPath ?? []).flatMap((extra) =>
|
|
107
|
+
picturesOf(extra.image, `${state.key}:${extra.path.id}`, extra.path.name, `Screenshot of ${state.name} along ${extra.path.name}`)
|
|
108
|
+
)
|
|
109
|
+
]);
|
|
110
|
+
return { pictures, films: [], moments: [] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A transition's video, the screens on either side, and the timeline of what the run did. */
|
|
114
|
+
export function eventLooks(view: AtlasView, transition: TransitionView): Looks {
|
|
115
|
+
const from = view.states.get(transition.source);
|
|
116
|
+
const to = transition.target ? view.states.get(transition.target) : undefined;
|
|
117
|
+
const side = (state: StateView | undefined, which: 'Before' | 'After') =>
|
|
118
|
+
state ? stateLooks(state).pictures.map((shot) => ({ ...shot, id: `${which.toLowerCase()}:${shot.id}`, label: `${which} · ${shot.label}`, alt: `${which}: ${shot.alt}` })) : [];
|
|
119
|
+
const pictures = unique([...side(from, 'Before'), ...side(to, 'After')]);
|
|
120
|
+
const entries = transition.result?.timeline ?? [];
|
|
121
|
+
const chapters = entries.map((entry) => ({ atMs: entry.atMs, label: entry.label }));
|
|
122
|
+
const clip = transition.result?.clip;
|
|
123
|
+
const films: FilmLook[] =
|
|
124
|
+
clip?.state === 'available' && clip.src
|
|
125
|
+
? [{ id: transition.id, label: transition.event, src: clip.src, poster: clip.poster, captions: clip.captions, durationMs: clip.durationMs, chapters }]
|
|
126
|
+
: [];
|
|
127
|
+
let shown = stillOf(from);
|
|
128
|
+
const moments: MomentLook[] = entries.map((entry, index) => {
|
|
129
|
+
if (entry.kind === 'screen') shown = stillOf(to) ?? shown;
|
|
130
|
+
const kind = MOMENT_KIND[entry.kind] ?? entry.kind;
|
|
131
|
+
return {
|
|
132
|
+
id: `${transition.id}:${index}`,
|
|
133
|
+
label: entry.label,
|
|
134
|
+
caption: entry.client ? `${kind} · ${entry.client}` : kind,
|
|
135
|
+
status: null,
|
|
136
|
+
thumb: shown,
|
|
137
|
+
point: entry.x !== undefined && entry.y !== undefined ? { x: entry.x, y: entry.y } : null,
|
|
138
|
+
pauseMs: null
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
return { pictures, films, moments };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The journey as a sequence of landing screens, plus a video for each step that recorded one. */
|
|
145
|
+
export function journeyLooks(view: AtlasView, journeyId: string): Looks {
|
|
146
|
+
const journey = view.journeys.find((item) => item.id === journeyId);
|
|
147
|
+
if (!journey) return EMPTY_LOOKS;
|
|
148
|
+
const pictures: PictureLook[] = [];
|
|
149
|
+
const films: FilmLook[] = [];
|
|
150
|
+
const moments: MomentLook[] = [];
|
|
151
|
+
const seen = new Set<string>();
|
|
152
|
+
journey.steps.forEach((step, index) => {
|
|
153
|
+
const transition = view.transitions.get(step.transitionIds[0] ?? '');
|
|
154
|
+
if (!transition) return;
|
|
155
|
+
const landing = transition.target ? view.states.get(transition.target) : undefined;
|
|
156
|
+
const origin = view.states.get(transition.source);
|
|
157
|
+
const landingShot = landing ? picturesOf(landing.result?.screenshot, landing.key, landing.name, `Screenshot of ${landing.name}`)[0] : undefined;
|
|
158
|
+
const originShot = origin ? picturesOf(origin.result?.screenshot, origin.key, origin.name, `Screenshot of ${origin.name}`)[0] : undefined;
|
|
159
|
+
// The screen they arrived on. When that screen was not captured, the screen they left is still what the step looked like.
|
|
160
|
+
const shot = landingShot ?? originShot;
|
|
161
|
+
if (shot && !seen.has(shot.src)) {
|
|
162
|
+
seen.add(shot.src);
|
|
163
|
+
pictures.push(shot);
|
|
164
|
+
}
|
|
165
|
+
const film = eventLooks(view, transition).films[0];
|
|
166
|
+
if (film) films.push({ ...film, id: `${transition.id}:film`, label: transition.event });
|
|
167
|
+
moments.push({
|
|
168
|
+
id: `${journeyId}:${index}`,
|
|
169
|
+
label: transition.event,
|
|
170
|
+
caption: (landingShot ? landing?.name : origin?.name) ?? null,
|
|
171
|
+
status: transition.status === 'failed' ? 'failed' : transition.status === 'passed' ? 'passed' : null,
|
|
172
|
+
thumb: shot?.src ?? shot?.thumb ?? null,
|
|
173
|
+
point: null,
|
|
174
|
+
pauseMs: film?.durationMs ?? null
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
return { pictures, films, moments };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function chaptersOf(report: RunReport): FilmChapter[] {
|
|
181
|
+
const chapters: FilmChapter[] = [];
|
|
182
|
+
const visit = (steps: RunReport['steps']) => {
|
|
183
|
+
for (const step of steps) {
|
|
184
|
+
if (step.title) chapters.push({ atMs: Math.max(0, step.startedAtMs - report.meta.startedAtMs), label: step.title });
|
|
185
|
+
visit(step.steps);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
visit(report.steps);
|
|
189
|
+
return chapters.some((chapter) => chapter.atMs > 0) ? chapters : [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Stills, videos, and the frame timeline published by any runner's report. */
|
|
193
|
+
export function reportLooks(report: RunReport, fileUrl: (file: string) => string): Looks {
|
|
194
|
+
const frames = testFrames({ report, blobs: {} });
|
|
195
|
+
const pictures: PictureLook[] = [];
|
|
196
|
+
const films: FilmLook[] = [];
|
|
197
|
+
const seen = new Set<string>();
|
|
198
|
+
const visit = (steps: RunReport['steps']) => {
|
|
199
|
+
for (const step of steps) {
|
|
200
|
+
for (const frame of step.frames) {
|
|
201
|
+
for (const blob of frame.snapshots) {
|
|
202
|
+
if (seen.has(blob.file)) continue;
|
|
203
|
+
if (blob.media !== 'png' && blob.media !== 'video') continue;
|
|
204
|
+
seen.add(blob.file);
|
|
205
|
+
const url = fileUrl(blob.file);
|
|
206
|
+
const who = blob.nodeId ?? blob.name;
|
|
207
|
+
if (blob.media === 'png') pictures.push({ id: blob.file, label: `${step.title} · ${who}`, alt: `Recorded ${who}`, src: url, thumb: url });
|
|
208
|
+
else films.push({ id: blob.file, label: who || step.title, src: url, poster: null, captions: null, durationMs: null, chapters: [] });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
visit(step.steps);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
visit(report.steps);
|
|
215
|
+
const chapters = chaptersOf(report);
|
|
216
|
+
for (const artifact of report.artifacts ?? []) {
|
|
217
|
+
if (seen.has(artifact.file)) continue;
|
|
218
|
+
if (artifact.media !== 'png' && artifact.media !== 'video') continue;
|
|
219
|
+
seen.add(artifact.file);
|
|
220
|
+
const url = fileUrl(artifact.file);
|
|
221
|
+
if (artifact.media === 'png') pictures.push({ id: artifact.file, label: artifact.name, alt: artifact.name, src: url, thumb: url });
|
|
222
|
+
else films.push({
|
|
223
|
+
id: artifact.file,
|
|
224
|
+
label: artifact.name,
|
|
225
|
+
src: url,
|
|
226
|
+
poster: artifact.poster ? fileUrl(artifact.poster) : null,
|
|
227
|
+
captions: artifact.captions ? fileUrl(artifact.captions) : null,
|
|
228
|
+
durationMs: null,
|
|
229
|
+
chapters
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const moments: MomentLook[] = frames.map((frame) => {
|
|
233
|
+
const png = frame.frame.snapshots.find((blob) => blob.media === 'png');
|
|
234
|
+
return {
|
|
235
|
+
id: `frame:${frame.index}`,
|
|
236
|
+
label: frame.step.title,
|
|
237
|
+
caption: frame.step.kind,
|
|
238
|
+
status: frame.step.status,
|
|
239
|
+
thumb: png ? fileUrl(png.file) : null,
|
|
240
|
+
point: null,
|
|
241
|
+
pauseMs: frame.frame.durationMs ?? null
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
return { pictures, films, moments };
|
|
245
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
.stage {
|
|
2
|
+
--look-h: min(58vh, 620px);
|
|
3
|
+
display: grid;
|
|
4
|
+
gap: 12px;
|
|
5
|
+
min-width: 0;
|
|
6
|
+
margin: 4px 0 8px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.modes {
|
|
10
|
+
display: flex;
|
|
11
|
+
gap: 4px;
|
|
12
|
+
border-bottom: 1px solid var(--line);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.modes button {
|
|
16
|
+
padding: 8px 12px;
|
|
17
|
+
border: 0;
|
|
18
|
+
background: none;
|
|
19
|
+
color: var(--ink-3);
|
|
20
|
+
font-size: 0.875rem;
|
|
21
|
+
font-weight: 500;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.modes button[aria-selected='true'] {
|
|
26
|
+
color: var(--ink);
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
box-shadow: inset 0 -2px 0 var(--place);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.viewport {
|
|
32
|
+
display: grid;
|
|
33
|
+
justify-items: center;
|
|
34
|
+
align-content: start;
|
|
35
|
+
gap: 10px;
|
|
36
|
+
min-height: 12rem;
|
|
37
|
+
padding: 16px;
|
|
38
|
+
border: 1px solid var(--line);
|
|
39
|
+
border-radius: 16px;
|
|
40
|
+
background:
|
|
41
|
+
linear-gradient(180deg, var(--bg-2) 0%, var(--bg) 100%);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.viewport:has(.moment) {
|
|
45
|
+
min-height: 0;
|
|
46
|
+
padding: 28px 20px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.shot,
|
|
50
|
+
.film {
|
|
51
|
+
display: block;
|
|
52
|
+
max-width: 100%;
|
|
53
|
+
max-height: var(--look-h);
|
|
54
|
+
object-fit: contain;
|
|
55
|
+
border-radius: 12px;
|
|
56
|
+
background: #fff;
|
|
57
|
+
box-shadow: var(--shadow-raised);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.film {
|
|
61
|
+
/* Phone clips stay phone-shaped. A wide clip uses the width of the plate. --ar is width / height, set once the file is known. */
|
|
62
|
+
--ar: 0.462;
|
|
63
|
+
width: min(100%, calc(var(--look-h) * var(--ar)));
|
|
64
|
+
height: auto;
|
|
65
|
+
background: #0e1216;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.shotFrame {
|
|
69
|
+
position: relative;
|
|
70
|
+
display: inline-block;
|
|
71
|
+
max-width: 100%;
|
|
72
|
+
line-height: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.mark {
|
|
76
|
+
position: absolute;
|
|
77
|
+
width: 28px;
|
|
78
|
+
height: 28px;
|
|
79
|
+
margin: -14px 0 0 -14px;
|
|
80
|
+
border: 2px solid #fff;
|
|
81
|
+
border-radius: 50%;
|
|
82
|
+
background: color-mix(in srgb, var(--place) 72%, transparent);
|
|
83
|
+
box-shadow:
|
|
84
|
+
0 0 0 7px color-mix(in srgb, var(--place) 28%, transparent),
|
|
85
|
+
var(--shadow-raised);
|
|
86
|
+
pointer-events: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.caption {
|
|
90
|
+
margin: 0;
|
|
91
|
+
color: var(--ink-2);
|
|
92
|
+
font-size: 0.8125rem;
|
|
93
|
+
text-align: center;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.caption[data-status='failed'] {
|
|
97
|
+
color: var(--failed-ink);
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.moment {
|
|
102
|
+
width: min(100%, 36rem);
|
|
103
|
+
margin: 0;
|
|
104
|
+
padding: 20px 22px;
|
|
105
|
+
border-radius: 12px;
|
|
106
|
+
background: var(--bg);
|
|
107
|
+
box-shadow: var(--shadow-raised);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.moment p {
|
|
111
|
+
margin: 0 0 6px;
|
|
112
|
+
color: var(--ink-3);
|
|
113
|
+
font-size: 0.75rem;
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
letter-spacing: 0.02em;
|
|
116
|
+
text-transform: uppercase;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.moment h3 {
|
|
120
|
+
margin: 0;
|
|
121
|
+
font-size: 1.25rem;
|
|
122
|
+
font-weight: 650;
|
|
123
|
+
letter-spacing: -0.01em;
|
|
124
|
+
line-height: 1.3;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.story {
|
|
128
|
+
display: grid;
|
|
129
|
+
justify-items: center;
|
|
130
|
+
gap: 10px;
|
|
131
|
+
width: 100%;
|
|
132
|
+
margin: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.storyCaption {
|
|
136
|
+
display: grid;
|
|
137
|
+
justify-items: center;
|
|
138
|
+
gap: 2px;
|
|
139
|
+
margin: 0;
|
|
140
|
+
text-align: center;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.storyCaption span {
|
|
144
|
+
color: var(--ink-3);
|
|
145
|
+
font-size: 0.75rem;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
letter-spacing: 0.04em;
|
|
148
|
+
text-transform: uppercase;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.storyCaption strong {
|
|
152
|
+
color: var(--ink);
|
|
153
|
+
font-size: 1rem;
|
|
154
|
+
font-weight: 650;
|
|
155
|
+
letter-spacing: -0.01em;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.storyCaption[data-status='failed'] strong {
|
|
159
|
+
color: var(--failed-ink);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.moment[data-status='failed'] h3 {
|
|
163
|
+
color: var(--failed-ink);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.body {
|
|
167
|
+
width: 100%;
|
|
168
|
+
min-width: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.transport {
|
|
172
|
+
display: flex;
|
|
173
|
+
flex-wrap: wrap;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 8px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.control {
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
justify-content: center;
|
|
182
|
+
height: 2rem;
|
|
183
|
+
padding: 0 12px;
|
|
184
|
+
border: 1px solid var(--line-2);
|
|
185
|
+
border-radius: var(--pill);
|
|
186
|
+
background: var(--bg);
|
|
187
|
+
color: var(--ink);
|
|
188
|
+
font-size: 0.8125rem;
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.stepper {
|
|
194
|
+
width: 2rem;
|
|
195
|
+
padding: 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.back {
|
|
199
|
+
transform: rotate(180deg);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.control:hover:enabled {
|
|
203
|
+
background: var(--bg-2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.control:disabled {
|
|
207
|
+
opacity: 0.45;
|
|
208
|
+
cursor: default;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.scrub {
|
|
212
|
+
flex: 1;
|
|
213
|
+
min-width: 8rem;
|
|
214
|
+
accent-color: var(--place);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.position {
|
|
218
|
+
color: var(--ink-3);
|
|
219
|
+
font-size: 0.8125rem;
|
|
220
|
+
white-space: nowrap;
|
|
221
|
+
font-variant-numeric: tabular-nums;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.strip,
|
|
225
|
+
.chapters {
|
|
226
|
+
display: flex;
|
|
227
|
+
gap: 8px;
|
|
228
|
+
margin: 0;
|
|
229
|
+
padding: 2px;
|
|
230
|
+
overflow-x: auto;
|
|
231
|
+
list-style: none;
|
|
232
|
+
scroll-snap-type: x mandatory;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.strip button,
|
|
236
|
+
.chapters button {
|
|
237
|
+
flex: none;
|
|
238
|
+
padding: 0;
|
|
239
|
+
border: 2px solid transparent;
|
|
240
|
+
border-radius: 10px;
|
|
241
|
+
background: var(--bg);
|
|
242
|
+
cursor: pointer;
|
|
243
|
+
scroll-snap-align: start;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.strip button {
|
|
247
|
+
width: 76px;
|
|
248
|
+
overflow: hidden;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.strip button[aria-current='true'],
|
|
252
|
+
.chapters button[aria-current='true'] {
|
|
253
|
+
border-color: var(--place);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.strip img {
|
|
257
|
+
display: block;
|
|
258
|
+
width: 100%;
|
|
259
|
+
height: 96px;
|
|
260
|
+
object-fit: contain;
|
|
261
|
+
background: var(--bg-2);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.chip {
|
|
265
|
+
display: block;
|
|
266
|
+
height: 64px;
|
|
267
|
+
padding: 8px;
|
|
268
|
+
overflow: hidden;
|
|
269
|
+
color: var(--ink-2);
|
|
270
|
+
font-size: 0.6875rem;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
line-height: 1.3;
|
|
273
|
+
text-align: left;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.chapters button {
|
|
277
|
+
max-width: 16rem;
|
|
278
|
+
padding: 6px 10px;
|
|
279
|
+
border-width: 1px;
|
|
280
|
+
border-color: var(--line-2);
|
|
281
|
+
border-radius: var(--pill);
|
|
282
|
+
color: var(--ink-2);
|
|
283
|
+
font-size: 0.75rem;
|
|
284
|
+
text-align: left;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.chapters button:hover {
|
|
288
|
+
background: var(--bg-2);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.notice,
|
|
292
|
+
.quiet {
|
|
293
|
+
margin: 0;
|
|
294
|
+
color: var(--ink-3);
|
|
295
|
+
font-size: 0.8125rem;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.empty {
|
|
299
|
+
width: min(100%, 20rem);
|
|
300
|
+
}
|
|
@@ -5,7 +5,7 @@ import { expect, it } from 'bun:test';
|
|
|
5
5
|
|
|
6
6
|
const SRC = path.dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const ENTRY = path.join(SRC, 'main.tsx');
|
|
8
|
-
const SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*|@import\s+(?:url\(\s*)?|\burl\(\s*)['"]([^'"]+)['"]/g;
|
|
8
|
+
const SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*|@import\s+(?:url\(\s*)?|\burl\(\s*|new URL\(\s*)['"]([^'"]+)['"]/g;
|
|
9
9
|
const SUFFIXES = ['', '.ts', '.tsx', '.css', '/index.ts', '/index.tsx'];
|
|
10
10
|
|
|
11
11
|
function filesUnder(dir: string): string[] {
|
|
@@ -11,7 +11,7 @@ type UiState = {
|
|
|
11
11
|
issuesOpen: boolean;
|
|
12
12
|
sourcesOpen: boolean;
|
|
13
13
|
resultFilter: ResultFilter;
|
|
14
|
-
/** Backends, branches and layers left out of the combined report. */
|
|
14
|
+
/** Backends, branches, versions, environments and layers left out of the combined report. */
|
|
15
15
|
excluded: SourceSelection;
|
|
16
16
|
saveViewport: (chartId: string, viewport: Viewport) => void;
|
|
17
17
|
setIssuesOpen: (open: boolean) => void;
|
|
@@ -38,7 +38,11 @@ export const useUiStore = create<UiState>()(
|
|
|
38
38
|
{
|
|
39
39
|
name: 'atlas-ui',
|
|
40
40
|
storage: createJSONStorage(() => sessionStorage),
|
|
41
|
-
partialize: (s) => ({ viewports: s.viewports, excluded: s.excluded, resultFilter: s.resultFilter })
|
|
41
|
+
partialize: (s) => ({ viewports: s.viewports, excluded: s.excluded, resultFilter: s.resultFilter }),
|
|
42
|
+
merge: (persisted, current) => {
|
|
43
|
+
const saved = (persisted ?? {}) as Partial<UiState>;
|
|
44
|
+
return { ...current, ...saved, excluded: { ...ALL_SOURCES, ...saved.excluded } };
|
|
45
|
+
}
|
|
42
46
|
}
|
|
43
47
|
)
|
|
44
48
|
);
|