@brett_lamy/docstream 1.22.0 → 1.23.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.
@@ -2,7 +2,8 @@
2
2
  plays the recording (rrweb loads lazily, on first render); this adds the chrome around it: a URL bar that
3
3
  follows the recording's navigations, play/pause, a scrubber with the recording's moments on a rail above
4
4
  it, a speed control, fullscreen, a drawn cursor whose clicks ripple, and DevTools: Console and Network buttons
5
- in the top bar with what had been logged and requested by the playhead, which open a DevTools panel over the page. Until the player is ready a
5
+ in the top bar with what had been logged and requested by the playhead, which open a DevTools panel over the page,
6
+ and inspections (inspections.ts) that open it on a request or message as playback reaches them. Until the player is ready a
6
7
  poster (a page-shaped skeleton by default) holds the frame's exact size, so nothing jumps.
7
8
 
8
9
  Plain React + plain CSS (`@layer docstream.replay` in styles.css, `[data-slot]` hooks). Keys (focus inside
@@ -18,6 +19,7 @@ import { Lock, Maximize, Minimize, Pause, Play, RotateCcw } from 'lucide-react'
18
19
  import { ReplayStage } from './ReplayStage'
19
20
  import { DevtoolsButtons, DevtoolsPanel, type ReplayDevtoolsTab } from './ReplayDevtools'
20
21
  import { getReplayDevtools, replayDevtoolsAt } from './devtools'
22
+ import { getReplayInspections, replayInspectionAt, resolveReplayInspections, type ReplayInspection } from './inspections'
21
23
  import { getReplayMarkers, getReplayMeta, getReplayPointerTrack, replayPointerAt } from './markers'
22
24
  import type { ReplayMarker, ReplayMarkerKind, ReplayPointerTrack } from './markers'
23
25
  import type { ReplayStageHandle } from './stage-types'
@@ -36,6 +38,8 @@ export interface ReplayPreviewHandle {
36
38
  seek(ms: number): void
37
39
  toggle(): void
38
40
  getCurrentTime(): number
41
+ /** Jump to an inspection (by its id) and open DevTools on what it found. */
42
+ inspect(id: string): void
39
43
  }
40
44
 
41
45
  /** Options shared by every `ReplayPreview` (an `events` player or a `source` embed). */
@@ -63,11 +67,17 @@ export interface ReplayPlayerOptions {
63
67
  /** Which recorded marker kinds to show (default all). */
64
68
  markerKinds?: ReplayMarkerKind[]
65
69
  /** Console and Network buttons (in the top bar, or beside the controls without the browser chrome) that open a
66
- * DevTools panel cut at the playhead. `true` (default) shows them when the recording has console messages or
67
- * requests; `false` hides them. */
70
+ * DevTools panel cut at the playhead, shown when the recording has console messages, requests or inspections.
71
+ * `disableDevtools` removes them, the panel and the inspections. */
72
+ disableDevtools?: boolean
73
+ /** @deprecated Use `disableDevtools`. `false` is the same as `disableDevtools`. */
68
74
  devtools?: boolean
69
75
  /** Open the DevTools panel on this tab at mount. */
70
76
  defaultDevtoolsTab?: ReplayDevtoolsTab
77
+ /** Moments where a request or console message was inspected. As playback reaches one, DevTools opens on its
78
+ * tab with that request or message selected and the label above it; playback doesn't stop. Added to the
79
+ * recording's own `inspect` custom events, and to the rail as checkpoints. */
80
+ inspections?: ReplayInspection[]
71
81
  /** Draw the recorded pointer and click ripples (default true). */
72
82
  cursor?: boolean
73
83
  /** `width` (default): the page scales to the frame's width and sets its height. `contain`: fill the frame's
@@ -98,7 +108,7 @@ const MARKER_NOUN: Record<ReplayMarkerKind, string> = {
98
108
  export function ReplayPlayer({
99
109
  events, url, title = 'Session replay', chrome = 'browser', trafficLights = true, controls = true,
100
110
  autoplay = false, loop = false, initialTime = 0, speeds = DEFAULT_PLAYER_SPEEDS, markers = true, markerKinds,
101
- cursor = true, devtools = true, defaultDevtoolsTab, fit = 'width', poster, lazy = true, playerRef, onTimeUpdate, onPlayingChange, onMarkerPress,
111
+ cursor = true, disableDevtools = false, devtools = true, defaultDevtoolsTab, inspections, fit = 'width', poster, lazy = true, playerRef, onTimeUpdate, onPlayingChange, onMarkerPress,
102
112
  className, style,
103
113
  }: ReplayPlayerOptions & { events: ReplayEvent[] }) {
104
114
  const rootRef = useRef<HTMLDivElement>(null)
@@ -107,14 +117,26 @@ export function ReplayPlayer({
107
117
  const meta = useMemo(() => getReplayMeta(events), [events])
108
118
  const track = useMemo(() => getReplayPointerTrack(events), [events])
109
119
  const recorded = useMemo(() => getReplayMarkers(events), [events])
110
- const shown = useMemo(() => {
111
- const list = Array.isArray(markers) ? markers : markers ? recorded : []
112
- return markerKinds ? list.filter((m) => markerKinds.includes(m.kind)) : list
113
- }, [markers, recorded, markerKinds])
114
120
 
121
+ const toolsOn = devtools && !disableDevtools
115
122
  const tools = useMemo(() => getReplayDevtools(events), [events])
116
- const showTools = devtools && (tools.console.length > 0 || tools.network.length > 0)
123
+ const resolved = useMemo(
124
+ () => (toolsOn ? resolveReplayInspections([...getReplayInspections(events), ...(inspections ?? [])], tools) : []),
125
+ [toolsOn, events, inspections, tools],
126
+ )
127
+ const showTools = toolsOn && (tools.console.length > 0 || tools.network.length > 0 || resolved.length > 0)
117
128
  const [panel, setPanel] = useState<ReplayDevtoolsTab | null>(defaultDevtoolsTab ?? null)
129
+ // Whether an inspection (rather than a press) opened the panel: it then leaves focus where it was.
130
+ const [autoOpened, setAutoOpened] = useState(false)
131
+
132
+ const shown = useMemo(() => {
133
+ // Inspections from the prop join the rail as checkpoints (the recording's own are already among `recorded`).
134
+ const own: ReplayMarker[] = toolsOn ? (inspections ?? []).map((inspection, i) => ({
135
+ id: `inspection-${i}`, kind: 'custom', time: Math.max(0, inspection.time), label: inspection.label ?? 'Inspected',
136
+ })) : []
137
+ const list = Array.isArray(markers) ? markers : markers ? [...recorded, ...own].sort((a, b) => a.time - b.time) : []
138
+ return markerKinds ? list.filter((m) => markerKinds.includes(m.kind)) : list
139
+ }, [markers, recorded, markerKinds, inspections, toolsOn])
118
140
 
119
141
  const [visible, setVisible] = useState(!lazy)
120
142
  const [ready, setReady] = useState(false)
@@ -151,12 +173,22 @@ export function ReplayPlayer({
151
173
  return () => clearTimeout(timer)
152
174
  }, [ready, reduced])
153
175
 
176
+ const resolvedRef = useRef(resolved)
177
+ resolvedRef.current = resolved
154
178
  const handle = useMemo<ReplayPreviewHandle>(() => ({
155
179
  play: (offset) => { setFinished(false); stage.current?.play(offset) },
156
180
  pause: (offset) => stage.current?.pause(offset),
157
181
  seek: (ms) => { setFinished(false); stage.current?.seek(ms) },
158
182
  toggle: () => { setFinished(false); stage.current?.toggle() },
159
183
  getCurrentTime: () => stage.current?.getCurrentTime() ?? 0,
184
+ inspect: (id) => {
185
+ const inspection = resolvedRef.current.find((x) => x.id === id)
186
+ if (!inspection) return
187
+ setFinished(false)
188
+ stage.current?.seek(inspection.time)
189
+ setPanel(inspection.tab)
190
+ setAutoOpened(false)
191
+ },
160
192
  }), [])
161
193
  useImperativeHandle(playerRef, () => handle, [handle])
162
194
 
@@ -209,7 +241,23 @@ export function ReplayPlayer({
209
241
  }, [url, meta.href, recorded, time])
210
242
 
211
243
  const toolsState = useMemo(() => replayDevtoolsAt(tools, time), [tools, time])
212
- const openTools = (tab: ReplayDevtoolsTab) => setPanel((p) => (p === tab ? null : tab))
244
+ const openTools = (tab: ReplayDevtoolsTab) => { setAutoOpened(false); setPanel((p) => (p === tab ? null : tab)) }
245
+
246
+ // Inspect mode: the inspection at the playhead opens DevTools on what it found. Only a change of inspection does
247
+ // (closing the panel keeps it closed until the next one); scrubbing back before the first closes what it opened.
248
+ const inspection = showTools ? replayInspectionAt(resolved, time) : undefined
249
+ const inspectionId = inspection?.id
250
+ const inspectionTab = inspection?.tab
251
+ const lastInspection = useRef<string | undefined>(undefined)
252
+ useEffect(() => {
253
+ if (inspectionId === lastInspection.current) return
254
+ const had = lastInspection.current
255
+ lastInspection.current = inspectionId
256
+ if (inspectionTab) {
257
+ setPanel(inspectionTab)
258
+ setAutoOpened(true)
259
+ } else if (had !== undefined && autoOpened) setPanel(null)
260
+ }, [inspectionId, inspectionTab, autoOpened])
213
261
  const toolButtons = showTools ? <DevtoolsButtons state={toolsState} open={panel} onOpen={openTools} /> : null
214
262
 
215
263
  const w = meta.width || 1280
@@ -269,11 +317,12 @@ export function ReplayPlayer({
269
317
  {showTools && panel ? (
270
318
  <DevtoolsPanel
271
319
  state={toolsState}
272
- at={time}
273
320
  tab={panel}
274
321
  onTab={setPanel}
275
322
  onClose={() => { setPanel(null); rootRef.current?.focus({ preventScroll: true }) }}
276
323
  onSeek={(ms) => handle.seek(ms)}
324
+ inspection={inspection}
325
+ autoFocus={!autoOpened}
277
326
  />
278
327
  ) : null}
279
328
 
@@ -17,6 +17,8 @@ export type ReplayConsoleLevel = 'log' | 'info' | 'warn' | 'error' | 'debug'
17
17
  export interface ReplayConsoleEntry {
18
18
  /** Stable id (`console-<n>`). */
19
19
  id: string
20
+ /** The recording's own id for the message (`payload.id` of a custom event), which an inspection can name. */
21
+ key?: string
20
22
  /** Milliseconds from the start of the recording. */
21
23
  time: number
22
24
  level: ReplayConsoleLevel
@@ -33,6 +35,9 @@ export interface ReplayConsoleEntry {
33
35
  export interface ReplayRequest {
34
36
  /** Stable id (`request-<n>`). */
35
37
  id: string
38
+ /** The recording's own id for the request (`id` / `requestId`, e.g. a Replay request id), which an
39
+ * inspection can name. */
40
+ key?: string
36
41
  /** When the request was sent, in ms from the start of the recording. */
37
42
  time: number
38
43
  /** How long it took, when known: the response arrived at `time + duration`. */
@@ -153,6 +158,7 @@ export function getReplayDevtools(events: readonly ReplayEvent[]): ReplayDevtool
153
158
  // request was sent that far before it (its own duration, for a lone request).
154
159
  const sent = Math.max(0, startTime !== undefined && batchEnd !== undefined ? at - (batchEnd - startTime) : at - (duration ?? 0))
155
160
  requests.push({
161
+ key: str(raw.requestId) ?? str(raw.id),
156
162
  time: sent,
157
163
  duration,
158
164
  method: (str(raw.method) ?? 'GET').toUpperCase(),
@@ -197,10 +203,10 @@ export function getReplayDevtools(events: readonly ReplayEvent[]): ReplayDevtool
197
203
  const detail = str(payload.message) ?? str(payload.detail)
198
204
  const message = detail ? `Uncaught ${label}: ${detail}` : `Uncaught ${label}`
199
205
  const trace = Array.isArray(payload.stack) ? payload.stack.map(String) : str(payload.stack)?.split('\n').map((l) => l.trim()).filter(Boolean)
200
- consoleEntries.push({ time, level: 'error', message, args: [message], trace: trace?.length ? trace : undefined, source: 'exception' })
206
+ consoleEntries.push({ key: str(payload.id), time, level: 'error', message, args: [message], trace: trace?.length ? trace : undefined, source: 'exception' })
201
207
  } else if (CONSOLE_TAGS.has(tag)) {
202
208
  const args = Array.isArray(payload.args) ? payload.args.map((a) => asText(a) ?? '') : [str(payload.message) ?? str(payload.label) ?? asText(payload.detail) ?? '']
203
- consoleEntries.push({ time, level: levelOf(payload.level ?? tag), message: args.join(' '), args, source: 'custom' })
209
+ consoleEntries.push({ key: str(payload.id), time, level: levelOf(payload.level ?? tag), message: args.join(' '), args, source: 'custom' })
204
210
  }
205
211
  }
206
212
  }
@@ -18,6 +18,8 @@ export type { ReplayMarker, ReplayMarkerKind, ReplayMeta, ReplayPointerPoint, Re
18
18
  export { getReplayDevtools, replayDevtoolsAt, requestName } from './devtools';
19
19
  export type { ReplayConsoleEntry, ReplayConsoleLevel, ReplayDevtools, ReplayDevtoolsState, ReplayRequest } from './devtools';
20
20
  export type { ReplayDevtoolsTab } from './ReplayDevtools';
21
+ export { getReplayInspections, replayInspectionAt, resolveReplayInspections } from './inspections';
22
+ export type { ReplayInspection, ReplayInspectionTab, ResolvedReplayInspection } from './inspections';
21
23
  export { ReplayEventType, ReplayIncrementalSource, ReplayMouseInteraction } from './types';
22
24
  export type { ReplayEvent } from './types';
23
25
  export { isReplayQaUrl, normalizeReplayEmbedUrl } from './url';
@@ -0,0 +1,105 @@
1
+ /* Inspections: moments in a recording where someone (a person, a debugging agent) looked at a request or a console
2
+ message — "at 0:35 we inspected GET /api/search?q=te". The player follows them by default: as the playhead
3
+ reaches one, DevTools opens on its tab with that request (or message) selected and the inspection's label above
4
+ it, and playback carries on.
5
+
6
+ They come from the `inspections` prop or from the recording itself, as custom events tagged `inspect`
7
+ (`record.addCustomEvent('inspect', { tab, request, message, label })`), so an .rrweb file can carry its own.
8
+ An inspection names its target by the recording's id for it (a request's `requestId` / `id`, a custom console
9
+ event's `id`) or, failing that, by URL (the request with that URL sent nearest the inspection) or by text (the
10
+ last message containing it). Pure readers, no rrweb import. */
11
+ import type { ReplayConsoleEntry, ReplayDevtools, ReplayRequest } from './devtools'
12
+ import { ReplayEventType, type ReplayEvent } from './types'
13
+
14
+ export type ReplayInspectionTab = 'network' | 'console'
15
+
16
+ export interface ReplayInspection {
17
+ /** Stable id; defaults to `inspect-<n>`. */
18
+ id?: string
19
+ /** Milliseconds from the start of the recording. */
20
+ time: number
21
+ /** The DevTools tab to open (default: `network` when `request` is set, else `console`). */
22
+ tab?: ReplayInspectionTab
23
+ /** The request to select: the recording's id for it (`key`), or its URL. */
24
+ request?: string
25
+ /** The console message to select: the recording's id for it (`key`), or text it contains. */
26
+ message?: string
27
+ /** What was found, shown above the panel while the inspection is current. */
28
+ label?: string
29
+ }
30
+
31
+ /** An inspection with its defaults filled in and its target resolved against the recording's DevTools. */
32
+ export interface ResolvedReplayInspection {
33
+ id: string
34
+ time: number
35
+ tab: ReplayInspectionTab
36
+ label?: string
37
+ /** The `ReplayRequest.id` to select, when the target was found. */
38
+ requestId?: string
39
+ /** The `ReplayConsoleEntry.id` to select, when the target was found. */
40
+ messageId?: string
41
+ }
42
+
43
+ export const INSPECT_TAG = 'inspect'
44
+
45
+ const str = (value: unknown): string | undefined => (typeof value === 'string' && value ? value : undefined)
46
+
47
+ /** The `inspect` custom events in a recording, as inspections (times in ms from its first event). */
48
+ export function getReplayInspections(events: readonly ReplayEvent[]): ReplayInspection[] {
49
+ const list = [...events].sort((a, b) => a.timestamp - b.timestamp)
50
+ const start = list[0]?.timestamp ?? 0
51
+ const out: ReplayInspection[] = []
52
+ for (const event of list) {
53
+ if (event.type !== ReplayEventType.Custom) continue
54
+ const data = (event.data ?? {}) as { tag?: unknown; payload?: Record<string, unknown> }
55
+ if (data.tag !== INSPECT_TAG) continue
56
+ const p = data.payload ?? {}
57
+ const tab = p.tab === 'network' || p.tab === 'console' ? p.tab : undefined
58
+ out.push({ id: str(p.id), time: event.timestamp - start, tab, request: str(p.request), message: str(p.message), label: str(p.label) })
59
+ }
60
+ return out
61
+ }
62
+
63
+ function findRequest(network: readonly ReplayRequest[], target: string, time: number): ReplayRequest | undefined {
64
+ const byKey = network.find((r) => r.key === target)
65
+ if (byKey) return byKey
66
+ // By URL (exact, else the address without its origin): the one sent nearest the inspection.
67
+ const matches = network.filter((r) => r.url === target || r.url.replace(/^\w+:\/\/[^/]+/, '') === target)
68
+ return matches.sort((a, b) => Math.abs(a.time - time) - Math.abs(b.time - time))[0]
69
+ }
70
+
71
+ function findMessage(messages: readonly ReplayConsoleEntry[], target: string, time: number): ReplayConsoleEntry | undefined {
72
+ const byKey = messages.find((m) => m.key === target)
73
+ if (byKey) return byKey
74
+ const matches = messages.filter((m) => m.message.includes(target))
75
+ // The last one logged by the inspection, else the first after it.
76
+ return matches.filter((m) => m.time <= time).at(-1) ?? matches[0]
77
+ }
78
+
79
+ /** Fills in each inspection's id and tab and finds its request or message; sorted by time. */
80
+ export function resolveReplayInspections(inspections: readonly ReplayInspection[], tools: ReplayDevtools): ResolvedReplayInspection[] {
81
+ return inspections
82
+ .map((inspection, i) => {
83
+ const request = inspection.request ? findRequest(tools.network, inspection.request, inspection.time) : undefined
84
+ const message = inspection.message ? findMessage(tools.console, inspection.message, inspection.time) : undefined
85
+ return {
86
+ id: inspection.id ?? `inspect-${i}`,
87
+ time: Math.max(0, inspection.time),
88
+ tab: inspection.tab ?? (inspection.request ? 'network' : 'console'),
89
+ label: inspection.label,
90
+ requestId: request?.id,
91
+ messageId: message?.id,
92
+ } satisfies ResolvedReplayInspection
93
+ })
94
+ .sort((a, b) => a.time - b.time)
95
+ }
96
+
97
+ /** The inspection current at `time`: the latest at or before it. */
98
+ export function replayInspectionAt(inspections: readonly ResolvedReplayInspection[], time: number): ResolvedReplayInspection | undefined {
99
+ let current: ResolvedReplayInspection | undefined
100
+ for (const inspection of inspections) {
101
+ if (inspection.time > time) break
102
+ current = inspection
103
+ }
104
+ return current
105
+ }
package/src/styles.css CHANGED
@@ -4440,6 +4440,14 @@
4440
4440
  /* ── Replay DevTools: the Console / Network buttons in the top bar and the panel over the page ──
4441
4441
  Chrome's DevTools, in the player's tokens. Failed requests and errors read --destructive, warnings --warning. */
4442
4442
  @layer docstream.replay {
4443
+ /* Text tones that keep 4.5:1 on the card in light and dark: the theme's muted, red and amber, pulled toward the
4444
+ foreground (a theme's own --muted-foreground or --destructive is often tuned for larger text). */
4445
+ :where([data-slot="replay-preview"]) {
4446
+ --replay-devtools-dim: color-mix(in srgb, var(--foreground, #18181b) 68%, var(--card, #ffffff));
4447
+ --replay-devtools-red: color-mix(in srgb, var(--destructive, #dc2626) 74%, var(--foreground, #18181b));
4448
+ --replay-devtools-amber: color-mix(in srgb, var(--warning, #b45309) 56%, var(--foreground, #18181b));
4449
+ }
4450
+
4443
4451
  :where([data-slot="replay-preview-actions"]) {
4444
4452
  display: flex;
4445
4453
  flex-shrink: 0;
@@ -4465,7 +4473,7 @@
4465
4473
  outline: none;
4466
4474
  cursor: pointer;
4467
4475
  appearance: none;
4468
- color: var(--muted-foreground, #71717a);
4476
+ color: var(--replay-devtools-dim);
4469
4477
  background: transparent;
4470
4478
  font: inherit;
4471
4479
  font-size: 11.5px;
@@ -4489,12 +4497,12 @@
4489
4497
  }
4490
4498
 
4491
4499
  :where([data-slot="replay-preview"] [data-part="failed"]) {
4492
- color: var(--destructive, #dc2626);
4500
+ color: var(--replay-devtools-red);
4493
4501
  font-weight: 600;
4494
4502
  }
4495
4503
 
4496
4504
  :where([data-slot="replay-preview"] [data-part="warn"]) {
4497
- color: var(--warning, #b45309);
4505
+ color: var(--replay-devtools-amber);
4498
4506
  font-weight: 600;
4499
4507
  }
4500
4508
 
@@ -4549,7 +4557,7 @@
4549
4557
  outline: none;
4550
4558
  cursor: pointer;
4551
4559
  appearance: none;
4552
- color: var(--muted-foreground, #71717a);
4560
+ color: var(--replay-devtools-dim);
4553
4561
  background: transparent;
4554
4562
  font: inherit;
4555
4563
  }
@@ -4563,19 +4571,47 @@
4563
4571
  box-shadow: inset 0 0 0 2px var(--ring, #6366f1);
4564
4572
  }
4565
4573
 
4566
- :where([data-slot="replay-devtools-tabs"] [data-part="at"]) {
4574
+ :where([data-slot="replay-devtools-tablist"]) {
4575
+ display: flex;
4576
+ align-items: stretch;
4577
+ }
4578
+
4579
+ :where([data-slot="replay-devtools-tablist"] > button) {
4567
4580
  display: inline-flex;
4568
4581
  align-items: center;
4569
- margin-inline-start: auto;
4570
- padding-inline: 8px;
4571
- color: var(--muted-foreground, #71717a);
4572
- font-variant-numeric: tabular-nums;
4582
+ gap: 5px;
4583
+ margin: 0;
4584
+ padding: 0 10px;
4585
+ border: 0;
4586
+ border-bottom: 2px solid transparent;
4587
+ outline: none;
4588
+ cursor: pointer;
4589
+ appearance: none;
4590
+ color: var(--replay-devtools-dim);
4591
+ background: transparent;
4592
+ font: inherit;
4573
4593
  }
4574
4594
 
4595
+ :where([data-slot="replay-devtools-tablist"] > button[aria-selected="true"]) {
4596
+ color: var(--foreground, #18181b);
4597
+ border-bottom-color: var(--primary, #2563eb);
4598
+ }
4599
+
4600
+ :where([data-slot="replay-devtools-tablist"] > button:focus-visible) {
4601
+ box-shadow: inset 0 0 0 2px var(--ring, #6366f1);
4602
+ }
4603
+
4604
+
4605
+
4606
+
4575
4607
  :where([data-slot="replay-devtools-tabs"] > [data-slot="replay-devtools-close"], [data-slot="replay-devtools-detail-tabs"] > [data-slot="replay-devtools-close"]) {
4576
4608
  padding: 0 8px;
4577
4609
  }
4578
4610
 
4611
+ :where([data-slot="replay-devtools-tabs"] > [data-slot="replay-devtools-close"]) {
4612
+ margin-inline-start: auto;
4613
+ }
4614
+
4579
4615
  :where([data-slot="replay-devtools-pane"]) {
4580
4616
  display: flex;
4581
4617
  flex: 1 1 0%;
@@ -4635,7 +4671,7 @@
4635
4671
  outline: none;
4636
4672
  cursor: pointer;
4637
4673
  appearance: none;
4638
- color: var(--muted-foreground, #71717a);
4674
+ color: var(--replay-devtools-dim);
4639
4675
  background: transparent;
4640
4676
  font: inherit;
4641
4677
  }
@@ -4669,19 +4705,19 @@
4669
4705
  }
4670
4706
 
4671
4707
  :where([data-slot="replay-devtools-message"][data-level="warn"]) {
4672
- color: color-mix(in srgb, var(--warning, #b45309) 80%, var(--foreground, #18181b));
4708
+ color: var(--replay-devtools-amber);
4673
4709
  background: color-mix(in srgb, var(--warning, #f59e0b) 10%, transparent);
4674
4710
  border-bottom-color: color-mix(in srgb, var(--warning, #f59e0b) 25%, transparent);
4675
4711
  }
4676
4712
 
4677
4713
  :where([data-slot="replay-devtools-message"][data-level="error"]) {
4678
- color: var(--destructive, #dc2626);
4714
+ color: var(--replay-devtools-red);
4679
4715
  background: color-mix(in srgb, var(--destructive, #dc2626) 8%, transparent);
4680
4716
  border-bottom-color: color-mix(in srgb, var(--destructive, #dc2626) 22%, transparent);
4681
4717
  }
4682
4718
 
4683
4719
  :where([data-slot="replay-devtools-message"][data-level="debug"]) {
4684
- color: var(--muted-foreground, #71717a);
4720
+ color: var(--replay-devtools-dim);
4685
4721
  }
4686
4722
 
4687
4723
  :where([data-slot="replay-devtools-message"] > details, [data-slot="replay-devtools-message"] > [data-part="line"]) {
@@ -4737,7 +4773,7 @@
4737
4773
  outline: none;
4738
4774
  cursor: pointer;
4739
4775
  appearance: none;
4740
- color: var(--muted-foreground, #71717a);
4776
+ color: var(--replay-devtools-dim);
4741
4777
  background: transparent;
4742
4778
  font: inherit;
4743
4779
  font-variant-numeric: tabular-nums;
@@ -4756,7 +4792,7 @@
4756
4792
  :where([data-slot="replay-devtools-empty"], [data-slot="replay-devtools-later"]) {
4757
4793
  margin: 0;
4758
4794
  padding: 10px 12px;
4759
- color: var(--muted-foreground, #71717a);
4795
+ color: var(--replay-devtools-dim);
4760
4796
  }
4761
4797
 
4762
4798
  :where([data-slot="replay-devtools-later"]) {
@@ -4796,7 +4832,7 @@
4796
4832
  height: 22px;
4797
4833
  padding: 0 8px;
4798
4834
  border-bottom: 1px solid var(--border, #e4e4e7);
4799
- color: var(--muted-foreground, #71717a);
4835
+ color: var(--replay-devtools-dim);
4800
4836
  background: var(--card, #ffffff);
4801
4837
  font-weight: 500;
4802
4838
  text-align: start;
@@ -4844,13 +4880,18 @@
4844
4880
  :where([data-slot="replay-devtools-table"] tbody tr:focus-visible) {
4845
4881
  box-shadow: inset 0 0 0 2px var(--ring, #6366f1);
4846
4882
  }
4883
+ /* The inspected request or message: a bar down its leading edge (the selection tint shows it's open). */
4884
+ :where([data-slot="replay-devtools-table"] tbody tr[data-inspected], [data-slot="replay-devtools-message"][data-inspected]) {
4885
+ box-shadow: inset 3px 0 0 var(--primary, #2563eb);
4886
+ }
4887
+
4847
4888
 
4848
4889
  :where([data-slot="replay-devtools-table"] tr[data-failed]) {
4849
- color: var(--destructive, #dc2626);
4890
+ color: var(--replay-devtools-red);
4850
4891
  }
4851
4892
 
4852
4893
  :where([data-slot="replay-devtools-table"] tr[data-pending]) {
4853
- color: var(--muted-foreground, #71717a);
4894
+ color: var(--replay-devtools-dim);
4854
4895
  }
4855
4896
 
4856
4897
  :where([data-slot="replay-devtools-table"] td[data-col="waterfall"]) {
@@ -4871,7 +4912,7 @@
4871
4912
  }
4872
4913
 
4873
4914
  :where([data-slot="replay-devtools-table"] tr[data-pending] [data-part="bar"]) {
4874
- background: color-mix(in srgb, var(--muted-foreground, #71717a) 50%, transparent);
4915
+ background: color-mix(in srgb, var(--replay-devtools-dim) 50%, transparent);
4875
4916
  }
4876
4917
 
4877
4918
  :where([data-slot="replay-devtools-status"]) {
@@ -4882,7 +4923,7 @@
4882
4923
  height: 22px;
4883
4924
  padding: 0 8px;
4884
4925
  border-top: 1px solid var(--border, #e4e4e7);
4885
- color: var(--muted-foreground, #71717a);
4926
+ color: var(--replay-devtools-dim);
4886
4927
  font-variant-numeric: tabular-nums;
4887
4928
  }
4888
4929
 
@@ -4939,7 +4980,7 @@
4939
4980
  :where([data-slot="replay-devtools-section"] dt) {
4940
4981
  flex-shrink: 0;
4941
4982
  min-width: 130px;
4942
- color: var(--muted-foreground, #71717a);
4983
+ color: var(--replay-devtools-dim);
4943
4984
  }
4944
4985
 
4945
4986
  :where([data-slot="replay-devtools-section"] dd) {