@mochabug/adapt-web 1.0.0-rc42 → 1.0.0-rc44

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/dist/esm/index.js CHANGED
@@ -36,9 +36,14 @@ const DEFAULT_STYLES = `
36
36
  --mb-adapt-dialog-z: 1000;
37
37
  --mb-adapt-dialog-width: 80%;
38
38
  --mb-adapt-dialog-height: 80%;
39
- --mb-adapt-drag-handle-width: 6px;
40
- --mb-adapt-drag-handle-color: #d1d5db;
41
- --mb-adapt-drag-handle-hover: #6b7280;
39
+ --mb-adapt-drag-handle-width: 8px;
40
+ --mb-adapt-drag-handle-color: #cbd5e1;
41
+ --mb-adapt-drag-handle-hover: #94a3b8;
42
+ --mb-adapt-toolbar-height: 32px;
43
+ --mb-adapt-toolbar-bg: rgba(249, 250, 251, 0.85);
44
+ --mb-adapt-toolbar-border: #e5e7eb;
45
+ --mb-adapt-toolbar-color: #6b7280;
46
+ --mb-adapt-toolbar-btn-hover: #e5e7eb;
42
47
  --mb-adapt-banner-height: 40px;
43
48
  --mb-adapt-banner-bg: #f9fafb;
44
49
  --mb-adapt-banner-border: #e5e7eb;
@@ -70,8 +75,12 @@ const DEFAULT_STYLES = `
70
75
  --mb-adapt-dialog-bg: #1f2937;
71
76
  --mb-adapt-banner-bg: #111827;
72
77
  --mb-adapt-banner-border: #374151;
73
- --mb-adapt-drag-handle-color: #4b5563;
74
- --mb-adapt-drag-handle-hover: #6b7280;
78
+ --mb-adapt-drag-handle-color: #475569;
79
+ --mb-adapt-drag-handle-hover: #64748b;
80
+ --mb-adapt-toolbar-bg: rgba(17, 24, 39, 0.85);
81
+ --mb-adapt-toolbar-border: #374151;
82
+ --mb-adapt-toolbar-color: #9ca3af;
83
+ --mb-adapt-toolbar-btn-hover: #374151;
75
84
  --mb-adapt-button-color: #e5e7eb;
76
85
  --mb-adapt-button-hover-bg: #374151;
77
86
 
@@ -121,12 +130,6 @@ const DEFAULT_STYLES = `
121
130
  transition: opacity 0.1s ease-out;
122
131
  }
123
132
 
124
- /* When inside main-container (dialog mode), allow iframe to size itself */
125
- .mb-adapt__main-container .mb-adapt__iframe {
126
- height: auto;
127
- min-height: 300px;
128
- }
129
-
130
133
  .mb-adapt__iframe--visible {
131
134
  opacity: 1;
132
135
  }
@@ -143,11 +146,32 @@ const DEFAULT_STYLES = `
143
146
  background-color: var(--mb-adapt-drag-handle-color);
144
147
  cursor: ew-resize;
145
148
  z-index: 10;
146
- transition: background-color 0.2s, left 0.15s ease-out;
149
+ transition: background-color 0.2s, left 0.15s ease-out, box-shadow 0.2s;
150
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04);
151
+ }
152
+
153
+ .mb-adapt__drag-handle::after {
154
+ content: '';
155
+ position: absolute;
156
+ top: 50%;
157
+ left: 50%;
158
+ transform: translate(-50%, -50%);
159
+ width: 4px;
160
+ height: 24px;
161
+ background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
162
+ background-size: 4px 6px;
163
+ background-position: center;
164
+ opacity: 0.5;
165
+ color: var(--mb-adapt-drag-handle-hover);
147
166
  }
148
167
 
149
168
  .mb-adapt__drag-handle:hover {
150
169
  background-color: var(--mb-adapt-drag-handle-hover);
170
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
171
+ }
172
+
173
+ .mb-adapt__drag-handle:hover::after {
174
+ opacity: 0.8;
151
175
  }
152
176
 
153
177
  .mb-adapt__drag-overlay {
@@ -228,43 +252,42 @@ const DEFAULT_STYLES = `
228
252
  position: absolute;
229
253
  top: 50%;
230
254
  transform: translateY(-50%);
231
- background: linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(248,250,252,0.98) 100%);
232
- border: none;
233
- color: #64748b;
255
+ background: linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(241,245,249,0.98) 100%);
256
+ border: 1px solid rgba(0, 0, 0, 0.08);
257
+ color: #475569;
234
258
  cursor: pointer;
235
- width: 14px;
236
- height: 48px;
259
+ width: 22px;
260
+ height: 64px;
237
261
  display: none;
238
262
  align-items: center;
239
263
  justify-content: center;
240
264
  z-index: 100;
241
265
  transition: all 0.2s ease-out;
242
- font-size: 14px;
266
+ font-size: 16px;
267
+ font-weight: 600;
243
268
  line-height: 1;
244
269
  padding: 0;
245
270
  box-shadow:
246
- 0 2px 8px rgba(0, 0, 0, 0.12),
247
- 0 1px 3px rgba(0, 0, 0, 0.08),
248
- inset 0 1px 0 rgba(255, 255, 255, 0.8);
249
- border-radius: 4px;
250
- backdrop-filter: blur(8px);
251
- -webkit-backdrop-filter: blur(8px);
271
+ 0 2px 12px rgba(0, 0, 0, 0.14),
272
+ 0 1px 4px rgba(0, 0, 0, 0.10);
273
+ border-radius: 6px;
274
+ backdrop-filter: blur(12px);
275
+ -webkit-backdrop-filter: blur(12px);
252
276
  }
253
277
 
254
278
  .mb-adapt__expand-button:hover {
255
- background: linear-gradient(180deg, rgba(255,255,255,1) 0%, rgba(241,245,249,1) 100%);
256
- color: #475569;
257
- transform: translateY(-50%) scale(1.02);
279
+ background: linear-gradient(180deg, rgba(255,255,255,1) 0%, rgba(226,232,240,1) 100%);
280
+ color: #1e293b;
281
+ transform: translateY(-50%) scale(1.04);
258
282
  box-shadow:
259
- 0 4px 12px rgba(0, 0, 0, 0.15),
260
- 0 2px 4px rgba(0, 0, 0, 0.1),
261
- inset 0 1px 0 rgba(255, 255, 255, 1);
283
+ 0 4px 16px rgba(0, 0, 0, 0.18),
284
+ 0 2px 6px rgba(0, 0, 0, 0.12);
262
285
  }
263
286
 
264
287
  .mb-adapt__expand-button:active {
265
- transform: translateY(-50%) scale(0.98);
288
+ transform: translateY(-50%) scale(0.97);
266
289
  box-shadow:
267
- 0 1px 4px rgba(0, 0, 0, 0.1),
290
+ 0 1px 4px rgba(0, 0, 0, 0.10),
268
291
  0 1px 2px rgba(0, 0, 0, 0.06);
269
292
  }
270
293
 
@@ -276,83 +299,79 @@ const DEFAULT_STYLES = `
276
299
  left: 0;
277
300
  border-top-left-radius: 0;
278
301
  border-bottom-left-radius: 0;
302
+ border-left: none;
279
303
  box-shadow:
280
- 2px 2px 8px rgba(0, 0, 0, 0.12),
281
- 1px 1px 3px rgba(0, 0, 0, 0.08),
282
- inset 0 1px 0 rgba(255, 255, 255, 0.8);
304
+ 2px 2px 12px rgba(0, 0, 0, 0.14),
305
+ 1px 1px 4px rgba(0, 0, 0, 0.10);
283
306
  }
284
307
 
285
308
  .mb-adapt__expand-button--left:hover {
286
309
  box-shadow:
287
- 3px 4px 12px rgba(0, 0, 0, 0.15),
288
- 2px 2px 4px rgba(0, 0, 0, 0.1),
289
- inset 0 1px 0 rgba(255, 255, 255, 1);
310
+ 3px 4px 16px rgba(0, 0, 0, 0.18),
311
+ 2px 2px 6px rgba(0, 0, 0, 0.12);
290
312
  }
291
313
 
292
314
  .mb-adapt__expand-button--right {
293
315
  right: 0;
294
316
  border-top-right-radius: 0;
295
317
  border-bottom-right-radius: 0;
318
+ border-right: none;
296
319
  box-shadow:
297
- -2px 2px 8px rgba(0, 0, 0, 0.12),
298
- -1px 1px 3px rgba(0, 0, 0, 0.08),
299
- inset 0 1px 0 rgba(255, 255, 255, 0.8);
320
+ -2px 2px 12px rgba(0, 0, 0, 0.14),
321
+ -1px 1px 4px rgba(0, 0, 0, 0.10);
300
322
  }
301
323
 
302
324
  .mb-adapt__expand-button--right:hover {
303
325
  box-shadow:
304
- -3px 4px 12px rgba(0, 0, 0, 0.15),
305
- -2px 2px 4px rgba(0, 0, 0, 0.1),
306
- inset 0 1px 0 rgba(255, 255, 255, 1);
326
+ -3px 4px 16px rgba(0, 0, 0, 0.18),
327
+ -2px 2px 6px rgba(0, 0, 0, 0.12);
307
328
  }
308
329
 
309
330
  .mb-adapt__expand-button--dialog {
310
331
  left: 50%;
311
332
  top: 0;
312
333
  transform: translateX(-50%);
313
- width: 56px;
314
- height: 20px;
334
+ width: 64px;
335
+ height: 24px;
315
336
  font-size: 16px;
316
- line-height: 20px;
337
+ line-height: 24px;
317
338
  vertical-align: middle;
318
339
  text-align: center;
319
- border-radius: 6px;
340
+ border-radius: 8px;
320
341
  border-top-left-radius: 0;
321
342
  border-top-right-radius: 0;
343
+ border-top: none;
322
344
  box-shadow:
323
- 0 3px 8px rgba(0, 0, 0, 0.12),
324
- 0 1px 3px rgba(0, 0, 0, 0.08),
325
- inset 0 1px 0 rgba(255, 255, 255, 0.8);
345
+ 0 3px 12px rgba(0, 0, 0, 0.14),
346
+ 0 1px 4px rgba(0, 0, 0, 0.10);
326
347
  }
327
348
 
328
349
  .mb-adapt__expand-button--dialog:hover {
329
- transform: translateX(-50%) scale(1.02);
350
+ transform: translateX(-50%) scale(1.04);
330
351
  box-shadow:
331
- 0 5px 12px rgba(0, 0, 0, 0.15),
332
- 0 2px 4px rgba(0, 0, 0, 0.1),
333
- inset 0 1px 0 rgba(255, 255, 255, 1);
352
+ 0 5px 16px rgba(0, 0, 0, 0.18),
353
+ 0 2px 6px rgba(0, 0, 0, 0.12);
334
354
  }
335
355
 
336
356
  .mb-adapt__expand-button--dialog:active {
337
- transform: translateX(-50%) scale(0.98);
357
+ transform: translateX(-50%) scale(0.97);
338
358
  }
339
359
 
340
360
  .mb-adapt--dark .mb-adapt__expand-button {
341
- background: linear-gradient(180deg, rgba(31,41,55,0.98) 0%, rgba(17,24,39,0.98) 100%);
342
- color: #9ca3af;
361
+ background: linear-gradient(180deg, rgba(51,65,85,0.98) 0%, rgba(30,41,59,0.98) 100%);
362
+ border-color: rgba(255, 255, 255, 0.1);
363
+ color: #cbd5e1;
343
364
  box-shadow:
344
- 0 2px 8px rgba(0, 0, 0, 0.3),
345
- 0 1px 3px rgba(0, 0, 0, 0.2),
346
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
365
+ 0 2px 12px rgba(0, 0, 0, 0.35),
366
+ 0 1px 4px rgba(0, 0, 0, 0.25);
347
367
  }
348
368
 
349
369
  .mb-adapt--dark .mb-adapt__expand-button:hover {
350
- background: linear-gradient(180deg, rgba(55,65,81,1) 0%, rgba(31,41,55,1) 100%);
351
- color: #d1d5db;
370
+ background: linear-gradient(180deg, rgba(71,85,105,1) 0%, rgba(51,65,85,1) 100%);
371
+ color: #f1f5f9;
352
372
  box-shadow:
353
- 0 4px 12px rgba(0, 0, 0, 0.4),
354
- 0 2px 4px rgba(0, 0, 0, 0.3),
355
- inset 0 1px 0 rgba(255, 255, 255, 0.15);
373
+ 0 4px 16px rgba(0, 0, 0, 0.45),
374
+ 0 2px 6px rgba(0, 0, 0, 0.35);
356
375
  }
357
376
 
358
377
  .mb-adapt__drag-handle--disabled {
@@ -360,14 +379,90 @@ const DEFAULT_STYLES = `
360
379
  opacity: 0.3;
361
380
  }
362
381
 
382
+ .mb-adapt__frame--has-toolbar {
383
+ display: flex;
384
+ flex-direction: column;
385
+ }
386
+
387
+ .mb-adapt__frame--has-toolbar.mb-adapt__frame--hidden {
388
+ display: none;
389
+ }
390
+
391
+ .mb-adapt__frame--has-toolbar .mb-adapt__iframe {
392
+ flex: 1;
393
+ height: auto;
394
+ }
395
+
396
+ .mb-adapt__toolbar {
397
+ display: flex;
398
+ align-items: center;
399
+ justify-content: flex-end;
400
+ height: var(--mb-adapt-toolbar-height);
401
+ background: var(--mb-adapt-toolbar-bg);
402
+ border-bottom: 1px solid var(--mb-adapt-toolbar-border);
403
+ padding: 0 4px;
404
+ flex-shrink: 0;
405
+ backdrop-filter: blur(12px);
406
+ -webkit-backdrop-filter: blur(12px);
407
+ gap: 2px;
408
+ }
409
+
410
+ .mb-adapt__toolbar-btn {
411
+ display: flex;
412
+ align-items: center;
413
+ justify-content: center;
414
+ width: 26px;
415
+ height: 26px;
416
+ border: none;
417
+ border-radius: 6px;
418
+ background: transparent;
419
+ color: var(--mb-adapt-toolbar-color);
420
+ cursor: pointer;
421
+ padding: 0;
422
+ transition: background-color 0.15s, color 0.15s;
423
+ }
424
+
425
+ .mb-adapt__toolbar-btn:hover {
426
+ background: var(--mb-adapt-toolbar-btn-hover);
427
+ color: var(--mb-adapt-button-color);
428
+ }
429
+
430
+ .mb-adapt__toolbar-btn svg {
431
+ width: 14px;
432
+ height: 14px;
433
+ stroke: currentColor;
434
+ stroke-width: 2;
435
+ fill: none;
436
+ stroke-linecap: round;
437
+ stroke-linejoin: round;
438
+ }
439
+
440
+ .mb-adapt__toolbar-btn--exit:hover {
441
+ background: #fecaca;
442
+ color: #dc2626;
443
+ }
444
+
445
+ .mb-adapt--dark .mb-adapt__toolbar-btn--exit:hover {
446
+ background: rgba(220, 38, 38, 0.2);
447
+ color: #fca5a5;
448
+ }
449
+
363
450
  .mb-adapt__main-container {
364
451
  position: relative;
452
+ display: flex;
453
+ flex-direction: column;
365
454
  width: 100%;
366
- flex: 0 0 auto;
455
+ flex: 1;
456
+ min-height: 0;
367
457
  overflow: visible;
368
458
  background: transparent;
369
459
  }
370
460
 
461
+ .mb-adapt__main-container .mb-adapt__iframe {
462
+ flex: 1;
463
+ min-height: 0;
464
+ }
465
+
371
466
  /* Responsive dialog - full screen on small viewports */
372
467
  @media (max-width: 600px) {
373
468
  .mb-adapt__dialog {
@@ -412,6 +507,21 @@ const DEFAULT_STYLES = `
412
507
  cap-widget::part(attribution) {
413
508
  display: none;
414
509
  }
510
+
511
+ .mb-adapt__stopped-placeholder {
512
+ display: flex;
513
+ align-items: center;
514
+ justify-content: center;
515
+ width: 100%;
516
+ height: 100%;
517
+ min-height: 200px;
518
+ color: #6b7280;
519
+ font-family: system-ui, -apple-system, sans-serif;
520
+ font-size: 16px;
521
+ border-radius: var(--mb-adapt-border-radius);
522
+ background: var(--mb-adapt-banner-bg);
523
+ border: 1px solid var(--mb-adapt-banner-border);
524
+ }
415
525
  `;
416
526
  /**
417
527
  * Browser client for rendering Adapt automation sessions in iframes.
@@ -438,6 +548,7 @@ export class AdaptWebClient {
438
548
  this.dragHandleElement = null;
439
549
  this.mainExpandButton = null;
440
550
  this.forkExpandButton = null;
551
+ this.forkToolbarElement = null;
441
552
  // Dialog elements
442
553
  this.mainContainer = null;
443
554
  this.dialogBackdrop = null;
@@ -450,6 +561,8 @@ export class AdaptWebClient {
450
561
  this.messageHandler = null;
451
562
  this.backdropClickHandler = null;
452
563
  this.destroyed = false;
564
+ // Stopped placeholder element
565
+ this.stoppedPlaceholder = null;
453
566
  // Cap.js widget instance (for PoW challenges)
454
567
  this.capWidgetInstance = null;
455
568
  this.options = {
@@ -648,6 +761,7 @@ export class AdaptWebClient {
648
761
  this.dragHandleElement = null;
649
762
  this.mainExpandButton = null;
650
763
  this.forkExpandButton = null;
764
+ this.forkToolbarElement = null;
651
765
  this.mainContainer = null;
652
766
  this.dialogBackdrop = null;
653
767
  this.dialogElement = null;
@@ -703,14 +817,10 @@ export class AdaptWebClient {
703
817
  this.dialogBackdrop.removeEventListener("click", this.backdropClickHandler);
704
818
  this.backdropClickHandler = null;
705
819
  }
706
- // Add new handler if enabled
820
+ // Add new handler if enabled (acts as minimize)
707
821
  if (enabled) {
708
822
  this.backdropClickHandler = () => {
709
- if (this.currentFork) {
710
- this.forkQueue.unshift(this.currentFork);
711
- }
712
- this.currentFork = null;
713
- this.updateDialogVisibility();
823
+ this.handleForkMinimize();
714
824
  };
715
825
  this.dialogBackdrop.addEventListener("click", this.backdropClickHandler);
716
826
  }
@@ -1004,6 +1114,7 @@ export class AdaptWebClient {
1004
1114
  this.dragHandleElement = null;
1005
1115
  this.mainExpandButton = null;
1006
1116
  this.forkExpandButton = null;
1117
+ this.forkToolbarElement = null;
1007
1118
  this.mainContainer = null;
1008
1119
  this.dialogBackdrop = null;
1009
1120
  this.dialogElement = null;
@@ -1013,11 +1124,16 @@ export class AdaptWebClient {
1013
1124
  this.resizeObserver = null;
1014
1125
  this.messageHandler = null;
1015
1126
  this.backdropClickHandler = null;
1127
+ this.stoppedPlaceholder = null;
1016
1128
  }
1017
1129
  onUrl(msg) {
1018
1130
  if (this.destroyed)
1019
1131
  return;
1020
1132
  const fork = msg.fork || "";
1133
+ if (msg.stopped) {
1134
+ this.handleStoppedMessage(fork);
1135
+ return;
1136
+ }
1021
1137
  if (msg.done) {
1022
1138
  this.handleDoneMessage(fork);
1023
1139
  return;
@@ -1048,27 +1164,60 @@ export class AdaptWebClient {
1048
1164
  if (!fork) {
1049
1165
  return;
1050
1166
  }
1051
- // Remove from queue
1167
+ // Remove from queue (but not the current fork — it's tracked separately).
1168
+ // Also discard any previously-completed (stale) forks from the queue.
1169
+ this.forkQueue = this.forkQueue.filter((item) => item.fork !== fork && !item.completed);
1170
+ if (this.currentFork?.fork === fork) {
1171
+ if (this.forkQueue.length > 0) {
1172
+ // Another fork is waiting — advance to it
1173
+ this.currentFork = null;
1174
+ this.activateNextFork();
1175
+ }
1176
+ else {
1177
+ // No active forks remain — keep this fork visible (graceful completion)
1178
+ // but mark it so it gets evicted when a new fork arrives.
1179
+ this.currentFork.completed = true;
1180
+ }
1181
+ }
1182
+ }
1183
+ handleStoppedMessage(fork) {
1184
+ if (!fork) {
1185
+ // Main URL stopped — clear iframe and show placeholder
1186
+ this.mainUrl = null;
1187
+ this.mainToken = undefined;
1188
+ if (this.mainIframe) {
1189
+ this.mainIframe.src = "about:blank";
1190
+ this.mainIframe.classList.remove("mb-adapt__iframe--visible");
1191
+ this.mainIframe.classList.add("mb-adapt__iframe--hidden");
1192
+ }
1193
+ this.showStoppedPlaceholder();
1194
+ return;
1195
+ }
1196
+ // Fork stopped — remove from queue
1052
1197
  this.forkQueue = this.forkQueue.filter((item) => item.fork !== fork);
1053
- // If current fork is done, clear tracking but keep iframe mounted
1054
- // Then check if there's another fork waiting
1055
1198
  if (this.currentFork?.fork === fork) {
1056
1199
  this.currentFork = null;
1057
- // Check if there's another fork in queue to display
1058
1200
  if (this.forkQueue.length > 0) {
1059
1201
  this.activateNextFork();
1060
1202
  }
1061
1203
  else {
1062
- // No more forks - update visibility (hide fork frame in side-by-side,
1063
- // or keep dialog open until user closes it)
1204
+ // No more forks clear the fork iframe
1205
+ if (this.forkIframe) {
1206
+ this.forkIframe.src = "about:blank";
1207
+ this.forkIframe.classList.remove("mb-adapt__iframe--visible");
1208
+ this.forkIframe.classList.add("mb-adapt__iframe--hidden");
1209
+ }
1064
1210
  if (this.options.forkDisplayMode === "side-by-side") {
1065
1211
  this.updateSideBySideVisibility();
1066
1212
  }
1067
- // For dialog mode: keep dialog open until user clicks close or backdrop
1213
+ else {
1214
+ this.updateDialogVisibility();
1215
+ }
1068
1216
  }
1069
1217
  }
1070
1218
  }
1071
1219
  handleMainUrl(url, token) {
1220
+ this.removeStoppedPlaceholder();
1072
1221
  this.mainUrl = url;
1073
1222
  this.mainToken = token;
1074
1223
  // Update main iframe only - never touch dialog state
@@ -1089,9 +1238,10 @@ export class AdaptWebClient {
1089
1238
  this.mainToken = token;
1090
1239
  this.updateMainIframe();
1091
1240
  }
1092
- // Add to queue and activate if no current fork
1241
+ // Add to queue and activate if no current fork or current fork is completed.
1093
1242
  this.forkQueue.push(forkItem);
1094
- if (!this.currentFork) {
1243
+ if (!this.currentFork || this.currentFork.completed) {
1244
+ this.currentFork = null;
1095
1245
  this.activateNextFork();
1096
1246
  }
1097
1247
  }
@@ -1132,11 +1282,16 @@ export class AdaptWebClient {
1132
1282
  updateForkIframe() {
1133
1283
  if (!this.currentFork || !this.forkIframe)
1134
1284
  return;
1135
- // Update iframe src if changed
1136
1285
  const newSrc = getIframeSrc(this.currentFork.url, this.currentFork.token);
1137
- if (this.forkIframe.src !== newSrc) {
1138
- this.forkIframe.src = newSrc;
1139
- }
1286
+ // Replace iframe to force a fresh load — browsers don't reload when only
1287
+ // the hash fragment changes (same plugin URL, different fork token).
1288
+ const parent = this.forkIframe.parentElement;
1289
+ if (parent) {
1290
+ const fresh = this.createHiddenIframe();
1291
+ parent.replaceChild(fresh, this.forkIframe);
1292
+ this.forkIframe = fresh;
1293
+ }
1294
+ this.forkIframe.src = newSrc;
1140
1295
  // Show the iframe with fade-in
1141
1296
  this.showIframe(this.forkIframe);
1142
1297
  }
@@ -1169,6 +1324,10 @@ export class AdaptWebClient {
1169
1324
  // Reset border radius in case coming from dialog mode
1170
1325
  this.forkIframe.style.borderRadius = "";
1171
1326
  this.mainFrameElement.appendChild(this.mainIframe);
1327
+ // Fork toolbar with minimize + exit
1328
+ this.forkToolbarElement = this.createForkToolbar();
1329
+ this.forkFrameElement.classList.add("mb-adapt__frame--has-toolbar");
1330
+ this.forkFrameElement.appendChild(this.forkToolbarElement);
1172
1331
  this.forkFrameElement.appendChild(this.forkIframe);
1173
1332
  // Drag handle - initially hidden until fork arrives
1174
1333
  this.dragHandleElement = document.createElement("div");
@@ -1315,6 +1474,76 @@ export class AdaptWebClient {
1315
1474
  this.forkFrameElement.style.width = `${forkWidth}%`;
1316
1475
  this.dragHandleElement.style.left = `${this.splitPercentage}%`;
1317
1476
  }
1477
+ createForkToolbar() {
1478
+ const toolbar = document.createElement("div");
1479
+ toolbar.className =
1480
+ this.options.classNames?.toolbar || "mb-adapt__toolbar";
1481
+ // Minimize button (horizontal line icon)
1482
+ const minimizeBtn = document.createElement("button");
1483
+ minimizeBtn.className =
1484
+ this.options.classNames?.toolbarButton || "mb-adapt__toolbar-btn";
1485
+ minimizeBtn.setAttribute("aria-label", "Minimize fork");
1486
+ minimizeBtn.setAttribute("title", "Minimize");
1487
+ minimizeBtn.innerHTML = `<svg viewBox="0 0 16 16"><line x1="3" y1="8" x2="13" y2="8"/></svg>`;
1488
+ minimizeBtn.addEventListener("click", () => {
1489
+ this.handleForkMinimize();
1490
+ });
1491
+ // Exit button (X icon) — stops the fork via the API
1492
+ const exitBtn = document.createElement("button");
1493
+ exitBtn.className = `${this.options.classNames?.toolbarButton || "mb-adapt__toolbar-btn"} mb-adapt__toolbar-btn--exit`;
1494
+ exitBtn.setAttribute("aria-label", "Close fork");
1495
+ exitBtn.setAttribute("title", "Close");
1496
+ exitBtn.innerHTML = `<svg viewBox="0 0 16 16"><line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/></svg>`;
1497
+ exitBtn.addEventListener("click", () => {
1498
+ this.handleForkExit();
1499
+ });
1500
+ toolbar.appendChild(minimizeBtn);
1501
+ toolbar.appendChild(exitBtn);
1502
+ return toolbar;
1503
+ }
1504
+ handleForkMinimize() {
1505
+ if (!this.currentFork)
1506
+ return;
1507
+ if (this.options.forkDisplayMode === "side-by-side") {
1508
+ this.splitPercentage = 100;
1509
+ this.updateSideBySideVisibility();
1510
+ }
1511
+ else {
1512
+ // Dialog mode: hide dialog, put fork back in queue so expand button shows
1513
+ this.forkQueue.unshift(this.currentFork);
1514
+ this.currentFork = null;
1515
+ this.updateDialogVisibility();
1516
+ }
1517
+ }
1518
+ handleForkExit() {
1519
+ if (!this.currentFork)
1520
+ return;
1521
+ const fork = this.currentFork.fork;
1522
+ // Stop the fork via the API (best-effort, just like Run.tsx/ClientWindow.tsx)
1523
+ if (this.sessionToken) {
1524
+ this.client.stop(this.sessionToken, fork).catch(() => { });
1525
+ }
1526
+ // Remove from queue immediately (user-dismissable, same as ClientWindow.tsx)
1527
+ this.forkQueue = this.forkQueue.filter((item) => item.fork !== fork);
1528
+ this.currentFork = null;
1529
+ if (this.forkQueue.length > 0) {
1530
+ this.activateNextFork();
1531
+ }
1532
+ else {
1533
+ // Clear the fork iframe
1534
+ if (this.forkIframe) {
1535
+ this.forkIframe.src = "about:blank";
1536
+ this.forkIframe.classList.remove("mb-adapt__iframe--visible");
1537
+ this.forkIframe.classList.add("mb-adapt__iframe--hidden");
1538
+ }
1539
+ if (this.options.forkDisplayMode === "side-by-side") {
1540
+ this.updateSideBySideVisibility();
1541
+ }
1542
+ else {
1543
+ this.updateDialogVisibility();
1544
+ }
1545
+ }
1546
+ }
1318
1547
  createDialogStructure() {
1319
1548
  if (this.mainContainer)
1320
1549
  return;
@@ -1342,14 +1571,10 @@ export class AdaptWebClient {
1342
1571
  this.dialogBackdrop = document.createElement("div");
1343
1572
  this.dialogBackdrop.className =
1344
1573
  this.options.classNames?.dialogBackdrop || "mb-adapt__dialog-backdrop";
1345
- // Set up backdrop click handler if enabled
1574
+ // Set up backdrop click handler if enabled (acts as minimize)
1346
1575
  if (this.options.dialogBackdropClose) {
1347
1576
  this.backdropClickHandler = () => {
1348
- if (this.currentFork) {
1349
- this.forkQueue.unshift(this.currentFork);
1350
- }
1351
- this.currentFork = null;
1352
- this.updateDialogVisibility();
1577
+ this.handleForkMinimize();
1353
1578
  };
1354
1579
  this.dialogBackdrop.addEventListener("click", this.backdropClickHandler);
1355
1580
  }
@@ -1357,28 +1582,13 @@ export class AdaptWebClient {
1357
1582
  this.dialogElement = document.createElement("div");
1358
1583
  this.dialogElement.className =
1359
1584
  this.options.classNames?.dialog || "mb-adapt__dialog";
1360
- const banner = document.createElement("div");
1361
- banner.className =
1362
- this.options.classNames?.dialogBanner || "mb-adapt__dialog-banner";
1363
- const closeBtn = document.createElement("button");
1364
- closeBtn.className =
1365
- this.options.classNames?.dialogClose || "mb-adapt__dialog-close";
1366
- closeBtn.innerHTML = "×";
1367
- closeBtn.setAttribute("aria-label", "Close dialog");
1368
- closeBtn.setAttribute("title", "Close dialog");
1369
- closeBtn.addEventListener("click", () => {
1370
- // Put current fork back in queue before closing
1371
- if (this.currentFork) {
1372
- this.forkQueue.unshift(this.currentFork);
1373
- }
1374
- this.currentFork = null;
1375
- this.updateDialogVisibility();
1376
- });
1377
- banner.appendChild(closeBtn);
1378
- this.dialogElement.appendChild(banner);
1585
+ // Same toolbar as side-by-side (minimize + exit)
1586
+ this.forkToolbarElement = this.createForkToolbar();
1587
+ this.dialogElement.appendChild(this.forkToolbarElement);
1379
1588
  const iframeContainer = document.createElement("div");
1380
1589
  iframeContainer.style.flex = "1";
1381
- iframeContainer.style.overflow = "hidden";
1590
+ iframeContainer.style.overflow = "auto";
1591
+ iframeContainer.style.minHeight = "0";
1382
1592
  // Reuse existing fork iframe or create new one
1383
1593
  if (!this.forkIframe) {
1384
1594
  this.forkIframe = this.createHiddenIframe();
@@ -1430,6 +1640,23 @@ export class AdaptWebClient {
1430
1640
  });
1431
1641
  return iframe;
1432
1642
  }
1643
+ showStoppedPlaceholder() {
1644
+ if (!this.mainFrameElement && !this.mainContainer)
1645
+ return;
1646
+ // Remove existing placeholder if any
1647
+ this.removeStoppedPlaceholder();
1648
+ this.stoppedPlaceholder = document.createElement("div");
1649
+ this.stoppedPlaceholder.className = "mb-adapt__stopped-placeholder";
1650
+ this.stoppedPlaceholder.textContent = "This session has been stopped";
1651
+ const parent = this.mainFrameElement || this.mainContainer;
1652
+ parent.appendChild(this.stoppedPlaceholder);
1653
+ }
1654
+ removeStoppedPlaceholder() {
1655
+ if (this.stoppedPlaceholder) {
1656
+ this.stoppedPlaceholder.remove();
1657
+ this.stoppedPlaceholder = null;
1658
+ }
1659
+ }
1433
1660
  showIframe(iframe) {
1434
1661
  iframe.classList.remove("mb-adapt__iframe--hidden");
1435
1662
  // Trigger reflow to ensure transition works