@seatlayer/js 0.13.0 → 0.15.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/dist/index.js CHANGED
@@ -52,6 +52,12 @@ var PubApi = class {
52
52
  body: { qty, ...categoryKey ? { categoryKey } : {} }
53
53
  });
54
54
  }
55
+ resume(key, holdId) {
56
+ return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold/resume`, {
57
+ method: "POST",
58
+ body: { holdId }
59
+ });
60
+ }
55
61
  release(key, labels, holdId) {
56
62
  return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {
57
63
  method: "POST",
@@ -108,6 +114,7 @@ var SeatingChart = class {
108
114
  currency: options.currency,
109
115
  onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),
110
116
  onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
117
+ onHoldRestored: (h) => this.opts.onHoldRestored?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
111
118
  onHoldExpired: () => this.opts.onHoldExpired?.(),
112
119
  onGAClick: (areaId) => {
113
120
  const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);
@@ -211,6 +218,21 @@ var SeatingChart = class {
211
218
  return null;
212
219
  }
213
220
  }
221
+ /** Restore an active hold by its opaque id without extending its expiry. */
222
+ async resumeHold(holdId) {
223
+ try {
224
+ const h = await this.controller.resumeHold(holdId);
225
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
226
+ } catch (err) {
227
+ this.opts.onError?.(err);
228
+ return null;
229
+ }
230
+ }
231
+ /** Current active hold known to this chart, if any. */
232
+ getCurrentHold() {
233
+ const h = this.controller.currentHold();
234
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
235
+ }
214
236
  getGAAreas() {
215
237
  return this.controller.getGAAreas();
216
238
  }
@@ -279,6 +301,10 @@ var SeatingChart = class {
279
301
  async release() {
280
302
  await this.controller.release();
281
303
  }
304
+ /** Release selected labels from the current hold while keeping the remainder. */
305
+ async releaseLabels(labels) {
306
+ return this.controller.releaseLabels(labels);
307
+ }
282
308
  /** Tear everything down: close the socket, stop timers, drop the canvas. */
283
309
  destroy() {
284
310
  if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
@@ -434,8 +460,12 @@ var CSS = `
434
460
  .sl-head-meta{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);margin-top:3px;
435
461
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
436
462
  .sl-hold-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
437
- background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums}
438
- .sl-hold-pill.on{display:inline-flex}
463
+ background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums;
464
+ transform-origin:right center}
465
+ .sl-hold-pill.on{display:inline-flex;animation:slPillIn .34s cubic-bezier(.2,.8,.2,1) both}
466
+ .sl-hold-dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.78;box-shadow:0 0 0 0 currentColor}
467
+ .sl-hold-pill.is-expiring .sl-hold-dot{animation:slHoldPulse 1.4s ease-out infinite}
468
+ .sl-hold-time{min-width:3.35em;text-align:left}
439
469
  .sl-close{width:32px;height:32px;border-radius:999px;flex:none;display:none;align-items:center;justify-content:center;
440
470
  border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
441
471
  .sl-close:hover{color:var(--sl-text);border-color:var(--sl-muted)}
@@ -457,11 +487,20 @@ var CSS = `
457
487
  .sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
458
488
  .sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
459
489
  .sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
460
- flex:none;height:50%;transition:height .22s ease}
490
+ flex:none;height:50%;transition:height .3s cubic-bezier(.2,.8,.2,1)}
461
491
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
462
492
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
463
493
  .sl-picker[data-layout="narrow"] .sl-tray{flex:none}
464
494
  .sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
495
+ .sl-picker[data-layout="narrow"] .sl-sheet-head{order:0}
496
+ .sl-picker[data-layout="narrow"] .sl-seats-sec{order:1}
497
+ .sl-picker[data-layout="narrow"] .sl-tray{order:2}
498
+ .sl-picker[data-layout="narrow"] .sl-filtersec{order:3}
499
+ .sl-picker[data-layout="narrow"] .sl-filters{order:4}
500
+ .sl-picker[data-layout="narrow"] .sl-prices-sec{order:5}
501
+ .sl-picker[data-layout="narrow"] .sl-pricef{order:6}
502
+ .sl-picker[data-layout="narrow"] .sl-prices{order:7}
503
+ .sl-picker[data-layout="narrow"] .sl-foot{order:8}
465
504
  /* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
466
505
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
467
506
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
@@ -508,15 +547,27 @@ var CSS = `
508
547
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
509
548
  .sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
510
549
  .sl-price-amt{font-weight:800;font-variant-numeric:tabular-nums}
550
+ .sl-status-key{display:flex;gap:12px;flex-wrap:wrap;padding:8px 16px 12px;border-bottom:1px solid var(--sl-line);color:var(--sl-muted);font-size:11px}
551
+ .sl-status-item{display:inline-flex;align-items:center;gap:6px}
552
+ .sl-status-icon{width:17px;height:17px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
553
+ color:#fff;background:#6b7280;font-size:9px;font-weight:900;line-height:1}
554
+ .sl-status-icon.sold{background:#374151;font-size:14px}
511
555
 
512
556
  /* tray */
513
557
  .sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
514
558
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
515
559
  .sl-chip{display:flex;align-items:center;gap:9px;padding:9px 11px;border:1px solid var(--sl-line);
516
- border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px}
560
+ border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px;transform-origin:center}
561
+ .sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
562
+ .sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
563
+ .sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
564
+ box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
517
565
  .sl-chip b{font-weight:800}
518
566
  .sl-chip .cat{color:var(--sl-muted);font-size:11.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
519
567
  .sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums}
568
+ .sl-chip-state{flex:none;padding:3px 6px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
569
+ font-size:8.5px;line-height:1;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
570
+ .sl-chip-state.sl-pending{background:transparent;color:var(--sl-muted);border:1px solid var(--sl-line)}
520
571
  .sl-chip .rm{width:22px;height:22px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
521
572
  .sl-chip .rm:hover{color:var(--sl-text)}
522
573
  .sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
@@ -534,17 +585,30 @@ var CSS = `
534
585
 
535
586
  /* footer */
536
587
  .sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
588
+ .sl-hold-note{display:none;align-items:center;gap:9px;margin-bottom:10px;padding:9px 10px;border-radius:var(--sl-r-sm);
589
+ border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
590
+ box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
591
+ font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
592
+ .sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
593
+ .sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
594
+ stroke-linecap:round;stroke-linejoin:round}
595
+ .sl-hold-note b{display:block;color:var(--sl-text);font-size:12px}
596
+ .sl-hold-copy{display:block}
537
597
  .sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
538
598
  .sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
599
+ .sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
539
600
  /* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
540
601
  '.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
541
602
  and the button renders as plain text with no accent fill. */
542
603
  .sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
543
604
  padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
544
605
  background:var(--sl-accent);color:var(--sl-accent-ink);
545
- transition:filter .15s,background .15s,color .15s,transform .06s}
606
+ transition:filter .15s,background .22s,color .22s,transform .12s,box-shadow .22s;gap:8px}
546
607
  .sl-picker .sl-cta:hover{filter:brightness(1.08)}
547
608
  .sl-picker .sl-cta:active{transform:translateY(1px);filter:brightness(.94)}
609
+ .sl-picker .sl-cta.sl-ready{animation:slCtaReady .42s cubic-bezier(.2,.8,.2,1)}
610
+ .sl-cta-spin,.sl-ba-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;
611
+ animation:slspin .7s linear infinite;flex:none}
548
612
  /* Disabled ("Select seats"): quieter, but still a full-width button shape. */
549
613
  .sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
550
614
  cursor:not-allowed;filter:none;transform:none}
@@ -582,11 +646,15 @@ var CSS = `
582
646
  .sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
583
647
 
584
648
  /* toast + boot states (toast flows in the bottom-center region) */
585
- .sl-toast{transform:translateY(6px);max-width:100%;
649
+ .sl-toast{transform:translateY(6px) scale(.98);max-width:100%;
586
650
  background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
587
- font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;white-space:nowrap;
651
+ font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
588
652
  overflow:hidden;text-overflow:ellipsis}
589
- .sl-toast.on{opacity:1;transform:translateY(0)}
653
+ .sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
654
+ .sl-toast[data-tone="error"]{border-color:#ef4444}
655
+ .sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
656
+ .sl-toast[data-tone="success"]{border-color:#22c55e}
657
+ .sl-toast.on[data-tone="error"]{animation:slToastNudge .32s ease-out}
590
658
  .sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
591
659
  gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
592
660
  .sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
@@ -610,15 +678,22 @@ var CSS = `
610
678
  .sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
611
679
 
612
680
  /* booked confirmation overlay (covers the widget once the held seats are sold) */
613
- .sl-booked{position:absolute;inset:0;z-index:11;display:none;flex-direction:column;align-items:center;
614
- justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)}
615
- .sl-booked.on{display:flex}
681
+ .sl-booked{position:absolute;inset:0;z-index:11;display:flex;flex-direction:column;align-items:center;
682
+ justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg);opacity:0;visibility:hidden;
683
+ pointer-events:none;transition:opacity .34s ease,visibility 0s linear .34s}
684
+ .sl-booked.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .34s ease,visibility 0s}
616
685
  .sl-booked-badge{width:60px;height:60px;border-radius:999px;display:flex;align-items:center;justify-content:center;
617
- background:var(--sl-accent);color:var(--sl-accent-ink)}
618
- .sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
686
+ background:var(--sl-accent);color:var(--sl-accent-ink);transform:scale(.72)}
687
+ .sl-booked.on .sl-booked-badge{animation:slSuccessPop .58s cubic-bezier(.2,1.25,.3,1) .08s both}
688
+ .sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round;
689
+ stroke-dasharray:30;stroke-dashoffset:30}
690
+ .sl-booked.on .sl-booked-badge svg{animation:slCheckDraw .42s ease-out .32s forwards}
619
691
  .sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
620
692
  .sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
621
693
  .sl-booked-seats{font-weight:700;color:var(--sl-text)}
694
+ .sl-booked.on .sl-booked-title,.sl-booked.on .sl-booked-sub{animation:slCopyRise .42s ease-out both}
695
+ .sl-booked.on .sl-booked-title{animation-delay:.22s}
696
+ .sl-booked.on .sl-booked-sub{animation-delay:.3s}
622
697
 
623
698
  /* a11y filter chips (flow within the top-left region) */
624
699
  .sl-chips{display:flex;gap:6px;flex-wrap:wrap}
@@ -627,28 +702,49 @@ var CSS = `
627
702
  .sl-chip-f:hover{color:var(--sl-text)}
628
703
  .sl-chip-f.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
629
704
 
630
- /* confirm popover */
631
- .sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
632
- border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px))}
633
- .sl-confirm-label{font-weight:800;font-size:14px}
634
- .sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
635
- .sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
705
+ /* confirm card: a candidate is not in the tray until Select. Map gestures and
706
+ floating chrome pause while the card owns focus, keeping the camera stable. */
707
+ .sl-picker[data-confirming="true"] .sl-map-host>:not(.sl-confirm){pointer-events:none}
708
+ .sl-picker[data-confirming="true"] .sl-anchor{pointer-events:none;opacity:.28;transition:opacity .16s}
709
+ .sl-picker[data-confirming="true"] .sl-side{pointer-events:none;opacity:.58;transition:opacity .16s}
710
+ .sl-confirm{position:absolute;z-index:10;width:276px;max-width:calc(100% - 24px);overflow:hidden;pointer-events:auto;
711
+ background:var(--sl-surface);border:1px solid color-mix(in srgb,var(--sl-line) 70%,var(--sl-text));
712
+ border-radius:15px;box-shadow:0 24px 64px -18px rgba(0,0,0,.72);transform:translate(-50%,calc(-100% - 16px));
713
+ animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
714
+ .sl-confirm[data-placement="below"]{transform:translate(-50%,16px);animation:slConfirmBelowIn .24s cubic-bezier(.2,.8,.2,1) both}
715
+ .sl-confirm-grid{display:grid;grid-template-columns:1.2fr .8fr .8fr;border-bottom:1px solid var(--sl-line)}
716
+ .sl-confirm-field{min-width:0;padding:12px 11px 10px;border-right:1px solid var(--sl-line)}
717
+ .sl-confirm-field:last-child{border-right:0;text-align:center}
718
+ .sl-confirm-field:nth-child(2){text-align:center}
719
+ .sl-confirm-key{display:block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
720
+ .sl-confirm-value{display:block;margin-top:4px;color:var(--sl-text);font-size:17px;line-height:1.1;font-weight:850;
721
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
722
+ .sl-confirm-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;background:color-mix(in srgb,var(--sl-cat) 76%,var(--sl-surface))}
723
+ .sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
724
+ .sl-confirm-cat-name{font-size:13.5px;font-weight:800;color:#fff;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
725
+ .sl-confirm-price{font-size:17px;font-weight:850;color:#fff;font-variant-numeric:tabular-nums}
726
+ .sl-confirm-body{padding:11px 12px 12px}
636
727
  .sl-confirm-row{display:flex;gap:8px;margin-top:10px}
637
- .sl-confirm-row button{flex:1;padding:8px;border-radius:8px;font-weight:700;font-size:12.5px}
638
- .sl-confirm-add{background:var(--sl-accent);color:var(--sl-accent-ink)}
639
- .sl-confirm-cancel{border:1px solid var(--sl-line);color:var(--sl-muted)}
728
+ .sl-confirm-row button{flex:1;min-height:44px;padding:9px 12px;border-radius:9px;font-weight:800;font-size:13px}
729
+ .sl-confirm-add{background:var(--sl-accent)!important;color:var(--sl-accent-ink)!important;display:flex;align-items:center;justify-content:center;gap:7px}
730
+ .sl-confirm-add svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
731
+ .sl-confirm-cancel{background:color-mix(in srgb,var(--sl-line) 44%,transparent)!important;border:1px solid var(--sl-line)!important;color:var(--sl-muted)!important}
640
732
  .sl-confirm-cancel:hover{color:var(--sl-text)}
733
+ .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
734
+ transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
641
735
 
642
736
  /* best-available row */
643
- .sl-ba{display:flex;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
737
+ .sl-ba{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
644
738
  .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
645
- font:inherit;font-size:12px;padding:5px 6px;max-width:110px}
739
+ font:inherit;font-size:12px;padding:7px 8px;min-width:0;width:100%;max-width:none}
646
740
  .sl-ba-qty{display:flex;align-items:center;gap:7px}
647
741
  .sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
648
742
  font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
649
743
  .sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
650
- .sl-ba-go{margin-left:auto;padding:7px 12px;border-radius:999px;border:1px solid var(--sl-line);font-weight:700;font-size:12px;transition:border-color .15s}
744
+ .sl-ba-go{grid-column:1/-1;width:100%;min-height:38px;padding:7px 12px;border-radius:9px;border:1px solid var(--sl-line);font-weight:800;font-size:12px;
745
+ transition:border-color .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:6px}
651
746
  .sl-ba-go:hover{border-color:var(--sl-muted)}
747
+ .sl-ba-go:disabled{opacity:.62;cursor:wait}
652
748
 
653
749
  /* screen-reader live region */
654
750
  .sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
@@ -754,6 +850,29 @@ var CSS = `
754
850
  .sl-price-row.sl-dim{opacity:.4}
755
851
  .sl-seccard-mix-item.sl-dim{opacity:.4}
756
852
 
853
+ /* Buyer-journey motion: every animation explains a state transition (selected,
854
+ held, checkout handoff, conflict or booked). No decorative infinite motion
855
+ except the expiring-hold pulse and active progress spinners. */
856
+ @keyframes slPillIn{from{opacity:0;transform:translateX(7px) scale(.9)}to{opacity:1;transform:translateX(0) scale(1)}}
857
+ @keyframes slHoldPulse{0%{box-shadow:0 0 0 0 currentColor;opacity:.9}75%,100%{box-shadow:0 0 0 7px transparent;opacity:.55}}
858
+ @keyframes slChipIn{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
859
+ @keyframes slChipOut{to{opacity:0;transform:translateX(10px) scale(.98)}}
860
+ @keyframes slNoticeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
861
+ @keyframes slValuePop{0%{opacity:.6;transform:translateY(3px)}55%{transform:translateY(-1px) scale(1.05)}100%{opacity:1;transform:none}}
862
+ @keyframes slCtaReady{0%{transform:scale(.98);box-shadow:0 0 0 0 transparent}55%{transform:scale(1.01);box-shadow:0 0 0 5px color-mix(in srgb,var(--sl-accent) 18%,transparent)}100%{transform:none;box-shadow:none}}
863
+ @keyframes slToastNudge{0%,100%{margin-left:0}30%{margin-left:-4px}60%{margin-left:3px}}
864
+ @keyframes slSuccessPop{0%{opacity:0;transform:scale(.72)}65%{opacity:1;transform:scale(1.08)}100%{opacity:1;transform:scale(1)}}
865
+ @keyframes slCheckDraw{to{stroke-dashoffset:0}}
866
+ @keyframes slCopyRise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
867
+ @keyframes slConfirmIn{from{opacity:0;transform:translate(-50%,calc(-100% - 8px)) scale(.96)}to{opacity:1;transform:translate(-50%,calc(-100% - 14px)) scale(1)}}
868
+ @keyframes slConfirmBelowIn{from{opacity:0;transform:translate(-50%,8px) scale(.96)}to{opacity:1;transform:translate(-50%,16px) scale(1)}}
869
+ @keyframes slConfirmMobileIn{from{opacity:0;transform:translate(-50%,10px) scale(.97)}to{opacity:1;transform:translate(-50%,0) scale(1)}}
870
+
871
+ @media(prefers-reduced-motion:reduce){
872
+ .sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
873
+ transition-duration:.001ms!important;scroll-behavior:auto!important}
874
+ }
875
+
757
876
  /* modal host */
758
877
  .sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
759
878
  .sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
@@ -794,6 +913,8 @@ var SeatPicker = class _SeatPicker {
794
913
  this.ro = null;
795
914
  this.holdTimer = null;
796
915
  this.toastTimer = null;
916
+ /** Short-lived UI motion timers; all are cancelled on destroy. */
917
+ this.motionTimers = /* @__PURE__ */ new Set();
797
918
  // state
798
919
  this.currency = "USD";
799
920
  this.hold = null;
@@ -837,6 +958,15 @@ var SeatPicker = class _SeatPicker {
837
958
  this.secCardShownAt = 0;
838
959
  /** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
839
960
  this.lastTrayCount = 0;
961
+ /** Previous computed total — drives a single explanatory value bump. */
962
+ this.lastTrayTotal = 0;
963
+ /** Stable item keys prevent tray chips re-animating on unrelated realtime syncs. */
964
+ this.lastTrayKeys = /* @__PURE__ */ new Set();
965
+ this.bestAvailableBusy = false;
966
+ this.releasingLabels = /* @__PURE__ */ new Set();
967
+ /** Selected labels awaiting the hold response; their own realtime echo can arrive first. */
968
+ this.holdingLabels = /* @__PURE__ */ new Set();
969
+ this.ctaPhase = "idle";
840
970
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
841
971
  this.a11yChipsEl = null;
842
972
  this.cbEl = null;
@@ -849,10 +979,11 @@ var SeatPicker = class _SeatPicker {
849
979
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
850
980
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
851
981
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
852
- this.opts = options;
853
- const api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
982
+ this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
983
+ this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
984
+ this.api = new PubApi(this.apiBase);
854
985
  this.controller = new PickerController2({
855
- transport: api,
986
+ transport: this.api,
856
987
  eventKey: options.event,
857
988
  maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
858
989
  currency: options.currency,
@@ -860,7 +991,7 @@ var SeatPicker = class _SeatPicker {
860
991
  colorblindSafe: options.colorblindSafe,
861
992
  onSelectionChange: () => {
862
993
  this.syncTray();
863
- if (this.controller.getSelection().length) this.collapseSectionCard();
994
+ if (this.committedSelection().length) this.collapseSectionCard();
864
995
  },
865
996
  onStatusChange: () => {
866
997
  this.syncPrices();
@@ -870,18 +1001,25 @@ var SeatPicker = class _SeatPicker {
870
1001
  },
871
1002
  onHoldExpired: () => {
872
1003
  this.hold = null;
1004
+ this.forgetHold();
873
1005
  this.handedOff = false;
874
1006
  this.bookedShown = false;
1007
+ this.ctaPhase = "idle";
875
1008
  this.stopHoldTimer();
876
1009
  this.gaQty.clear();
877
- this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
1010
+ this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.", "warning");
878
1011
  this.syncTray();
1012
+ this.emitHoldChange();
879
1013
  this.opts.onHoldExpired?.();
880
1014
  },
881
- confirmSelection: options.confirmSelection,
1015
+ confirmSelection: this.opts.confirmSelection,
882
1016
  onSelect: (seat) => {
1017
+ this.flashPickedSeat(seat.id);
883
1018
  if (this.opts.confirmSelection) this.showConfirm(seat);
884
1019
  },
1020
+ onDeselect: (seat) => {
1021
+ if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
1022
+ },
885
1023
  onViewChange: () => {
886
1024
  this.reanchorConfirm();
887
1025
  this.syncRung();
@@ -921,17 +1059,32 @@ var SeatPicker = class _SeatPicker {
921
1059
  const picker = new _SeatPicker({ ...options, container: frame });
922
1060
  picker.modalScrim = scrim;
923
1061
  picker.prevFocus = document.activeElement;
1062
+ let closing = false;
924
1063
  const close = () => {
1064
+ if (closing) return;
1065
+ closing = true;
925
1066
  document.body.style.overflow = prevOverflow;
926
- picker.destroy();
927
- options.onClose?.();
1067
+ scrim.style.opacity = "0";
1068
+ scrim.style.pointerEvents = "none";
1069
+ const finish = () => {
1070
+ picker.destroy();
1071
+ options.onClose?.();
1072
+ };
1073
+ if (picker.hold && !picker.handedOff) void picker.release().finally(finish);
1074
+ else finish();
928
1075
  };
929
1076
  picker.closeModal = close;
930
1077
  scrim.addEventListener("mousedown", (e) => {
931
1078
  if (e.target === scrim) close();
932
1079
  });
933
1080
  picker.escHandler = (e) => {
934
- if (e.key === "Escape") close();
1081
+ if (e.key !== "Escape") return;
1082
+ if (picker.confirmSeat) {
1083
+ e.preventDefault();
1084
+ picker.cancelConfirm();
1085
+ } else {
1086
+ close();
1087
+ }
935
1088
  };
936
1089
  document.addEventListener("keydown", picker.escHandler);
937
1090
  await picker.render();
@@ -948,8 +1101,15 @@ var SeatPicker = class _SeatPicker {
948
1101
  const mount = resolveContainer3(this.opts.container);
949
1102
  const root = document.createElement("div");
950
1103
  root.className = "sl-picker";
1104
+ root.tabIndex = -1;
951
1105
  this.root = root;
952
1106
  mount.appendChild(root);
1107
+ root.addEventListener("keydown", (e) => {
1108
+ if (e.key !== "Escape" || !this.confirmSeat) return;
1109
+ e.preventDefault();
1110
+ e.stopPropagation();
1111
+ this.cancelConfirm();
1112
+ });
953
1113
  Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
954
1114
  root.innerHTML = `
955
1115
  <div class="sl-head">
@@ -988,11 +1148,15 @@ var SeatPicker = class _SeatPicker {
988
1148
  </div>
989
1149
  <div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
990
1150
  <div class="sl-filters" data-ref="filters"></div>
991
- <div class="sl-sec" data-ref="pricesSec">Prices</div>
1151
+ <div class="sl-sec sl-prices-sec" data-ref="pricesSec">Prices</div>
992
1152
  <div class="sl-prices" data-ref="prices"></div>
993
- <div class="sl-sec">Your seats</div>
1153
+ <div class="sl-sec sl-seats-sec">Your seats</div>
994
1154
  <div class="sl-tray" data-ref="tray"></div>
995
1155
  <div class="sl-foot">
1156
+ <div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
1157
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
1158
+ <span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
1159
+ </div>
996
1160
  <div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
997
1161
  <button type="button" class="sl-cta" data-ref="cta" disabled></button>
998
1162
  </div>
@@ -1144,6 +1308,8 @@ var SeatPicker = class _SeatPicker {
1144
1308
  this.buildExtendPrompt();
1145
1309
  this.buildBookedOverlay();
1146
1310
  this.dockLayoutChrome();
1311
+ await this.restoreRememberedHold();
1312
+ if (this.destroyed) return this;
1147
1313
  this.syncPrices();
1148
1314
  this.syncTray();
1149
1315
  return this;
@@ -1219,6 +1385,156 @@ var SeatPicker = class _SeatPicker {
1219
1385
  cssVar(name) {
1220
1386
  return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
1221
1387
  }
1388
+ /** Motion is progressive enhancement; all state remains legible when reduced. */
1389
+ reducedMotion() {
1390
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1391
+ }
1392
+ scheduleMotion(fn, delay) {
1393
+ const timer = setTimeout(() => {
1394
+ this.motionTimers.delete(timer);
1395
+ if (!this.destroyed) fn();
1396
+ }, delay);
1397
+ this.motionTimers.add(timer);
1398
+ }
1399
+ /** Restart one finite CSS animation without leaving a permanent state class. */
1400
+ animateOnce(el, className, duration = 600) {
1401
+ if (!el || this.reducedMotion()) return;
1402
+ el.classList.remove(className);
1403
+ void el.offsetWidth;
1404
+ el.classList.add(className);
1405
+ this.scheduleMotion(() => el.classList.remove(className), duration);
1406
+ }
1407
+ /** Selection feedback belongs on the selected seat, not across the whole map. */
1408
+ flashPickedSeat(id) {
1409
+ if (this.reducedMotion()) return;
1410
+ this.controller.flashSeat(id, this.cssVar("--sl-accent") || "#f4b740");
1411
+ }
1412
+ /** A completed hold gets one short map ripple per concrete seat. */
1413
+ flashHeldSeats(hold) {
1414
+ if (this.reducedMotion()) return;
1415
+ const labels = (hold.items ?? []).filter((item) => item.objectType !== "ga").map((item) => item.label);
1416
+ labels.slice(0, 10).forEach((label, index) => {
1417
+ const seat = this.controller.seatByLabel(label);
1418
+ if (!seat) return;
1419
+ this.scheduleMotion(
1420
+ () => this.controller.flashSeat(seat.id, this.cssVar("--sl-accent") || "#f4b740"),
1421
+ index * 55
1422
+ );
1423
+ });
1424
+ }
1425
+ /** Update only the action affordance; selection callbacks must not refire. */
1426
+ committedSelection() {
1427
+ const candidateId = this.confirmSeat?.id;
1428
+ return this.controller.getSelection().filter((seat) => seat.id !== candidateId);
1429
+ }
1430
+ pendingSelectionCount() {
1431
+ const heldItems = this.hold?.items ?? [];
1432
+ const heldLabels = new Set(heldItems.map((item) => item.label));
1433
+ const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1434
+ const heldGA = /* @__PURE__ */ new Map();
1435
+ for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
1436
+ heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1437
+ }
1438
+ const pendingGA = [...this.gaQty.entries()].reduce(
1439
+ (sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
1440
+ 0
1441
+ );
1442
+ return pendingSeats + pendingGA;
1443
+ }
1444
+ syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1445
+ const cta = this.els.cta;
1446
+ if (!cta) return;
1447
+ if (this.confirmSeat) {
1448
+ cta.disabled = true;
1449
+ cta.textContent = "Confirm or cancel this seat";
1450
+ return;
1451
+ }
1452
+ if (this.ctaPhase === "holding") {
1453
+ cta.disabled = true;
1454
+ cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
1455
+ return;
1456
+ }
1457
+ if (this.ctaPhase === "checkout") {
1458
+ cta.disabled = true;
1459
+ cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Opening checkout\u2026';
1460
+ return;
1461
+ }
1462
+ cta.disabled = count === 0;
1463
+ cta.textContent = this.hold ? pending ? `Secure ${pending} more & checkout` : "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
1464
+ }
1465
+ setCtaPhase(phase) {
1466
+ this.ctaPhase = phase;
1467
+ this.syncCta();
1468
+ if (phase === "checkout") {
1469
+ this.scheduleMotion(() => {
1470
+ if (this.ctaPhase !== "checkout") return;
1471
+ this.ctaPhase = "idle";
1472
+ this.syncCta();
1473
+ }, 1100);
1474
+ }
1475
+ }
1476
+ /** Session-scoped capability key: isolated by API origin and event. */
1477
+ holdStorageKey() {
1478
+ return `@seatlayer/hold/v1/${encodeURIComponent(this.apiBase)}/${encodeURIComponent(this.opts.event)}`;
1479
+ }
1480
+ rememberedHoldId() {
1481
+ if (this.opts.initialHoldId) return this.opts.initialHoldId;
1482
+ if (this.opts.restoreHold === false || typeof window === "undefined") return null;
1483
+ try {
1484
+ return window.sessionStorage.getItem(this.holdStorageKey());
1485
+ } catch {
1486
+ return null;
1487
+ }
1488
+ }
1489
+ rememberHold(hold) {
1490
+ if (this.opts.restoreHold === false || typeof window === "undefined") return;
1491
+ try {
1492
+ window.sessionStorage.setItem(this.holdStorageKey(), hold.holdId);
1493
+ } catch {
1494
+ }
1495
+ }
1496
+ forgetHold() {
1497
+ if (typeof window === "undefined") return;
1498
+ try {
1499
+ window.sessionStorage.removeItem(this.holdStorageKey());
1500
+ } catch {
1501
+ }
1502
+ }
1503
+ async resumeHoldFromServer(holdId, automatic) {
1504
+ try {
1505
+ const h = await this.controller.resumeHold(holdId);
1506
+ if (!h) return null;
1507
+ const restored = {
1508
+ holdId: h.holdId,
1509
+ expiresAt: h.expiresAt,
1510
+ seats: h.seats,
1511
+ items: h.items
1512
+ };
1513
+ this.hold = restored;
1514
+ this.handedOff = true;
1515
+ this.bookedShown = false;
1516
+ this.ctaPhase = "idle";
1517
+ this.startHoldTimer(restored.expiresAt);
1518
+ this.rememberHold(restored);
1519
+ this.syncTray();
1520
+ this.emitHoldChange();
1521
+ this.opts.onHoldRestored?.(restored, restored.seats ?? [], this.buildHandoff(restored));
1522
+ if (automatic) this.toast("Your held tickets have been restored.", "success");
1523
+ return restored;
1524
+ } catch (error) {
1525
+ const status = error?.status;
1526
+ if (status === 404 || status === 409) {
1527
+ this.forgetHold();
1528
+ } else {
1529
+ this.opts.onError?.(error);
1530
+ }
1531
+ return null;
1532
+ }
1533
+ }
1534
+ async restoreRememberedHold() {
1535
+ const holdId = this.rememberedHoldId();
1536
+ if (holdId) await this.resumeHoldFromServer(holdId, true);
1537
+ }
1222
1538
  /** Section-bearing objects on the active floor (single-floor → doc.objects). */
1223
1539
  activeFloorObjects() {
1224
1540
  const doc = this.controller.doc;
@@ -1473,7 +1789,11 @@ var SeatPicker = class _SeatPicker {
1473
1789
  (r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
1474
1790
  ).join("");
1475
1791
  pills.querySelectorAll("button").forEach((btn) => {
1476
- btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
1792
+ btn.addEventListener("click", () => {
1793
+ const rung = btn.dataset.rung;
1794
+ this.controller.setRung(rung);
1795
+ if (rung === "seats") this.collapseSectionCard();
1796
+ });
1477
1797
  });
1478
1798
  this.regions["top-center"].appendChild(pills);
1479
1799
  this.rungsEl = pills;
@@ -1524,7 +1844,7 @@ var SeatPicker = class _SeatPicker {
1524
1844
  this.secCardEl?.remove();
1525
1845
  this.secCardEl = null;
1526
1846
  if (!summary) return;
1527
- this.secCardCollapsed = false;
1847
+ this.secCardCollapsed = this.controller.getRung() === "seats";
1528
1848
  this.secCardShownAt = Date.now();
1529
1849
  this.renderSectionCard(summary);
1530
1850
  }
@@ -1590,6 +1910,10 @@ var SeatPicker = class _SeatPicker {
1590
1910
  sectionCardOnView() {
1591
1911
  if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
1592
1912
  if (this.root?.dataset.layout === "narrow") return;
1913
+ if (this.controller.getRung() === "seats") {
1914
+ this.collapseSectionCard();
1915
+ return;
1916
+ }
1593
1917
  if (Date.now() - this.secCardShownAt < 1400) {
1594
1918
  if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
1595
1919
  return;
@@ -1632,36 +1956,78 @@ var SeatPicker = class _SeatPicker {
1632
1956
  const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
1633
1957
  this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
1634
1958
  }
1635
- // ---- confirm popover (opt-in confirmSelection mode) ------------------------
1959
+ // ---- seat candidate confirmation ------------------------------------------
1636
1960
  showConfirm(seat) {
1637
- this.closeConfirm();
1961
+ const previousId = this.confirmSeat?.id;
1962
+ this.confirmEl?.remove();
1963
+ this.confirmEl = null;
1964
+ this.confirmSeat = seat;
1965
+ this.root?.setAttribute("data-confirming", "true");
1966
+ this.controller.setSelectionFocus(seat.id);
1967
+ if (previousId && previousId !== seat.id) this.controller.deselect([previousId]);
1638
1968
  if (this.tipEl) this.tipEl.style.display = "none";
1969
+ const details = this.controller.seatDetails(seat.id);
1639
1970
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
1640
- const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
1971
+ const price = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
1972
+ const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
1973
+ "&": "&amp;",
1974
+ "<": "&lt;",
1975
+ ">": "&gt;",
1976
+ '"': "&quot;"
1977
+ })[char]);
1641
1978
  const el = document.createElement("div");
1642
1979
  el.className = "sl-confirm";
1643
- el.innerHTML = `<div class="sl-confirm-label">${seat.label}</div><div class="sl-confirm-meta"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span>${cat?.label ?? seat.categoryKey}${price != null ? `<b>${this.money(price)}</b>` : ""}</div>` + (this.seatViewEnabled() ? `<button type="button" class="sl-confirm-view"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>${t2("picker.open360")}</button>` : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add">Add seat</button></div>`;
1980
+ el.setAttribute("role", "dialog");
1981
+ el.setAttribute("aria-modal", "true");
1982
+ el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
1983
+ el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
1984
+ el.innerHTML = `<div class="sl-confirm-grid"><div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details?.sectionLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Row</span><span class="sl-confirm-value">${safe(details?.rowLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? seat.label)}</span></div></div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + (this.seatViewEnabled() ? `<button type="button" class="sl-confirm-view"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>${t2("picker.open360")}</button>` : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
1644
1985
  this.els.map.appendChild(el);
1645
1986
  this.confirmEl = el;
1646
- this.confirmSeat = seat;
1647
1987
  this.reanchorConfirm();
1648
1988
  el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
1649
- el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
1650
- el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
1651
- this.controller.deselect([seat.id]);
1652
- this.closeConfirm();
1653
- });
1989
+ el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
1990
+ el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
1991
+ requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
1654
1992
  }
1655
1993
  reanchorConfirm() {
1656
1994
  if (!this.confirmEl || !this.confirmSeat) return;
1657
1995
  const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
1658
- this.confirmEl.style.left = `${p.x}px`;
1659
- this.confirmEl.style.top = `${p.y}px`;
1660
- }
1661
- closeConfirm() {
1996
+ if (this.root?.dataset.layout === "narrow") return;
1997
+ const mapWidth = this.els.map.clientWidth;
1998
+ const mapHeight = this.els.map.clientHeight;
1999
+ const cardWidth = this.confirmEl.offsetWidth || 276;
2000
+ const cardHeight = this.confirmEl.offsetHeight || 230;
2001
+ const half = cardWidth / 2 + 12;
2002
+ const x = Math.max(half, Math.min(mapWidth - half, p.x));
2003
+ const belowFits = p.y + cardHeight + 24 <= mapHeight;
2004
+ const placeBelow = p.y < cardHeight + 24 && belowFits;
2005
+ this.confirmEl.dataset.placement = placeBelow ? "below" : "above";
2006
+ this.confirmEl.style.left = `${x}px`;
2007
+ this.confirmEl.style.top = `${Math.max(8, Math.min(mapHeight - 8, p.y))}px`;
2008
+ }
2009
+ dismissConfirm() {
1662
2010
  this.confirmEl?.remove();
1663
2011
  this.confirmEl = null;
1664
2012
  this.confirmSeat = null;
2013
+ this.root?.removeAttribute("data-confirming");
2014
+ this.controller.setSelectionFocus(null);
2015
+ }
2016
+ commitConfirm() {
2017
+ if (!this.confirmSeat) return;
2018
+ this.dismissConfirm();
2019
+ this.collapseSectionCard();
2020
+ this.syncTray();
2021
+ }
2022
+ cancelConfirm() {
2023
+ const seat = this.confirmSeat;
2024
+ if (!seat) return;
2025
+ this.controller.deselect([seat.id]);
2026
+ if (this.confirmSeat) this.dismissConfirm();
2027
+ this.root?.focus({ preventScroll: true });
2028
+ }
2029
+ closeConfirm() {
2030
+ this.dismissConfirm();
1665
2031
  }
1666
2032
  // ---- 360° view-from-seat modal --------------------------------------------
1667
2033
  seatViewEnabled() {
@@ -1684,7 +2050,6 @@ var SeatPicker = class _SeatPicker {
1684
2050
  openSeatView(seat) {
1685
2051
  if (!this.root || !this.seatViewEnabled()) return;
1686
2052
  this.closeSeatView();
1687
- this.closeConfirm();
1688
2053
  const doc = this.controller.doc;
1689
2054
  const activeId = this.controller.getActiveFloorId();
1690
2055
  const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
@@ -1795,7 +2160,7 @@ var SeatPicker = class _SeatPicker {
1795
2160
  const price = c.tiers?.length ? c.tiers[0].price : c.price;
1796
2161
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
1797
2162
  return `<div class="sl-price-row${dim ? " sl-dim" : ""}" data-cat="${c.key}"><span class="sl-dot" style="background:${c.color}"></span><span class="sl-price-label">${c.label}</span><span class="sl-price-left">${left[c.key] ?? 0} left</span>` + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
1798
- }).join("");
2163
+ }).join("") + `<div class="sl-status-key" aria-label="Seat status legend"><span class="sl-status-item"><i class="sl-status-icon">H</i>Held by another buyer</span><span class="sl-status-item"><i class="sl-status-icon sold">\xD7</i>Sold</span></div>`;
1799
2164
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
1800
2165
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
1801
2166
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
@@ -1803,39 +2168,47 @@ var SeatPicker = class _SeatPicker {
1803
2168
  }
1804
2169
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
1805
2170
  evictTakenSelections() {
1806
- const ownLabels = new Set(this.controller.currentHold()?.labels ?? []);
2171
+ const ownLabels = /* @__PURE__ */ new Set([
2172
+ ...this.controller.currentHold()?.labels ?? [],
2173
+ ...this.holdingLabels
2174
+ ]);
1807
2175
  const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
1808
2176
  if (!gone.length) return;
1809
2177
  this.controller.deselect(gone.map((s) => s.id));
1810
- this.toast(`Seat ${gone[0].label} was just taken by another buyer.`);
2178
+ this.toast(`Seat ${gone[0].label} was just taken by another buyer.`, "error");
1811
2179
  }
1812
2180
  syncTray() {
1813
2181
  if (!this.els.tray) return;
1814
- const seats = this.controller.getSelection();
2182
+ const seats = this.committedSelection();
1815
2183
  const gaAreas = this.controller.getGAAreas();
1816
2184
  const heldItems = this.hold?.items ?? [];
1817
2185
  const parts = [];
2186
+ const nextTrayKeys = /* @__PURE__ */ new Set();
1818
2187
  if (!seats.length && !heldItems.length && !gaAreas.length) {
1819
2188
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
1820
2189
  } else if (!seats.length && !heldItems.length) {
1821
2190
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
1822
2191
  }
1823
2192
  for (const item of heldItems) {
2193
+ const itemKey = `held:${item.label}`;
2194
+ nextTrayKeys.add(itemKey);
1824
2195
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
1825
2196
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
1826
2197
  const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
1827
2198
  parts.push(
1828
- `<div class="sl-chip"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span>` + (canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${t2("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`
2199
+ `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="sl-chip-state">Held by you</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span>` + (canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${t2("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `<button type="button" class="rm" aria-label="Remove held ticket ${item.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
1829
2200
  );
1830
2201
  }
1831
2202
  const heldLabels = new Set(heldItems.map((item) => item.label));
1832
2203
  const canView = this.seatViewEnabled();
1833
2204
  for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
2205
+ const itemKey = `seat:${s.id}`;
2206
+ nextTrayKeys.add(itemKey);
1834
2207
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
1835
2208
  const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
1836
2209
  const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${t2("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
1837
2210
  parts.push(
1838
- `<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span>` + tierSelect + `<span class="amt">${this.money(s.price)}</span>` + viewBtn + `<button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
2211
+ `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span>` + tierSelect + `<span class="sl-chip-state sl-pending">Selected</span><span class="amt">${this.money(s.price)}</span>` + viewBtn + `<button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
1839
2212
  );
1840
2213
  }
1841
2214
  for (const area of gaAreas) {
@@ -1847,10 +2220,11 @@ var SeatPicker = class _SeatPicker {
1847
2220
  if (!this.hold) {
1848
2221
  const cats = this.controller.doc?.categories ?? [];
1849
2222
  parts.push(
1850
- `<div class="sl-ba">` + (cats.length > 1 ? `<select aria-label="Category" data-ba-cat><option value="">Any tier</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go">Best available</button></div>`
2223
+ `<div class="sl-ba">` + (cats.length > 1 ? `<select aria-label="Category" data-ba-cat><option value="">Any tier</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026` : "Best available") + `</button></div>`
1851
2224
  );
1852
2225
  }
1853
2226
  this.els.tray.innerHTML = parts.join("");
2227
+ this.lastTrayKeys = nextTrayKeys;
1854
2228
  this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
1855
2229
  btn.addEventListener("click", () => {
1856
2230
  this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
@@ -1865,8 +2239,18 @@ var SeatPicker = class _SeatPicker {
1865
2239
  });
1866
2240
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
1867
2241
  btn.addEventListener("click", () => {
1868
- const id = btn.closest(".sl-chip").dataset.seat;
1869
- this.controller.deselect([id]);
2242
+ const chip = btn.closest(".sl-chip");
2243
+ if (chip.dataset.held) {
2244
+ void this.removeHeldLabel(decodeURIComponent(chip.dataset.held), chip);
2245
+ return;
2246
+ }
2247
+ const id = chip.dataset.seat;
2248
+ if (this.reducedMotion()) {
2249
+ this.controller.deselect([id]);
2250
+ return;
2251
+ }
2252
+ chip.classList.add("sl-leave");
2253
+ this.scheduleMotion(() => this.controller.deselect([id]), 150);
1870
2254
  });
1871
2255
  });
1872
2256
  this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
@@ -1895,39 +2279,85 @@ var SeatPicker = class _SeatPicker {
1895
2279
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
1896
2280
  const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
1897
2281
  const count = freshSeats.length + gaCount + heldCount;
2282
+ const pendingCount = this.pendingSelectionCount();
2283
+ const previousCount = this.lastTrayCount;
2284
+ const previousTotal = this.lastTrayTotal;
1898
2285
  this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
1899
2286
  this.els.total.textContent = count ? this.money(total) : "";
1900
- const cta = this.els.cta;
1901
- cta.disabled = count === 0;
1902
- cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
2287
+ this.syncCta(count, pendingCount);
2288
+ if (this.hold) {
2289
+ const securedCount = heldCount || this.hold.seats?.length || 0;
2290
+ if (this.els.holdTitle) {
2291
+ this.els.holdTitle.textContent = `${securedCount} ${securedCount === 1 ? "seat" : "seats"} secured`;
2292
+ }
2293
+ if (this.els.holdCopy) {
2294
+ this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected \u2014 secure before checkout.` : "Checkout timer is running.";
2295
+ }
2296
+ }
2297
+ if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
2298
+ if (total !== previousTotal) this.animateOnce(this.els.total, "sl-value-pop", 380);
2299
+ if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
1903
2300
  if (this.els.peek) {
1904
2301
  if (count) {
1905
- this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? "Continue" : "Review"}</span>`;
2302
+ this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? pendingCount ? "Secure more" : "Continue" : "Review"}</span>`;
1906
2303
  } else {
1907
2304
  const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
1908
2305
  this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<span class="sub">\xB7 Best available</span>`;
1909
2306
  }
1910
2307
  }
1911
- if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
1912
- this.root.dataset.sheet = "open";
1913
- }
1914
2308
  this.lastTrayCount = count;
2309
+ this.lastTrayTotal = total;
1915
2310
  this.opts.onSelectionChange?.(seats);
1916
2311
  }
2312
+ async removeHeldLabel(label, chip) {
2313
+ if (!label || this.releasingLabels.has(label)) return false;
2314
+ this.releasingLabels.add(label);
2315
+ chip?.setAttribute("aria-busy", "true");
2316
+ const button = chip?.querySelector(".rm");
2317
+ if (button) button.disabled = true;
2318
+ try {
2319
+ const preserveAcrossNavigation = this.handedOff;
2320
+ const released = await this.controller.releaseLabels([label]);
2321
+ if (!released) {
2322
+ this.toast(`Couldn't remove ${label}. Your hold is unchanged.`, "error");
2323
+ return false;
2324
+ }
2325
+ const remaining = this.controller.currentHold();
2326
+ this.hold = remaining ? { holdId: remaining.holdId, expiresAt: remaining.expiresAt, seats: remaining.seats, items: remaining.items } : null;
2327
+ this.handedOff = !!this.hold && preserveAcrossNavigation;
2328
+ this.bookedShown = false;
2329
+ this.ctaPhase = "idle";
2330
+ if (this.hold) {
2331
+ this.startHoldTimer(this.hold.expiresAt);
2332
+ } else {
2333
+ this.stopHoldTimer();
2334
+ this.forgetHold();
2335
+ }
2336
+ this.syncTray();
2337
+ this.emitHoldChange();
2338
+ this.toast(`${label} removed from your hold.`, "success");
2339
+ return true;
2340
+ } finally {
2341
+ this.releasingLabels.delete(label);
2342
+ chip?.removeAttribute("aria-busy");
2343
+ if (button?.isConnected) button.disabled = false;
2344
+ }
2345
+ }
1917
2346
  async handleCta() {
1918
- const cta = this.els.cta;
1919
- if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
1920
- const seats = this.hold.seats ?? this.controller.getSelection();
2347
+ const committed = this.committedSelection();
2348
+ if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
2349
+ const seats = this.hold.seats ?? committed;
1921
2350
  this.handedOff = true;
2351
+ this.setCtaPhase("checkout");
1922
2352
  this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
1923
2353
  return;
1924
2354
  }
1925
- cta.disabled = true;
1926
- cta.textContent = "Holding\u2026";
2355
+ this.holdingLabels = new Set(committed.map((seat) => seat.label));
2356
+ this.setCtaPhase("holding");
1927
2357
  try {
1928
2358
  let hold = null;
1929
2359
  const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
1930
- const chosenSeats = this.controller.getSelection();
2360
+ const chosenSeats = this.committedSelection();
1931
2361
  if (chosenSeats.length) {
1932
2362
  const h = await this.controller.hold(void 0, this.opts.holdTtlMs);
1933
2363
  hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
@@ -1937,31 +2367,43 @@ var SeatPicker = class _SeatPicker {
1937
2367
  hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : hold;
1938
2368
  }
1939
2369
  if (!hold) {
1940
- this.toast("One or more seats were just taken. Please pick again.");
2370
+ this.toast("One or more seats were just taken. Please pick again.", "error");
2371
+ this.setCtaPhase("idle");
1941
2372
  this.syncTray();
1942
2373
  return;
1943
2374
  }
1944
2375
  this.hold = hold;
1945
2376
  this.handedOff = true;
1946
2377
  this.startHoldTimer(hold.expiresAt);
1947
- this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? [], this.buildHandoff(hold));
2378
+ this.flashHeldSeats(hold);
2379
+ this.setCtaPhase("checkout");
2380
+ this.emitHoldChange();
2381
+ this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
1948
2382
  } catch (err) {
1949
2383
  this.opts.onError?.(err);
1950
- this.toast("One or more seats were just taken. Please pick again.");
2384
+ this.toast("One or more seats were just taken. Please pick again.", "error");
2385
+ this.setCtaPhase("idle");
1951
2386
  } finally {
2387
+ this.holdingLabels.clear();
2388
+ if (this.ctaPhase === "holding") this.ctaPhase = "idle";
1952
2389
  this.syncTray();
1953
2390
  }
1954
2391
  }
1955
2392
  startHoldTimer(expiresAt) {
1956
2393
  this.stopHoldTimer();
1957
2394
  this.holdExpiresAt = expiresAt;
2395
+ if (this.hold) this.rememberHold(this.hold);
1958
2396
  const pill = this.els.hold;
2397
+ pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
2398
+ const time = pill.querySelector('[data-ref="holdTime"]');
2399
+ this.els.holdNote?.classList.add("on");
1959
2400
  const tick = () => {
1960
2401
  const ms = Math.max(0, this.holdExpiresAt - Date.now());
1961
2402
  const m = Math.floor(ms / 6e4);
1962
2403
  const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
1963
- pill.textContent = `Held ${m}:${s}`;
2404
+ if (time) time.textContent = `${m}:${s}`;
1964
2405
  pill.classList.add("on");
2406
+ pill.classList.toggle("is-expiring", ms > 0 && ms <= EXTEND_PROMPT_MS);
1965
2407
  this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
1966
2408
  if (ms <= 0) this.stopHoldTimer();
1967
2409
  };
@@ -1971,7 +2413,8 @@ var SeatPicker = class _SeatPicker {
1971
2413
  stopHoldTimer() {
1972
2414
  if (this.holdTimer) clearInterval(this.holdTimer);
1973
2415
  this.holdTimer = null;
1974
- this.els.hold?.classList.remove("on");
2416
+ this.els.hold?.classList.remove("on", "is-expiring");
2417
+ this.els.holdNote?.classList.remove("on");
1975
2418
  this.setExtendPrompt(false, 0);
1976
2419
  }
1977
2420
  /** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
@@ -1996,13 +2439,15 @@ var SeatPicker = class _SeatPicker {
1996
2439
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
1997
2440
  this.holdExpiresAt = h.expiresAt;
1998
2441
  this.extendEl?.classList.remove("on");
1999
- this.toast("More time added \u2014 your seats are still held.");
2442
+ this.rememberHold(this.hold);
2443
+ this.emitHoldChange();
2444
+ this.toast("More time added \u2014 your seats are still held.", "success");
2000
2445
  } else {
2001
- this.toast("Couldn't add more time \u2014 please head to checkout now.");
2446
+ this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
2002
2447
  }
2003
2448
  } catch (err) {
2004
2449
  this.opts.onError?.(err);
2005
- this.toast("Couldn't add more time \u2014 please head to checkout now.");
2450
+ this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
2006
2451
  } finally {
2007
2452
  btn.disabled = false;
2008
2453
  btn.textContent = prev;
@@ -2026,6 +2471,7 @@ var SeatPicker = class _SeatPicker {
2026
2471
  this.bookedShown = true;
2027
2472
  const handoff = this.buildHandoff(this.hold);
2028
2473
  this.stopHoldTimer();
2474
+ this.forgetHold();
2029
2475
  const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
2030
2476
  if (this.els.bookedSub) {
2031
2477
  this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
@@ -2050,13 +2496,25 @@ var SeatPicker = class _SeatPicker {
2050
2496
  const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
2051
2497
  return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
2052
2498
  }
2053
- toast(msg) {
2499
+ emitHoldChange() {
2500
+ const hold = this.hold;
2501
+ this.opts.onHoldChange?.(
2502
+ hold,
2503
+ hold?.seats ?? [],
2504
+ hold ? this.buildHandoff(hold) : null
2505
+ );
2506
+ }
2507
+ toast(msg, tone = "neutral") {
2054
2508
  const el = this.els.toast;
2055
2509
  if (!el) return;
2056
2510
  el.textContent = msg;
2511
+ el.dataset.tone = tone;
2057
2512
  el.classList.add("on");
2058
2513
  if (this.toastTimer) clearTimeout(this.toastTimer);
2059
- this.toastTimer = setTimeout(() => el.classList.remove("on"), 4200);
2514
+ this.toastTimer = setTimeout(() => {
2515
+ el.classList.remove("on");
2516
+ el.dataset.tone = "neutral";
2517
+ }, 4200);
2060
2518
  }
2061
2519
  placeTooltip() {
2062
2520
  if (!this.tipEl) return;
@@ -2077,15 +2535,40 @@ var SeatPicker = class _SeatPicker {
2077
2535
  return;
2078
2536
  }
2079
2537
  const statusLine = details.status === "free" ? "" : `<div style="margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;font-weight:700">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
2080
- this.tipEl.innerHTML = `<div style="font-weight:800;font-size:13px">${details.label}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span style="opacity:.75">${details.categoryLabel}</span><span style="margin-left:auto;font-weight:800">${this.money(details.price)}</span></div>` + statusLine;
2538
+ const location = [
2539
+ details.sectionLabel ? `Section ${details.sectionLabel}` : "",
2540
+ details.rowLabel ? `Row ${details.rowLabel}` : "",
2541
+ details.seatNumber ? `Seat ${details.seatNumber}` : details.label
2542
+ ].filter(Boolean).join(" \xB7 ");
2543
+ this.tipEl.innerHTML = `<div style="font-weight:800;font-size:13px">${location}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span style="opacity:.75">${details.categoryLabel}</span><span style="margin-left:auto;font-weight:800">${this.money(details.price)}</span></div>` + statusLine;
2081
2544
  this.tipEl.style.display = "block";
2082
2545
  this.placeTooltip();
2083
2546
  }
2084
2547
  // ---- public conveniences ----------------------------------------------------
2085
2548
  getSelection() {
2086
- return this.controller.getSelection();
2549
+ return this.committedSelection();
2550
+ }
2551
+ /** Current active/restored hold reflected in the tray. */
2552
+ getCurrentHold() {
2553
+ return this.hold;
2554
+ }
2555
+ /** Explicit host-driven hold restore (automatic session restore is on by default). */
2556
+ async resumeHold(holdId) {
2557
+ return this.resumeHoldFromServer(holdId, false);
2558
+ }
2559
+ /** Remove one server-held ticket while keeping the rest of the hold active. */
2560
+ async removeHeldTicket(label) {
2561
+ return this.removeHeldLabel(label);
2087
2562
  }
2088
2563
  async bestAvailable(qty, categoryKey) {
2564
+ if (this.bestAvailableBusy) return null;
2565
+ if (this.confirmSeat) this.cancelConfirm();
2566
+ this.bestAvailableBusy = true;
2567
+ const button = this.els.tray?.querySelector(".sl-ba-go");
2568
+ if (button) {
2569
+ button.disabled = true;
2570
+ button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
2571
+ }
2089
2572
  try {
2090
2573
  const h = await this.controller.bestAvailable(qty, categoryKey);
2091
2574
  if (h) {
@@ -2093,30 +2576,67 @@ var SeatPicker = class _SeatPicker {
2093
2576
  this.handedOff = false;
2094
2577
  this.bookedShown = false;
2095
2578
  this.startHoldTimer(h.expiresAt);
2579
+ this.flashHeldSeats(this.hold);
2096
2580
  this.syncTray();
2581
+ this.emitHoldChange();
2097
2582
  return this.hold;
2098
2583
  }
2099
2584
  return null;
2100
2585
  } catch (err) {
2101
2586
  this.opts.onError?.(err);
2587
+ this.toast("Those seats are no longer available. Try another quantity or category.", "error");
2102
2588
  return null;
2589
+ } finally {
2590
+ this.bestAvailableBusy = false;
2591
+ if (!this.hold && button?.isConnected) {
2592
+ button.disabled = false;
2593
+ button.textContent = "Best available";
2594
+ }
2103
2595
  }
2104
2596
  }
2105
2597
  async release() {
2106
- await this.controller.release();
2598
+ const tracked = this.hold;
2599
+ const controllerHold = this.controller.currentHold();
2600
+ let released = true;
2601
+ if (controllerHold) {
2602
+ released = await this.controller.release();
2603
+ } else if (tracked) {
2604
+ const labels = [.../* @__PURE__ */ new Set([
2605
+ ...(tracked.items ?? []).map((item) => item.label),
2606
+ ...(tracked.seats ?? []).map((seat) => seat.label)
2607
+ ])];
2608
+ if (labels.length) {
2609
+ try {
2610
+ await this.api.release(this.opts.event, labels, tracked.holdId);
2611
+ } catch (error) {
2612
+ this.opts.onError?.(error);
2613
+ released = false;
2614
+ }
2615
+ }
2616
+ }
2617
+ if (!released) {
2618
+ this.toast("Couldn't release your tickets. Your hold is unchanged.", "error");
2619
+ return;
2620
+ }
2107
2621
  this.hold = null;
2622
+ this.forgetHold();
2108
2623
  this.handedOff = false;
2109
2624
  this.bookedShown = false;
2625
+ this.ctaPhase = "idle";
2110
2626
  this.stopHoldTimer();
2111
2627
  this.gaQty.clear();
2112
2628
  this.syncTray();
2629
+ this.emitHoldChange();
2113
2630
  }
2114
2631
  destroy() {
2115
2632
  this.destroyed = true;
2633
+ if (this.hold && !this.handedOff) void this.controller.release();
2116
2634
  this.closeConfirm();
2117
2635
  this.closeSeatView();
2118
2636
  this.stopHoldTimer();
2119
2637
  if (this.toastTimer) clearTimeout(this.toastTimer);
2638
+ for (const timer of this.motionTimers) clearTimeout(timer);
2639
+ this.motionTimers.clear();
2120
2640
  this.ro?.disconnect();
2121
2641
  this.ro = null;
2122
2642
  if (this.escHandler) document.removeEventListener("keydown", this.escHandler);