@lvce-editor/renderer-process 30.60.0 → 30.60.2

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.
@@ -1,40 +1,3 @@
1
- const createActivityChart = document => {
2
- const namespace = 'http://www.w3.org/2000/svg';
3
- const element = document.createElementNS(namespace, 'svg');
4
- element.classList.add('SessionReplayActivity');
5
- element.setAttribute('viewBox', '0 0 240 48');
6
- element.setAttribute('preserveAspectRatio', 'none');
7
- element.setAttribute('role', 'img');
8
- element.setAttribute('aria-label', 'Session replay activity');
9
- const title = document.createElementNS(namespace, 'title');
10
- title.textContent = 'Recorded activity over time. Click to seek, or use the position slider with the keyboard.';
11
- const path = document.createElementNS(namespace, 'path');
12
- const cursor = document.createElementNS(namespace, 'line');
13
- cursor.classList.add('SessionReplayActivityCursor');
14
- cursor.setAttribute('y1', '0');
15
- cursor.setAttribute('y2', '48');
16
- cursor.setAttribute('vector-effect', 'non-scaling-stroke');
17
- element.append(title, path, cursor);
18
- return {
19
- element,
20
- setActivity(counts) {
21
- const maximum = Math.max(1, ...counts);
22
- const width = 240 / counts.length;
23
- const segments = counts.map((count, index) => {
24
- if (!count) return '';
25
- const height = count / maximum * 44;
26
- return `M${index * width} 48v-${height}h${width}v${height}z`;
27
- });
28
- path.setAttribute('d', segments.join(' '));
29
- },
30
- setPosition(fraction) {
31
- const x = String(fraction * 240);
32
- cursor.setAttribute('x1', x);
33
- cursor.setAttribute('x2', x);
34
- }
35
- };
36
- };
37
-
38
1
  // Walk the structured-clone graph, including collections and shared buffer views.
39
2
  const getTransferrables = value => {
40
3
  const seen = [];
@@ -113,176 +76,6 @@ const createClient = url => {
113
76
  };
114
77
  };
115
78
 
116
- const playerStyles = `
117
- .SessionReplay > .SessionReplayControls {
118
- box-sizing: border-box;
119
- display: grid;
120
- grid-template-columns: auto minmax(0, 1fr) auto;
121
- flex: none;
122
- align-items: center;
123
- gap: 0 18px;
124
- margin: 12px 16px 16px;
125
- padding: 10px 16px 10px 10px;
126
- border: 1px solid #ffffff1f;
127
- border-radius: 12px;
128
- background: #181a1f;
129
- color: #f4f4f5;
130
- box-shadow: 0 4px 20px #00000040;
131
- font: 13px/1.4 system-ui, sans-serif;
132
- color-scheme: dark;
133
- }
134
- .SessionReplayControls > .SessionReplayPlay {
135
- grid-column: 1;
136
- grid-row: 2;
137
- appearance: none;
138
- display: grid;
139
- place-items: center;
140
- flex: none;
141
- width: 52px;
142
- height: 44px;
143
- padding: 0;
144
- border: 0;
145
- border-radius: 8px;
146
- background: #00adef;
147
- color: #07141c;
148
- cursor: pointer;
149
- }
150
- .SessionReplayControls > .SessionReplayPlay:hover:not(:disabled) {
151
- background: #43caff;
152
- }
153
- .SessionReplayControls > .SessionReplayPlay:active:not(:disabled) {
154
- background: #0095ce;
155
- }
156
- .SessionReplayPlay > svg {
157
- display: block;
158
- width: 22px;
159
- height: 22px;
160
- fill: currentColor;
161
- pointer-events: none;
162
- }
163
- .SessionReplayControls > .SessionReplayPosition {
164
- grid-column: 2;
165
- grid-row: 2;
166
- appearance: none;
167
- box-sizing: border-box;
168
- flex: 1;
169
- min-width: 0;
170
- width: 100%;
171
- height: 28px;
172
- margin: 0;
173
- padding: 0;
174
- border: 0;
175
- border-radius: 3px;
176
- background: transparent;
177
- cursor: pointer;
178
- --replay-progress: 0%;
179
- }
180
- .SessionReplayPosition::-webkit-slider-runnable-track {
181
- height: 5px;
182
- border-radius: 3px;
183
- background: linear-gradient(to right, #00adef var(--replay-progress), #454950 var(--replay-progress));
184
- }
185
- .SessionReplayPosition::-moz-range-track {
186
- height: 5px;
187
- border-radius: 3px;
188
- background: #454950;
189
- }
190
- .SessionReplayPosition::-moz-range-progress {
191
- height: 5px;
192
- border-radius: 3px;
193
- background: #00adef;
194
- }
195
- .SessionReplayPosition::-webkit-slider-thumb {
196
- appearance: none;
197
- width: 13px;
198
- height: 13px;
199
- margin-top: -4px;
200
- border: 0;
201
- border-radius: 50%;
202
- background: #ffffff;
203
- box-shadow: 0 1px 5px #00000066;
204
- }
205
- .SessionReplayPosition::-moz-range-thumb {
206
- width: 13px;
207
- height: 13px;
208
- border: 0;
209
- border-radius: 50%;
210
- background: #ffffff;
211
- box-shadow: 0 1px 5px #00000066;
212
- }
213
- .SessionReplayControls > .SessionReplayActivity {
214
- grid-column: 2;
215
- grid-row: 1;
216
- display: block;
217
- width: calc(100% - 13px);
218
- min-width: 0;
219
- height: 48px;
220
- margin: 0 6.5px;
221
- border-bottom: 1px solid #454950;
222
- fill: #00adef80;
223
- cursor: pointer;
224
- }
225
- .SessionReplayControls > .SessionReplayActivity:hover {
226
- fill: #00adefb3;
227
- }
228
- .SessionReplayActivityCursor {
229
- stroke: #ffffff;
230
- stroke-width: 1;
231
- pointer-events: none;
232
- }
233
- .SessionReplayControls > :focus-visible {
234
- outline: 2px solid #ffffff;
235
- outline-offset: 4px;
236
- }
237
- .SessionReplayControls > :disabled {
238
- opacity: 0.45;
239
- cursor: default;
240
- }
241
- .SessionReplayControls > .SessionReplayTime {
242
- grid-column: 3;
243
- grid-row: 2;
244
- flex: none;
245
- font: inherit;
246
- font-variant-numeric: tabular-nums;
247
- white-space: nowrap;
248
- }
249
- .SessionReplayControls > .SessionReplayTime[role='alert'] {
250
- flex: 1;
251
- min-width: 0;
252
- color: #ffb4ab;
253
- white-space: normal;
254
- overflow-wrap: anywhere;
255
- }
256
- @media (max-width: 480px) {
257
- .SessionReplay > .SessionReplayControls {
258
- column-gap: 12px;
259
- margin: 8px;
260
- padding: 8px;
261
- font-size: 12px;
262
- }
263
- .SessionReplayControls > .SessionReplayPlay {
264
- width: 44px;
265
- }
266
- }
267
- @media (forced-colors: active) {
268
- .SessionReplayControls > .SessionReplayActivity {
269
- fill: Highlight;
270
- border-color: CanvasText;
271
- }
272
- .SessionReplayActivityCursor {
273
- stroke: CanvasText;
274
- }
275
- .SessionReplayControls > .SessionReplayPlay {
276
- border: 1px solid ButtonText;
277
- background: ButtonFace;
278
- color: ButtonText;
279
- }
280
- .SessionReplayControls > .SessionReplayPosition {
281
- appearance: auto;
282
- }
283
- }
284
- `;
285
-
286
79
  var __create = Object.create;
287
80
  var __defProp = Object.defineProperty;
288
81
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -10986,7 +10779,7 @@ var walker_default = {
10986
10779
  };
10987
10780
 
10988
10781
  // ../../node_modules/css-tree/lib/syntax/index.js
10989
- var syntax_default = create_default({
10782
+ create_default({
10990
10783
  ...lexer_default,
10991
10784
  ...parser_default,
10992
10785
  ...walker_default
@@ -11058,122 +10851,7 @@ function encode3(str) {
11058
10851
  return encoded;
11059
10852
  }
11060
10853
 
11061
- // ../../node_modules/css-tree/lib/index.js
11062
- var {
11063
- parse: parse51,
11064
- generate: generate51,
11065
- walk: walk2} = syntax_default;
11066
-
11067
- // ../session-replay-worker/src/parts/AssetUrls/AssetUrls.ts
11068
- var resolveAssetUrl$1 = (value, assetBaseUrl) => {
11069
- if (!assetBaseUrl) return void 0;
11070
- value = value.replace("/extensions/builtin.vscode-icons/icons/", "/file-icons/");
11071
- const match = /\/(icons|fonts|file-icons)\/([\w./-]+)(?:[?#].*)?$/.exec(value);
11072
- if (!match || match[2].split("/").some(part => !part || part === "." || part === "..")) return void 0;
11073
- if (!/\.(svg|png|jpg|jpeg|gif|webp|woff2?|ttf|otf)$/.test(match[2])) return void 0;
11074
- return `${assetBaseUrl}${match[1]}/${match[2]}`;
11075
- };
11076
-
11077
- // ../session-replay-worker/src/parts/ReplayCss/ReplayCss.ts
11078
- var allowedRules = ["media", "supports", "container", "layer", "font-face", "keyframes", "-webkit-keyframes", "property"];
11079
- var blockedFunctions = ["url", "image-set", "-webkit-image-set", "image", "src", "attr", "paint"];
11080
- var replayCss = (css, assetBaseUrl, inline = false) => {
11081
- try {
11082
- const ast = parse51(css, {
11083
- context: inline ? "declarationList" : "stylesheet",
11084
- parseCustomProperty: true
11085
- });
11086
- walk2(ast, {
11087
- enter(node, item, list) {
11088
- if (node.type === "Atrule" && !allowedRules.includes(ident_exports.decode(node.name).toLowerCase())) {
11089
- list.remove(item);
11090
- return this.skip;
11091
- }
11092
- if (node.type === "Raw") {
11093
- list.remove(item);
11094
- return this.skip;
11095
- }
11096
- if (node.type !== "Declaration") return void 0;
11097
- let unsafe = false;
11098
- walk2(node.value, value => {
11099
- if (value.type === "Raw" || value.type === "Function" && blockedFunctions.includes(ident_exports.decode(value.name).toLowerCase())) unsafe = true;
11100
- if (value.type === "Url") {
11101
- const resolved = resolveAssetUrl$1(value.value, assetBaseUrl);
11102
- if (resolved) value.value = resolved;else if (!/^data:(?:image\/(?:png|jpeg|gif|webp)|font\/[\w-]+);base64,[a-z\d+/=\s]+$/i.test(value.value) && !/^#[\w-]+$/.test(value.value)) unsafe = true;
11103
- }
11104
- });
11105
- if (unsafe) list.remove(item);
11106
- return this.skip;
11107
- }
11108
- });
11109
- return generate51(ast);
11110
- } catch {
11111
- return "";
11112
- }
11113
- };
11114
-
11115
- // Only known editor asset paths can be remapped into the host's trusted asset directory.
11116
- const resolveAssetUrl = (value, assetBaseUrl) => {
11117
- if (!assetBaseUrl) return undefined;
11118
- value = value.replace('/extensions/builtin.vscode-icons/icons/', '/file-icons/');
11119
- const match = /\/(icons|fonts|file-icons)\/([\w./-]+)(?:[?#].*)?$/.exec(value);
11120
- if (!match || match[2].split('/').some(part => !part || part === '.' || part === '..')) return undefined;
11121
- if (!/\.(svg|png|jpg|jpeg|gif|webp|woff2?|ttf|otf)$/.test(match[2])) return undefined;
11122
- return `${assetBaseUrl}${match[1]}/${match[2]}`;
11123
- };
11124
-
11125
- const tags = 'body div span p pre code main section article header footer nav aside h1 h2 h3 h4 h5 h6 ul ol li table thead tbody tr td th button input textarea select option label form fieldset legend a img br hr strong em b i u s small details summary svg path rect circle ellipse line polyline polygon g defs clipPath text tspan'.split(' ');
11126
- const attributes = /^(class|style|id|title|role|type|checked|disabled|selected|placeholder|width|height|viewBox|d|fill|stroke|cx|cy|r|x|y|x1|x2|y1|y2|points|transform|xmlns|data-[\w-]+|aria-[\w-]+)$/i;
11127
10854
  const svgNamespace = 'http://www.w3.org/2000/svg';
11128
- const attributeValue = (name, value, assetBaseUrl) => {
11129
- if (name === 'style') return replayCss(value, assetBaseUrl, true);
11130
- if (name !== 'fill' && name !== 'stroke') return value;
11131
- const css = replayCss(`${name}:${value}`, assetBaseUrl, true);
11132
- return css ? css.slice(css.indexOf(':') + 1) : '';
11133
- };
11134
- const imageSource = (attrs, assetBaseUrl) => {
11135
- const value = attrs.src;
11136
- if (typeof value === 'string') {
11137
- if (/^data:image\/(png|jpeg|gif|webp);base64,/.test(value)) return value;
11138
- const src = resolveAssetUrl(value, assetBaseUrl);
11139
- if (src) return src;
11140
- }
11141
- // Older captures omitted image sources; the editor logo has a stable identity.
11142
- return attrs.class?.split(/\s+/).includes('TitleBarIconIcon') ? resolveAssetUrl('/icons/icon.svg', assetBaseUrl) : undefined;
11143
- };
11144
- const normalizeAttributes = (value, tag, assetBaseUrl) => {
11145
- const attrs = Object.create(null);
11146
- const entries = Object.entries(value.attrs || {});
11147
- for (const [key, val] of entries) {
11148
- if (typeof val !== 'string') continue;
11149
- if (attributes.test(key)) attrs[key] = attributeValue(key.toLowerCase(), val, assetBaseUrl);
11150
- }
11151
- if (tag === 'img') {
11152
- const src = imageSource(value.attrs || {}, assetBaseUrl);
11153
- if (src) attrs.src = src;
11154
- }
11155
- return attrs;
11156
- };
11157
- const normalizeDom = (root, assetBaseUrl) => {
11158
- let count = 0;
11159
- const visit = (value, depth) => {
11160
- if (++count > 100_000 || depth > 150 || !value || typeof value !== 'object') throw new Error('Invalid replay DOM');
11161
- if (typeof value.text === 'string') return {
11162
- text: value.text
11163
- };
11164
- const tag = value.tag && tags.includes(value.tag) ? value.tag : 'div';
11165
- return {
11166
- attrs: normalizeAttributes(value, tag, assetBaseUrl),
11167
- checked: value.checked === true,
11168
- children: (value.children || []).map(child => visit(child, depth + 1)),
11169
- scroll: Array.isArray(value.scroll) ? [value.scroll[0], value.scroll[1]] : [0, 0],
11170
- svg: value.svg === true,
11171
- tag,
11172
- value: typeof value.value === 'string' ? value.value : undefined
11173
- };
11174
- };
11175
- return visit(root, 0);
11176
- };
11177
10855
  const key = value => value.attrs?.['data-uid'] || value.attrs?.id;
11178
10856
  const compatible = (before, after) => typeof before.text === typeof after.text && before.tag === after.tag && before.svg === after.svg;
11179
10857
  const createNode = (document, value) => {
@@ -11237,8 +10915,7 @@ const createDomRenderer = root => {
11237
10915
  patchAttributes(element, old?.value.attrs || {}, value.attrs);
11238
10916
  const children = patchChildren(element, old?.children || [], value.children);
11239
10917
  patchProperties(element, old?.value, value);
11240
- const [x, y] = value.scroll;
11241
- scrolls.push([element, x, y]);
10918
+ if (value.scroll) scrolls.push([element, ...value.scroll]);
11242
10919
  return {
11243
10920
  children,
11244
10921
  node,
@@ -11272,30 +10949,30 @@ const createRenderer = target => {
11272
10949
  const render = createDomRenderer(surface);
11273
10950
  let sheets = [];
11274
10951
  let previousStyles = [];
11275
- let previousBase;
11276
10952
  let previousClass = originalClass;
11277
10953
  let previousTheme;
11278
10954
  let appliedTheme = originalTheme;
11279
10955
  const removeSheets = () => document.adoptedStyleSheets.filter(sheet => !sheets.includes(sheet));
11280
- const update = (frame, assetBaseUrl) => {
11281
- const dom = normalizeDom(frame.dom, assetBaseUrl);
10956
+ const update = frame => {
10957
+ const {
10958
+ dom
10959
+ } = frame;
11282
10960
  const styles = (frame.styles || []).filter(value => typeof value === 'string');
11283
- if (previousBase !== assetBaseUrl || styles.length !== previousStyles.length || styles.some((css, index) => css !== previousStyles[index])) {
10961
+ if (styles.length !== previousStyles.length || styles.some((css, index) => css !== previousStyles[index])) {
11284
10962
  const others = removeSheets();
11285
10963
  sheets = styles.map((css, index) => {
11286
- if (previousBase === assetBaseUrl && css === previousStyles[index]) return sheets[index];
10964
+ if (css === previousStyles[index]) return sheets[index];
11287
10965
  const sheet = new Sheet();
11288
- sheet.replaceSync(replayCss(css, assetBaseUrl));
10966
+ sheet.replaceSync(css);
11289
10967
  return sheet;
11290
10968
  });
11291
10969
  document.adoptedStyleSheets = [...others, ...sheets];
11292
10970
  previousStyles = styles;
11293
- previousBase = assetBaseUrl;
11294
10971
  }
11295
10972
  const className = typeof frame.documentElement?.className === 'string' ? frame.documentElement.className : '';
11296
10973
  if (root.className !== className) root.className = className;
11297
10974
  previousClass = className;
11298
- const theme = replayCss(typeof frame.documentElement?.style === 'string' ? frame.documentElement.style : '', assetBaseUrl, true);
10975
+ const theme = frame.documentElement?.style || '';
11299
10976
  if (theme !== previousTheme) root.style.cssText = theme;
11300
10977
  previousTheme = theme;
11301
10978
  appliedTheme = root.style.cssText;
@@ -11327,18 +11004,66 @@ const disposeFrame = target => {
11327
11004
  renderers.get(target)?.dispose();
11328
11005
  renderers.delete(target);
11329
11006
  };
11330
- const renderFrame = (target, frame, assetBaseUrl) => {
11007
+ const renderPreparedFrame = (target, frame) => {
11331
11008
  let render = renderers.get(target);
11332
11009
  if (!render) {
11333
11010
  render = createRenderer(target);
11334
11011
  renderers.set(target, render);
11335
11012
  }
11336
- render.render(frame, assetBaseUrl);
11013
+ render.render(frame);
11337
11014
  };
11338
11015
 
11016
+ const createTimelinePreview = container => {
11017
+ const document = container.ownerDocument;
11018
+ const view = document.defaultView;
11019
+ const popup = container.querySelector(':scope > .SessionReplay > .SessionReplayPreview');
11020
+ const image = popup.querySelector('.SessionReplayPreviewImage');
11021
+ let frame;
11022
+ const removeFrame = () => {
11023
+ if (frame?.contentDocument) disposeFrame(frame.contentDocument);
11024
+ frame?.remove();
11025
+ frame = undefined;
11026
+ };
11027
+ return {
11028
+ dispose() {
11029
+ removeFrame();
11030
+ popup.remove();
11031
+ },
11032
+ render(preview, enabled) {
11033
+ if (!enabled) removeFrame();
11034
+ if (!preview) return;
11035
+ if (!frame) {
11036
+ frame = document.createElement('iframe');
11037
+ frame.title = 'Session replay preview';
11038
+ frame.tabIndex = -1;
11039
+ frame.setAttribute('sandbox', 'allow-same-origin');
11040
+ image.append(frame);
11041
+ }
11042
+ const {
11043
+ height,
11044
+ scale,
11045
+ width,
11046
+ x,
11047
+ y
11048
+ } = preview;
11049
+ frame.style.width = `${width}px`;
11050
+ frame.style.height = `${height}px`;
11051
+ frame.style.transform = `scale(${scale})`;
11052
+ image.style.width = `${width * scale}px`;
11053
+ image.style.height = `${height * scale}px`;
11054
+ renderPreparedFrame(frame.contentDocument, preview.frame);
11055
+ const bounds = popup.getBoundingClientRect();
11056
+ popup.style.left = `${Math.max(8, Math.min(view.innerWidth - bounds.width - 8, x - bounds.width / 2))}px`;
11057
+ popup.style.top = `${Math.max(8, y - bounds.height - 8)}px`;
11058
+ }
11059
+ };
11060
+ };
11061
+
11062
+ const settingKey = 'sessionReplay.timelinePreviewEnabled';
11339
11063
  const mountPlayer = async (container, {
11340
11064
  assetBaseUrl,
11341
11065
  source,
11066
+ timelinePreviewEnabled,
11342
11067
  workerUrl
11343
11068
  }) => {
11344
11069
  const {
@@ -11351,131 +11076,206 @@ const mountPlayer = async (container, {
11351
11076
  if (!assets.pathname.endsWith('/')) assets.pathname += '/';
11352
11077
  }
11353
11078
  const client = createClient(workerUrl);
11079
+ const document = ownerDocument;
11080
+ const view = document.defaultView;
11081
+ let enabled = timelinePreviewEnabled ?? true;
11082
+ if (timelinePreviewEnabled === undefined) {
11083
+ try {
11084
+ enabled = view.localStorage.getItem(settingKey) !== 'false';
11085
+ } catch {
11086
+ /* Storage may be unavailable in embedded players. */
11087
+ }
11088
+ }
11354
11089
  container.replaceChildren();
11355
- container.className = 'SessionReplay';
11356
- container.style.cssText = 'position:fixed;inset:0;margin:0;display:flex;flex-direction:column;background:#202020;color:white;z-index:2147483647';
11357
- const document = container.ownerDocument;
11358
- const style = document.createElement('style');
11359
- style.textContent = playerStyles;
11360
- const viewport = document.createElement('div');
11361
- viewport.style.cssText = 'flex:1;min-height:0;overflow:auto;position:relative';
11362
- const surface = createReplayRoot(document);
11363
- viewport.append(surface);
11364
- const controls = document.createElement('div');
11365
- controls.className = 'SessionReplayControls';
11366
- controls.setAttribute('role', 'group');
11367
- controls.setAttribute('aria-label', 'Session replay controls');
11368
- const play = document.createElement('button');
11369
- play.type = 'button';
11370
- play.className = 'SessionReplayPlay';
11371
- const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
11372
- icon.setAttribute('viewBox', '0 0 24 24');
11373
- icon.setAttribute('aria-hidden', 'true');
11374
- icon.setAttribute('focusable', 'false');
11375
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
11376
- icon.append(path);
11377
- play.append(icon);
11378
- const updatePlayButton = playing => {
11379
- const label = playing ? 'Pause' : 'Play';
11380
- play.setAttribute('aria-label', label);
11381
- play.title = label;
11382
- path.setAttribute('d', playing ? 'M6 4h4v16H6zM14 4h4v16h-4z' : 'M8 4v16l12-8z');
11383
- };
11384
- updatePlayButton(false);
11385
- const slider = document.createElement('input');
11386
- slider.type = 'range';
11387
- slider.min = '0';
11388
- slider.step = '1';
11389
- slider.value = '0';
11390
- slider.setAttribute('aria-label', 'Session replay position');
11391
- slider.className = 'SessionReplayPosition';
11392
- const status = document.createElement('output');
11393
- status.className = 'SessionReplayTime';
11394
- // Playback updates frequently; announce the position only when the slider is used.
11395
- status.setAttribute('aria-live', 'off');
11396
- const activity = createActivityChart(document);
11397
- controls.append(play, activity.element, slider, status);
11398
- container.append(style, viewport, controls);
11090
+ const setDom = createDomRenderer(container);
11091
+ let preview;
11399
11092
  let disposed = false;
11400
- let playing = false;
11093
+ let sequence = 0;
11401
11094
  let timer;
11402
- let requestId = 0;
11403
- let position = 0;
11404
- let duration = 0;
11405
- const show = result => {
11406
- ({
11407
- duration,
11408
- position
11409
- } = result);
11410
- activity.setPosition(duration > 0 ? position / duration : 0);
11411
- slider.max = String(Math.ceil(duration));
11412
- slider.value = String(Math.round(position));
11413
- slider.style.setProperty('--replay-progress', `${duration > 0 ? position / duration * 100 : 0}%`);
11414
- const time = `${(position / 1000).toFixed(1)} / ${(duration / 1000).toFixed(1)} s`;
11415
- slider.ariaValueText = time;
11416
- status.textContent = time;
11417
- renderFrame(surface, result.frame, assets?.href);
11418
- };
11419
- const pause = () => {
11420
- playing = false;
11421
- clearTimeout(timer);
11422
- updatePlayButton(false);
11423
- };
11424
- const seek = async time => {
11425
- const id = ++requestId;
11426
- const result = await client.invoke('seek', time);
11427
- if (!disposed && id === requestId) show(result);
11095
+ let previewsEnabled = enabled;
11096
+ const apply = result => {
11097
+ if (disposed) return;
11098
+ setDom(result.dom);
11099
+ if (result.frame) renderPreparedFrame(container.querySelector('.SessionReplaySurface'), result.frame);
11100
+ previewsEnabled = result.previewEnabled;
11101
+ preview ||= createTimelinePreview(container);
11102
+ preview.render(result.preview, result.previewEnabled);
11103
+ if (result.delay === undefined) {
11104
+ clearTimeout(timer);
11105
+ timer = undefined;
11106
+ } else if (timer === undefined) {
11107
+ timer = setTimeout(() => {
11108
+ timer = undefined;
11109
+ void send({
11110
+ now: performance.now(),
11111
+ type: 'tick'
11112
+ });
11113
+ }, result.delay);
11114
+ }
11428
11115
  };
11429
11116
  const report = error => {
11430
- pause();
11117
+ if (disposed) return;
11118
+ clearTimeout(timer);
11119
+ timer = undefined;
11120
+ const status = container.querySelector(':scope > .SessionReplay > .SessionReplayControls > output') || document.createElement('output');
11121
+ if (!status.parentNode) container.append(status);
11431
11122
  status.setAttribute('role', 'alert');
11432
11123
  status.setAttribute('aria-live', 'assertive');
11433
11124
  status.textContent = error instanceof Error ? error.message : String(error);
11434
11125
  };
11435
- slider.oninput = () => {
11436
- pause();
11437
- void seek(Number(slider.value)).catch(report);
11438
- };
11439
- activity.element.onclick = event => {
11440
- const bounds = activity.element.getBoundingClientRect();
11441
- if (!bounds.width || slider.disabled) return;
11442
- pause();
11443
- slider.focus();
11444
- const fraction = Math.max(0, Math.min(1, (event.clientX - bounds.left) / bounds.width));
11445
- void seek(fraction * duration).catch(report);
11446
- };
11447
- play.onclick = () => {
11448
- if (playing) {
11449
- pause();
11450
- return;
11126
+ const send = async event => {
11127
+ if (disposed) return;
11128
+ try {
11129
+ apply(await client.invoke('SessionReplay.dispatch', 1, event, ++sequence));
11130
+ } catch (error) {
11131
+ report(error);
11451
11132
  }
11452
- playing = true;
11453
- updatePlayButton(true);
11454
- const origin = performance.now() - (position >= duration ? 0 : position);
11455
- const tick = async () => {
11456
- if (!playing || disposed) return;
11457
- try {
11458
- await seek(performance.now() - origin);
11459
- if (position >= duration) pause();else if (playing) timer = setTimeout(() => {
11460
- void tick();
11461
- }, 50);
11462
- } catch (error) {
11463
- report(error);
11464
- }
11465
- };
11466
- void tick();
11467
11133
  };
11468
11134
  try {
11469
- const initial = await client.invoke('load', source);
11470
- activity.setActivity(initial.activity);
11471
- show(initial);
11135
+ apply(await client.invoke('SessionReplay.create', 1, enabled, assets?.href));
11136
+ apply(await client.invoke('SessionReplay.loadContent', 1, source));
11472
11137
  } catch (error) {
11473
- play.disabled = slider.disabled = true;
11474
11138
  report(error);
11475
11139
  }
11140
+ if (!container.querySelector(':scope > .SessionReplay > .SessionReplayControls')) {
11141
+ client.dispose();
11142
+ return () => container.replaceChildren();
11143
+ }
11144
+ let previewTimer;
11145
+ let pendingPreview;
11146
+ let previewBusy = false;
11147
+ let previewGeneration = 0;
11148
+ const surface = container.querySelector('.SessionReplaySurface');
11149
+ const controls = container.querySelector(':scope > .SessionReplay > .SessionReplayControls');
11150
+ const slider = controls.querySelector('.SessionReplayPosition');
11151
+ const chart = controls.querySelector('.SessionReplayActivity');
11152
+ const play = controls.querySelector('.SessionReplayPlay');
11153
+ const checkbox = controls.querySelector('[type="checkbox"]');
11154
+ const seek = event => {
11155
+ clearTimeout(timer);
11156
+ timer = undefined;
11157
+ void send(event);
11158
+ };
11159
+ play.onclick = () => seek({
11160
+ now: performance.now(),
11161
+ type: 'togglePlay'
11162
+ });
11163
+ slider.oninput = () => seek({
11164
+ now: performance.now(),
11165
+ position: Number(slider.value),
11166
+ type: 'seek'
11167
+ });
11168
+ const point = event => {
11169
+ const target = event.currentTarget;
11170
+ const bounds = target.getBoundingClientRect();
11171
+ return {
11172
+ left: bounds.left,
11173
+ pointerId: event.pointerId,
11174
+ pointerType: event.pointerType,
11175
+ slider: target === slider,
11176
+ top: chart.getBoundingClientRect().top,
11177
+ width: bounds.width,
11178
+ windowWidth: view.innerWidth,
11179
+ x: event.clientX
11180
+ };
11181
+ };
11182
+ chart.onpointerdown = event => {
11183
+ if (event.button !== 0 || !event.isPrimary || slider.disabled) return;
11184
+ event.preventDefault();
11185
+ chart.setPointerCapture(event.pointerId);
11186
+ slider.focus();
11187
+ seek({
11188
+ now: performance.now(),
11189
+ point: point(event),
11190
+ type: 'pointerDown'
11191
+ });
11192
+ };
11193
+ chart.onpointermove = event => {
11194
+ if (chart.hasPointerCapture(event.pointerId)) seek({
11195
+ now: performance.now(),
11196
+ point: point(event),
11197
+ type: 'pointerMove'
11198
+ });
11199
+ };
11200
+ const endDrag = event => {
11201
+ if (chart.hasPointerCapture(event.pointerId)) chart.releasePointerCapture(event.pointerId);
11202
+ void send({
11203
+ pointerId: event.pointerId,
11204
+ type: 'pointerUp'
11205
+ });
11206
+ };
11207
+ chart.onpointerup = endDrag;
11208
+ chart.onpointercancel = endDrag;
11209
+ chart.onlostpointercapture = endDrag;
11210
+ const updatePreview = async () => {
11211
+ previewTimer = undefined;
11212
+ if (!pendingPreview || previewBusy || disposed) return;
11213
+ const event = {
11214
+ point: pendingPreview,
11215
+ type: 'preview'
11216
+ };
11217
+ pendingPreview = undefined;
11218
+ previewBusy = true;
11219
+ const generation = previewGeneration;
11220
+ const id = ++sequence;
11221
+ try {
11222
+ const result = await client.invoke('SessionReplay.dispatch', 1, event, id);
11223
+ if (!disposed && generation === previewGeneration) apply(result);
11224
+ } catch {
11225
+ /* Preview errors must not interrupt playback. */
11226
+ } finally {
11227
+ previewBusy = false;
11228
+ if (pendingPreview && !disposed) previewTimer = setTimeout(() => void updatePreview(), 60);
11229
+ }
11230
+ };
11231
+ const hover = event => {
11232
+ if (!previewsEnabled || slider.disabled || event.pointerType === 'touch') return;
11233
+ previewGeneration++;
11234
+ pendingPreview = point(event);
11235
+ if (!previewBusy && previewTimer === undefined) previewTimer = setTimeout(() => void updatePreview(), 60);
11236
+ };
11237
+ const hide = () => {
11238
+ previewGeneration++;
11239
+ pendingPreview = undefined;
11240
+ clearTimeout(previewTimer);
11241
+ previewTimer = undefined;
11242
+ void send({
11243
+ type: 'hidePreview'
11244
+ });
11245
+ };
11246
+ for (const target of [slider, chart]) {
11247
+ target.addEventListener('pointermove', hover);
11248
+ target.addEventListener('pointerleave', hide);
11249
+ target.addEventListener('pointercancel', hide);
11250
+ }
11251
+ const keydown = event => {
11252
+ if (event.key === 'Escape') hide();
11253
+ };
11254
+ view.addEventListener('resize', hide);
11255
+ view.addEventListener('blur', hide);
11256
+ document.addEventListener('keydown', keydown);
11257
+ checkbox.onchange = () => {
11258
+ hide();
11259
+ const enabled = checkbox.checked;
11260
+ void send({
11261
+ enabled,
11262
+ type: 'setPreviewEnabled'
11263
+ });
11264
+ try {
11265
+ view.localStorage.setItem(settingKey, String(enabled));
11266
+ } catch {
11267
+ /* The setting still works without storage. */
11268
+ }
11269
+ };
11476
11270
  return () => {
11477
11271
  disposed = true;
11478
- pause();
11272
+ clearTimeout(timer);
11273
+ clearTimeout(previewTimer);
11274
+ pendingPreview = undefined;
11275
+ view.removeEventListener('resize', hide);
11276
+ view.removeEventListener('blur', hide);
11277
+ document.removeEventListener('keydown', keydown);
11278
+ preview?.dispose();
11479
11279
  client.dispose();
11480
11280
  disposeFrame(surface);
11481
11281
  container.replaceChildren();