@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.cjs CHANGED
@@ -84,6 +84,12 @@ var PubApi = class {
84
84
  body: { qty, ...categoryKey ? { categoryKey } : {} }
85
85
  });
86
86
  }
87
+ resume(key, holdId) {
88
+ return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold/resume`, {
89
+ method: "POST",
90
+ body: { holdId }
91
+ });
92
+ }
87
93
  release(key, labels, holdId) {
88
94
  return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {
89
95
  method: "POST",
@@ -140,6 +146,7 @@ var SeatingChart = class {
140
146
  currency: options.currency,
141
147
  onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),
142
148
  onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
149
+ onHoldRestored: (h) => this.opts.onHoldRestored?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
143
150
  onHoldExpired: () => this.opts.onHoldExpired?.(),
144
151
  onGAClick: (areaId) => {
145
152
  const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);
@@ -243,6 +250,21 @@ var SeatingChart = class {
243
250
  return null;
244
251
  }
245
252
  }
253
+ /** Restore an active hold by its opaque id without extending its expiry. */
254
+ async resumeHold(holdId) {
255
+ try {
256
+ const h = await this.controller.resumeHold(holdId);
257
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
258
+ } catch (err) {
259
+ this.opts.onError?.(err);
260
+ return null;
261
+ }
262
+ }
263
+ /** Current active hold known to this chart, if any. */
264
+ getCurrentHold() {
265
+ const h = this.controller.currentHold();
266
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
267
+ }
246
268
  getGAAreas() {
247
269
  return this.controller.getGAAreas();
248
270
  }
@@ -311,6 +333,10 @@ var SeatingChart = class {
311
333
  async release() {
312
334
  await this.controller.release();
313
335
  }
336
+ /** Release selected labels from the current hold while keeping the remainder. */
337
+ async releaseLabels(labels) {
338
+ return this.controller.releaseLabels(labels);
339
+ }
314
340
  /** Tear everything down: close the socket, stop timers, drop the canvas. */
315
341
  destroy() {
316
342
  if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
@@ -458,8 +484,12 @@ var CSS = `
458
484
  .sl-head-meta{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);margin-top:3px;
459
485
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
460
486
  .sl-hold-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
461
- background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums}
462
- .sl-hold-pill.on{display:inline-flex}
487
+ background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums;
488
+ transform-origin:right center}
489
+ .sl-hold-pill.on{display:inline-flex;animation:slPillIn .34s cubic-bezier(.2,.8,.2,1) both}
490
+ .sl-hold-dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.78;box-shadow:0 0 0 0 currentColor}
491
+ .sl-hold-pill.is-expiring .sl-hold-dot{animation:slHoldPulse 1.4s ease-out infinite}
492
+ .sl-hold-time{min-width:3.35em;text-align:left}
463
493
  .sl-close{width:32px;height:32px;border-radius:999px;flex:none;display:none;align-items:center;justify-content:center;
464
494
  border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
465
495
  .sl-close:hover{color:var(--sl-text);border-color:var(--sl-muted)}
@@ -481,11 +511,20 @@ var CSS = `
481
511
  .sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
482
512
  .sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
483
513
  .sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
484
- flex:none;height:50%;transition:height .22s ease}
514
+ flex:none;height:50%;transition:height .3s cubic-bezier(.2,.8,.2,1)}
485
515
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
486
516
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
487
517
  .sl-picker[data-layout="narrow"] .sl-tray{flex:none}
488
518
  .sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
519
+ .sl-picker[data-layout="narrow"] .sl-sheet-head{order:0}
520
+ .sl-picker[data-layout="narrow"] .sl-seats-sec{order:1}
521
+ .sl-picker[data-layout="narrow"] .sl-tray{order:2}
522
+ .sl-picker[data-layout="narrow"] .sl-filtersec{order:3}
523
+ .sl-picker[data-layout="narrow"] .sl-filters{order:4}
524
+ .sl-picker[data-layout="narrow"] .sl-prices-sec{order:5}
525
+ .sl-picker[data-layout="narrow"] .sl-pricef{order:6}
526
+ .sl-picker[data-layout="narrow"] .sl-prices{order:7}
527
+ .sl-picker[data-layout="narrow"] .sl-foot{order:8}
489
528
  /* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
490
529
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
491
530
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
@@ -532,15 +571,27 @@ var CSS = `
532
571
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
533
572
  .sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
534
573
  .sl-price-amt{font-weight:800;font-variant-numeric:tabular-nums}
574
+ .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}
575
+ .sl-status-item{display:inline-flex;align-items:center;gap:6px}
576
+ .sl-status-icon{width:17px;height:17px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
577
+ color:#fff;background:#6b7280;font-size:9px;font-weight:900;line-height:1}
578
+ .sl-status-icon.sold{background:#374151;font-size:14px}
535
579
 
536
580
  /* tray */
537
581
  .sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
538
582
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
539
583
  .sl-chip{display:flex;align-items:center;gap:9px;padding:9px 11px;border:1px solid var(--sl-line);
540
- border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px}
584
+ border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px;transform-origin:center}
585
+ .sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
586
+ .sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
587
+ .sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
588
+ box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
541
589
  .sl-chip b{font-weight:800}
542
590
  .sl-chip .cat{color:var(--sl-muted);font-size:11.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
543
591
  .sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums}
592
+ .sl-chip-state{flex:none;padding:3px 6px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
593
+ font-size:8.5px;line-height:1;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
594
+ .sl-chip-state.sl-pending{background:transparent;color:var(--sl-muted);border:1px solid var(--sl-line)}
544
595
  .sl-chip .rm{width:22px;height:22px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
545
596
  .sl-chip .rm:hover{color:var(--sl-text)}
546
597
  .sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
@@ -558,17 +609,30 @@ var CSS = `
558
609
 
559
610
  /* footer */
560
611
  .sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
612
+ .sl-hold-note{display:none;align-items:center;gap:9px;margin-bottom:10px;padding:9px 10px;border-radius:var(--sl-r-sm);
613
+ border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
614
+ box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
615
+ font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
616
+ .sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
617
+ .sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
618
+ stroke-linecap:round;stroke-linejoin:round}
619
+ .sl-hold-note b{display:block;color:var(--sl-text);font-size:12px}
620
+ .sl-hold-copy{display:block}
561
621
  .sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
562
622
  .sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
623
+ .sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
563
624
  /* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
564
625
  '.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
565
626
  and the button renders as plain text with no accent fill. */
566
627
  .sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
567
628
  padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
568
629
  background:var(--sl-accent);color:var(--sl-accent-ink);
569
- transition:filter .15s,background .15s,color .15s,transform .06s}
630
+ transition:filter .15s,background .22s,color .22s,transform .12s,box-shadow .22s;gap:8px}
570
631
  .sl-picker .sl-cta:hover{filter:brightness(1.08)}
571
632
  .sl-picker .sl-cta:active{transform:translateY(1px);filter:brightness(.94)}
633
+ .sl-picker .sl-cta.sl-ready{animation:slCtaReady .42s cubic-bezier(.2,.8,.2,1)}
634
+ .sl-cta-spin,.sl-ba-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;
635
+ animation:slspin .7s linear infinite;flex:none}
572
636
  /* Disabled ("Select seats"): quieter, but still a full-width button shape. */
573
637
  .sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
574
638
  cursor:not-allowed;filter:none;transform:none}
@@ -606,11 +670,15 @@ var CSS = `
606
670
  .sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
607
671
 
608
672
  /* toast + boot states (toast flows in the bottom-center region) */
609
- .sl-toast{transform:translateY(6px);max-width:100%;
673
+ .sl-toast{transform:translateY(6px) scale(.98);max-width:100%;
610
674
  background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
611
- font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;white-space:nowrap;
675
+ font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
612
676
  overflow:hidden;text-overflow:ellipsis}
613
- .sl-toast.on{opacity:1;transform:translateY(0)}
677
+ .sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
678
+ .sl-toast[data-tone="error"]{border-color:#ef4444}
679
+ .sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
680
+ .sl-toast[data-tone="success"]{border-color:#22c55e}
681
+ .sl-toast.on[data-tone="error"]{animation:slToastNudge .32s ease-out}
614
682
  .sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
615
683
  gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
616
684
  .sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
@@ -634,15 +702,22 @@ var CSS = `
634
702
  .sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
635
703
 
636
704
  /* booked confirmation overlay (covers the widget once the held seats are sold) */
637
- .sl-booked{position:absolute;inset:0;z-index:11;display:none;flex-direction:column;align-items:center;
638
- justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)}
639
- .sl-booked.on{display:flex}
705
+ .sl-booked{position:absolute;inset:0;z-index:11;display:flex;flex-direction:column;align-items:center;
706
+ justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg);opacity:0;visibility:hidden;
707
+ pointer-events:none;transition:opacity .34s ease,visibility 0s linear .34s}
708
+ .sl-booked.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .34s ease,visibility 0s}
640
709
  .sl-booked-badge{width:60px;height:60px;border-radius:999px;display:flex;align-items:center;justify-content:center;
641
- background:var(--sl-accent);color:var(--sl-accent-ink)}
642
- .sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
710
+ background:var(--sl-accent);color:var(--sl-accent-ink);transform:scale(.72)}
711
+ .sl-booked.on .sl-booked-badge{animation:slSuccessPop .58s cubic-bezier(.2,1.25,.3,1) .08s both}
712
+ .sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round;
713
+ stroke-dasharray:30;stroke-dashoffset:30}
714
+ .sl-booked.on .sl-booked-badge svg{animation:slCheckDraw .42s ease-out .32s forwards}
643
715
  .sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
644
716
  .sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
645
717
  .sl-booked-seats{font-weight:700;color:var(--sl-text)}
718
+ .sl-booked.on .sl-booked-title,.sl-booked.on .sl-booked-sub{animation:slCopyRise .42s ease-out both}
719
+ .sl-booked.on .sl-booked-title{animation-delay:.22s}
720
+ .sl-booked.on .sl-booked-sub{animation-delay:.3s}
646
721
 
647
722
  /* a11y filter chips (flow within the top-left region) */
648
723
  .sl-chips{display:flex;gap:6px;flex-wrap:wrap}
@@ -651,28 +726,49 @@ var CSS = `
651
726
  .sl-chip-f:hover{color:var(--sl-text)}
652
727
  .sl-chip-f.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
653
728
 
654
- /* confirm popover */
655
- .sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
656
- border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px))}
657
- .sl-confirm-label{font-weight:800;font-size:14px}
658
- .sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
659
- .sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
729
+ /* confirm card: a candidate is not in the tray until Select. Map gestures and
730
+ floating chrome pause while the card owns focus, keeping the camera stable. */
731
+ .sl-picker[data-confirming="true"] .sl-map-host>:not(.sl-confirm){pointer-events:none}
732
+ .sl-picker[data-confirming="true"] .sl-anchor{pointer-events:none;opacity:.28;transition:opacity .16s}
733
+ .sl-picker[data-confirming="true"] .sl-side{pointer-events:none;opacity:.58;transition:opacity .16s}
734
+ .sl-confirm{position:absolute;z-index:10;width:276px;max-width:calc(100% - 24px);overflow:hidden;pointer-events:auto;
735
+ background:var(--sl-surface);border:1px solid color-mix(in srgb,var(--sl-line) 70%,var(--sl-text));
736
+ border-radius:15px;box-shadow:0 24px 64px -18px rgba(0,0,0,.72);transform:translate(-50%,calc(-100% - 16px));
737
+ animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
738
+ .sl-confirm[data-placement="below"]{transform:translate(-50%,16px);animation:slConfirmBelowIn .24s cubic-bezier(.2,.8,.2,1) both}
739
+ .sl-confirm-grid{display:grid;grid-template-columns:1.2fr .8fr .8fr;border-bottom:1px solid var(--sl-line)}
740
+ .sl-confirm-field{min-width:0;padding:12px 11px 10px;border-right:1px solid var(--sl-line)}
741
+ .sl-confirm-field:last-child{border-right:0;text-align:center}
742
+ .sl-confirm-field:nth-child(2){text-align:center}
743
+ .sl-confirm-key{display:block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
744
+ .sl-confirm-value{display:block;margin-top:4px;color:var(--sl-text);font-size:17px;line-height:1.1;font-weight:850;
745
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
746
+ .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))}
747
+ .sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
748
+ .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}
749
+ .sl-confirm-price{font-size:17px;font-weight:850;color:#fff;font-variant-numeric:tabular-nums}
750
+ .sl-confirm-body{padding:11px 12px 12px}
660
751
  .sl-confirm-row{display:flex;gap:8px;margin-top:10px}
661
- .sl-confirm-row button{flex:1;padding:8px;border-radius:8px;font-weight:700;font-size:12.5px}
662
- .sl-confirm-add{background:var(--sl-accent);color:var(--sl-accent-ink)}
663
- .sl-confirm-cancel{border:1px solid var(--sl-line);color:var(--sl-muted)}
752
+ .sl-confirm-row button{flex:1;min-height:44px;padding:9px 12px;border-radius:9px;font-weight:800;font-size:13px}
753
+ .sl-confirm-add{background:var(--sl-accent)!important;color:var(--sl-accent-ink)!important;display:flex;align-items:center;justify-content:center;gap:7px}
754
+ .sl-confirm-add svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
755
+ .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}
664
756
  .sl-confirm-cancel:hover{color:var(--sl-text)}
757
+ .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
758
+ transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
665
759
 
666
760
  /* best-available row */
667
- .sl-ba{display:flex;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
761
+ .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)}
668
762
  .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
669
- font:inherit;font-size:12px;padding:5px 6px;max-width:110px}
763
+ font:inherit;font-size:12px;padding:7px 8px;min-width:0;width:100%;max-width:none}
670
764
  .sl-ba-qty{display:flex;align-items:center;gap:7px}
671
765
  .sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
672
766
  font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
673
767
  .sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
674
- .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}
768
+ .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;
769
+ transition:border-color .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:6px}
675
770
  .sl-ba-go:hover{border-color:var(--sl-muted)}
771
+ .sl-ba-go:disabled{opacity:.62;cursor:wait}
676
772
 
677
773
  /* screen-reader live region */
678
774
  .sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
@@ -778,6 +874,29 @@ var CSS = `
778
874
  .sl-price-row.sl-dim{opacity:.4}
779
875
  .sl-seccard-mix-item.sl-dim{opacity:.4}
780
876
 
877
+ /* Buyer-journey motion: every animation explains a state transition (selected,
878
+ held, checkout handoff, conflict or booked). No decorative infinite motion
879
+ except the expiring-hold pulse and active progress spinners. */
880
+ @keyframes slPillIn{from{opacity:0;transform:translateX(7px) scale(.9)}to{opacity:1;transform:translateX(0) scale(1)}}
881
+ @keyframes slHoldPulse{0%{box-shadow:0 0 0 0 currentColor;opacity:.9}75%,100%{box-shadow:0 0 0 7px transparent;opacity:.55}}
882
+ @keyframes slChipIn{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
883
+ @keyframes slChipOut{to{opacity:0;transform:translateX(10px) scale(.98)}}
884
+ @keyframes slNoticeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
885
+ @keyframes slValuePop{0%{opacity:.6;transform:translateY(3px)}55%{transform:translateY(-1px) scale(1.05)}100%{opacity:1;transform:none}}
886
+ @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}}
887
+ @keyframes slToastNudge{0%,100%{margin-left:0}30%{margin-left:-4px}60%{margin-left:3px}}
888
+ @keyframes slSuccessPop{0%{opacity:0;transform:scale(.72)}65%{opacity:1;transform:scale(1.08)}100%{opacity:1;transform:scale(1)}}
889
+ @keyframes slCheckDraw{to{stroke-dashoffset:0}}
890
+ @keyframes slCopyRise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
891
+ @keyframes slConfirmIn{from{opacity:0;transform:translate(-50%,calc(-100% - 8px)) scale(.96)}to{opacity:1;transform:translate(-50%,calc(-100% - 14px)) scale(1)}}
892
+ @keyframes slConfirmBelowIn{from{opacity:0;transform:translate(-50%,8px) scale(.96)}to{opacity:1;transform:translate(-50%,16px) scale(1)}}
893
+ @keyframes slConfirmMobileIn{from{opacity:0;transform:translate(-50%,10px) scale(.97)}to{opacity:1;transform:translate(-50%,0) scale(1)}}
894
+
895
+ @media(prefers-reduced-motion:reduce){
896
+ .sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
897
+ transition-duration:.001ms!important;scroll-behavior:auto!important}
898
+ }
899
+
781
900
  /* modal host */
782
901
  .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}
783
902
  .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)}
@@ -818,6 +937,8 @@ var SeatPicker = class _SeatPicker {
818
937
  this.ro = null;
819
938
  this.holdTimer = null;
820
939
  this.toastTimer = null;
940
+ /** Short-lived UI motion timers; all are cancelled on destroy. */
941
+ this.motionTimers = /* @__PURE__ */ new Set();
821
942
  // state
822
943
  this.currency = "USD";
823
944
  this.hold = null;
@@ -861,6 +982,15 @@ var SeatPicker = class _SeatPicker {
861
982
  this.secCardShownAt = 0;
862
983
  /** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
863
984
  this.lastTrayCount = 0;
985
+ /** Previous computed total — drives a single explanatory value bump. */
986
+ this.lastTrayTotal = 0;
987
+ /** Stable item keys prevent tray chips re-animating on unrelated realtime syncs. */
988
+ this.lastTrayKeys = /* @__PURE__ */ new Set();
989
+ this.bestAvailableBusy = false;
990
+ this.releasingLabels = /* @__PURE__ */ new Set();
991
+ /** Selected labels awaiting the hold response; their own realtime echo can arrive first. */
992
+ this.holdingLabels = /* @__PURE__ */ new Set();
993
+ this.ctaPhase = "idle";
864
994
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
865
995
  this.a11yChipsEl = null;
866
996
  this.cbEl = null;
@@ -873,10 +1003,11 @@ var SeatPicker = class _SeatPicker {
873
1003
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
874
1004
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
875
1005
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
876
- this.opts = options;
877
- const api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
1006
+ this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
1007
+ this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
1008
+ this.api = new PubApi(this.apiBase);
878
1009
  this.controller = new import_core2.PickerController({
879
- transport: api,
1010
+ transport: this.api,
880
1011
  eventKey: options.event,
881
1012
  maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
882
1013
  currency: options.currency,
@@ -884,7 +1015,7 @@ var SeatPicker = class _SeatPicker {
884
1015
  colorblindSafe: options.colorblindSafe,
885
1016
  onSelectionChange: () => {
886
1017
  this.syncTray();
887
- if (this.controller.getSelection().length) this.collapseSectionCard();
1018
+ if (this.committedSelection().length) this.collapseSectionCard();
888
1019
  },
889
1020
  onStatusChange: () => {
890
1021
  this.syncPrices();
@@ -894,18 +1025,25 @@ var SeatPicker = class _SeatPicker {
894
1025
  },
895
1026
  onHoldExpired: () => {
896
1027
  this.hold = null;
1028
+ this.forgetHold();
897
1029
  this.handedOff = false;
898
1030
  this.bookedShown = false;
1031
+ this.ctaPhase = "idle";
899
1032
  this.stopHoldTimer();
900
1033
  this.gaQty.clear();
901
- this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
1034
+ this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.", "warning");
902
1035
  this.syncTray();
1036
+ this.emitHoldChange();
903
1037
  this.opts.onHoldExpired?.();
904
1038
  },
905
- confirmSelection: options.confirmSelection,
1039
+ confirmSelection: this.opts.confirmSelection,
906
1040
  onSelect: (seat) => {
1041
+ this.flashPickedSeat(seat.id);
907
1042
  if (this.opts.confirmSelection) this.showConfirm(seat);
908
1043
  },
1044
+ onDeselect: (seat) => {
1045
+ if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
1046
+ },
909
1047
  onViewChange: () => {
910
1048
  this.reanchorConfirm();
911
1049
  this.syncRung();
@@ -945,17 +1083,32 @@ var SeatPicker = class _SeatPicker {
945
1083
  const picker = new _SeatPicker({ ...options, container: frame });
946
1084
  picker.modalScrim = scrim;
947
1085
  picker.prevFocus = document.activeElement;
1086
+ let closing = false;
948
1087
  const close = () => {
1088
+ if (closing) return;
1089
+ closing = true;
949
1090
  document.body.style.overflow = prevOverflow;
950
- picker.destroy();
951
- options.onClose?.();
1091
+ scrim.style.opacity = "0";
1092
+ scrim.style.pointerEvents = "none";
1093
+ const finish = () => {
1094
+ picker.destroy();
1095
+ options.onClose?.();
1096
+ };
1097
+ if (picker.hold && !picker.handedOff) void picker.release().finally(finish);
1098
+ else finish();
952
1099
  };
953
1100
  picker.closeModal = close;
954
1101
  scrim.addEventListener("mousedown", (e) => {
955
1102
  if (e.target === scrim) close();
956
1103
  });
957
1104
  picker.escHandler = (e) => {
958
- if (e.key === "Escape") close();
1105
+ if (e.key !== "Escape") return;
1106
+ if (picker.confirmSeat) {
1107
+ e.preventDefault();
1108
+ picker.cancelConfirm();
1109
+ } else {
1110
+ close();
1111
+ }
959
1112
  };
960
1113
  document.addEventListener("keydown", picker.escHandler);
961
1114
  await picker.render();
@@ -972,8 +1125,15 @@ var SeatPicker = class _SeatPicker {
972
1125
  const mount = resolveContainer3(this.opts.container);
973
1126
  const root = document.createElement("div");
974
1127
  root.className = "sl-picker";
1128
+ root.tabIndex = -1;
975
1129
  this.root = root;
976
1130
  mount.appendChild(root);
1131
+ root.addEventListener("keydown", (e) => {
1132
+ if (e.key !== "Escape" || !this.confirmSeat) return;
1133
+ e.preventDefault();
1134
+ e.stopPropagation();
1135
+ this.cancelConfirm();
1136
+ });
977
1137
  Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
978
1138
  root.innerHTML = `
979
1139
  <div class="sl-head">
@@ -1012,11 +1172,15 @@ var SeatPicker = class _SeatPicker {
1012
1172
  </div>
1013
1173
  <div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
1014
1174
  <div class="sl-filters" data-ref="filters"></div>
1015
- <div class="sl-sec" data-ref="pricesSec">Prices</div>
1175
+ <div class="sl-sec sl-prices-sec" data-ref="pricesSec">Prices</div>
1016
1176
  <div class="sl-prices" data-ref="prices"></div>
1017
- <div class="sl-sec">Your seats</div>
1177
+ <div class="sl-sec sl-seats-sec">Your seats</div>
1018
1178
  <div class="sl-tray" data-ref="tray"></div>
1019
1179
  <div class="sl-foot">
1180
+ <div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
1181
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
1182
+ <span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
1183
+ </div>
1020
1184
  <div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
1021
1185
  <button type="button" class="sl-cta" data-ref="cta" disabled></button>
1022
1186
  </div>
@@ -1168,6 +1332,8 @@ var SeatPicker = class _SeatPicker {
1168
1332
  this.buildExtendPrompt();
1169
1333
  this.buildBookedOverlay();
1170
1334
  this.dockLayoutChrome();
1335
+ await this.restoreRememberedHold();
1336
+ if (this.destroyed) return this;
1171
1337
  this.syncPrices();
1172
1338
  this.syncTray();
1173
1339
  return this;
@@ -1243,6 +1409,156 @@ var SeatPicker = class _SeatPicker {
1243
1409
  cssVar(name) {
1244
1410
  return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
1245
1411
  }
1412
+ /** Motion is progressive enhancement; all state remains legible when reduced. */
1413
+ reducedMotion() {
1414
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1415
+ }
1416
+ scheduleMotion(fn, delay) {
1417
+ const timer = setTimeout(() => {
1418
+ this.motionTimers.delete(timer);
1419
+ if (!this.destroyed) fn();
1420
+ }, delay);
1421
+ this.motionTimers.add(timer);
1422
+ }
1423
+ /** Restart one finite CSS animation without leaving a permanent state class. */
1424
+ animateOnce(el, className, duration = 600) {
1425
+ if (!el || this.reducedMotion()) return;
1426
+ el.classList.remove(className);
1427
+ void el.offsetWidth;
1428
+ el.classList.add(className);
1429
+ this.scheduleMotion(() => el.classList.remove(className), duration);
1430
+ }
1431
+ /** Selection feedback belongs on the selected seat, not across the whole map. */
1432
+ flashPickedSeat(id) {
1433
+ if (this.reducedMotion()) return;
1434
+ this.controller.flashSeat(id, this.cssVar("--sl-accent") || "#f4b740");
1435
+ }
1436
+ /** A completed hold gets one short map ripple per concrete seat. */
1437
+ flashHeldSeats(hold) {
1438
+ if (this.reducedMotion()) return;
1439
+ const labels = (hold.items ?? []).filter((item) => item.objectType !== "ga").map((item) => item.label);
1440
+ labels.slice(0, 10).forEach((label, index) => {
1441
+ const seat = this.controller.seatByLabel(label);
1442
+ if (!seat) return;
1443
+ this.scheduleMotion(
1444
+ () => this.controller.flashSeat(seat.id, this.cssVar("--sl-accent") || "#f4b740"),
1445
+ index * 55
1446
+ );
1447
+ });
1448
+ }
1449
+ /** Update only the action affordance; selection callbacks must not refire. */
1450
+ committedSelection() {
1451
+ const candidateId = this.confirmSeat?.id;
1452
+ return this.controller.getSelection().filter((seat) => seat.id !== candidateId);
1453
+ }
1454
+ pendingSelectionCount() {
1455
+ const heldItems = this.hold?.items ?? [];
1456
+ const heldLabels = new Set(heldItems.map((item) => item.label));
1457
+ const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1458
+ const heldGA = /* @__PURE__ */ new Map();
1459
+ for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
1460
+ heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1461
+ }
1462
+ const pendingGA = [...this.gaQty.entries()].reduce(
1463
+ (sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
1464
+ 0
1465
+ );
1466
+ return pendingSeats + pendingGA;
1467
+ }
1468
+ syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1469
+ const cta = this.els.cta;
1470
+ if (!cta) return;
1471
+ if (this.confirmSeat) {
1472
+ cta.disabled = true;
1473
+ cta.textContent = "Confirm or cancel this seat";
1474
+ return;
1475
+ }
1476
+ if (this.ctaPhase === "holding") {
1477
+ cta.disabled = true;
1478
+ cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
1479
+ return;
1480
+ }
1481
+ if (this.ctaPhase === "checkout") {
1482
+ cta.disabled = true;
1483
+ cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Opening checkout\u2026';
1484
+ return;
1485
+ }
1486
+ cta.disabled = count === 0;
1487
+ cta.textContent = this.hold ? pending ? `Secure ${pending} more & checkout` : "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
1488
+ }
1489
+ setCtaPhase(phase) {
1490
+ this.ctaPhase = phase;
1491
+ this.syncCta();
1492
+ if (phase === "checkout") {
1493
+ this.scheduleMotion(() => {
1494
+ if (this.ctaPhase !== "checkout") return;
1495
+ this.ctaPhase = "idle";
1496
+ this.syncCta();
1497
+ }, 1100);
1498
+ }
1499
+ }
1500
+ /** Session-scoped capability key: isolated by API origin and event. */
1501
+ holdStorageKey() {
1502
+ return `@seatlayer/hold/v1/${encodeURIComponent(this.apiBase)}/${encodeURIComponent(this.opts.event)}`;
1503
+ }
1504
+ rememberedHoldId() {
1505
+ if (this.opts.initialHoldId) return this.opts.initialHoldId;
1506
+ if (this.opts.restoreHold === false || typeof window === "undefined") return null;
1507
+ try {
1508
+ return window.sessionStorage.getItem(this.holdStorageKey());
1509
+ } catch {
1510
+ return null;
1511
+ }
1512
+ }
1513
+ rememberHold(hold) {
1514
+ if (this.opts.restoreHold === false || typeof window === "undefined") return;
1515
+ try {
1516
+ window.sessionStorage.setItem(this.holdStorageKey(), hold.holdId);
1517
+ } catch {
1518
+ }
1519
+ }
1520
+ forgetHold() {
1521
+ if (typeof window === "undefined") return;
1522
+ try {
1523
+ window.sessionStorage.removeItem(this.holdStorageKey());
1524
+ } catch {
1525
+ }
1526
+ }
1527
+ async resumeHoldFromServer(holdId, automatic) {
1528
+ try {
1529
+ const h = await this.controller.resumeHold(holdId);
1530
+ if (!h) return null;
1531
+ const restored = {
1532
+ holdId: h.holdId,
1533
+ expiresAt: h.expiresAt,
1534
+ seats: h.seats,
1535
+ items: h.items
1536
+ };
1537
+ this.hold = restored;
1538
+ this.handedOff = true;
1539
+ this.bookedShown = false;
1540
+ this.ctaPhase = "idle";
1541
+ this.startHoldTimer(restored.expiresAt);
1542
+ this.rememberHold(restored);
1543
+ this.syncTray();
1544
+ this.emitHoldChange();
1545
+ this.opts.onHoldRestored?.(restored, restored.seats ?? [], this.buildHandoff(restored));
1546
+ if (automatic) this.toast("Your held tickets have been restored.", "success");
1547
+ return restored;
1548
+ } catch (error) {
1549
+ const status = error?.status;
1550
+ if (status === 404 || status === 409) {
1551
+ this.forgetHold();
1552
+ } else {
1553
+ this.opts.onError?.(error);
1554
+ }
1555
+ return null;
1556
+ }
1557
+ }
1558
+ async restoreRememberedHold() {
1559
+ const holdId = this.rememberedHoldId();
1560
+ if (holdId) await this.resumeHoldFromServer(holdId, true);
1561
+ }
1246
1562
  /** Section-bearing objects on the active floor (single-floor → doc.objects). */
1247
1563
  activeFloorObjects() {
1248
1564
  const doc = this.controller.doc;
@@ -1497,7 +1813,11 @@ var SeatPicker = class _SeatPicker {
1497
1813
  (r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
1498
1814
  ).join("");
1499
1815
  pills.querySelectorAll("button").forEach((btn) => {
1500
- btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
1816
+ btn.addEventListener("click", () => {
1817
+ const rung = btn.dataset.rung;
1818
+ this.controller.setRung(rung);
1819
+ if (rung === "seats") this.collapseSectionCard();
1820
+ });
1501
1821
  });
1502
1822
  this.regions["top-center"].appendChild(pills);
1503
1823
  this.rungsEl = pills;
@@ -1548,7 +1868,7 @@ var SeatPicker = class _SeatPicker {
1548
1868
  this.secCardEl?.remove();
1549
1869
  this.secCardEl = null;
1550
1870
  if (!summary) return;
1551
- this.secCardCollapsed = false;
1871
+ this.secCardCollapsed = this.controller.getRung() === "seats";
1552
1872
  this.secCardShownAt = Date.now();
1553
1873
  this.renderSectionCard(summary);
1554
1874
  }
@@ -1614,6 +1934,10 @@ var SeatPicker = class _SeatPicker {
1614
1934
  sectionCardOnView() {
1615
1935
  if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
1616
1936
  if (this.root?.dataset.layout === "narrow") return;
1937
+ if (this.controller.getRung() === "seats") {
1938
+ this.collapseSectionCard();
1939
+ return;
1940
+ }
1617
1941
  if (Date.now() - this.secCardShownAt < 1400) {
1618
1942
  if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
1619
1943
  return;
@@ -1656,36 +1980,78 @@ var SeatPicker = class _SeatPicker {
1656
1980
  const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
1657
1981
  this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
1658
1982
  }
1659
- // ---- confirm popover (opt-in confirmSelection mode) ------------------------
1983
+ // ---- seat candidate confirmation ------------------------------------------
1660
1984
  showConfirm(seat) {
1661
- this.closeConfirm();
1985
+ const previousId = this.confirmSeat?.id;
1986
+ this.confirmEl?.remove();
1987
+ this.confirmEl = null;
1988
+ this.confirmSeat = seat;
1989
+ this.root?.setAttribute("data-confirming", "true");
1990
+ this.controller.setSelectionFocus(seat.id);
1991
+ if (previousId && previousId !== seat.id) this.controller.deselect([previousId]);
1662
1992
  if (this.tipEl) this.tipEl.style.display = "none";
1993
+ const details = this.controller.seatDetails(seat.id);
1663
1994
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
1664
- const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
1995
+ const price = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
1996
+ const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
1997
+ "&": "&amp;",
1998
+ "<": "&lt;",
1999
+ ">": "&gt;",
2000
+ '"': "&quot;"
2001
+ })[char]);
1665
2002
  const el = document.createElement("div");
1666
2003
  el.className = "sl-confirm";
1667
- 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>${(0, import_core2.t)("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>`;
2004
+ el.setAttribute("role", "dialog");
2005
+ el.setAttribute("aria-modal", "true");
2006
+ el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
2007
+ el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
2008
+ 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>${(0, import_core2.t)("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>`;
1668
2009
  this.els.map.appendChild(el);
1669
2010
  this.confirmEl = el;
1670
- this.confirmSeat = seat;
1671
2011
  this.reanchorConfirm();
1672
2012
  el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
1673
- el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
1674
- el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
1675
- this.controller.deselect([seat.id]);
1676
- this.closeConfirm();
1677
- });
2013
+ el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
2014
+ el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
2015
+ requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
1678
2016
  }
1679
2017
  reanchorConfirm() {
1680
2018
  if (!this.confirmEl || !this.confirmSeat) return;
1681
2019
  const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
1682
- this.confirmEl.style.left = `${p.x}px`;
1683
- this.confirmEl.style.top = `${p.y}px`;
1684
- }
1685
- closeConfirm() {
2020
+ if (this.root?.dataset.layout === "narrow") return;
2021
+ const mapWidth = this.els.map.clientWidth;
2022
+ const mapHeight = this.els.map.clientHeight;
2023
+ const cardWidth = this.confirmEl.offsetWidth || 276;
2024
+ const cardHeight = this.confirmEl.offsetHeight || 230;
2025
+ const half = cardWidth / 2 + 12;
2026
+ const x = Math.max(half, Math.min(mapWidth - half, p.x));
2027
+ const belowFits = p.y + cardHeight + 24 <= mapHeight;
2028
+ const placeBelow = p.y < cardHeight + 24 && belowFits;
2029
+ this.confirmEl.dataset.placement = placeBelow ? "below" : "above";
2030
+ this.confirmEl.style.left = `${x}px`;
2031
+ this.confirmEl.style.top = `${Math.max(8, Math.min(mapHeight - 8, p.y))}px`;
2032
+ }
2033
+ dismissConfirm() {
1686
2034
  this.confirmEl?.remove();
1687
2035
  this.confirmEl = null;
1688
2036
  this.confirmSeat = null;
2037
+ this.root?.removeAttribute("data-confirming");
2038
+ this.controller.setSelectionFocus(null);
2039
+ }
2040
+ commitConfirm() {
2041
+ if (!this.confirmSeat) return;
2042
+ this.dismissConfirm();
2043
+ this.collapseSectionCard();
2044
+ this.syncTray();
2045
+ }
2046
+ cancelConfirm() {
2047
+ const seat = this.confirmSeat;
2048
+ if (!seat) return;
2049
+ this.controller.deselect([seat.id]);
2050
+ if (this.confirmSeat) this.dismissConfirm();
2051
+ this.root?.focus({ preventScroll: true });
2052
+ }
2053
+ closeConfirm() {
2054
+ this.dismissConfirm();
1689
2055
  }
1690
2056
  // ---- 360° view-from-seat modal --------------------------------------------
1691
2057
  seatViewEnabled() {
@@ -1708,7 +2074,6 @@ var SeatPicker = class _SeatPicker {
1708
2074
  openSeatView(seat) {
1709
2075
  if (!this.root || !this.seatViewEnabled()) return;
1710
2076
  this.closeSeatView();
1711
- this.closeConfirm();
1712
2077
  const doc = this.controller.doc;
1713
2078
  const activeId = this.controller.getActiveFloorId();
1714
2079
  const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
@@ -1819,7 +2184,7 @@ var SeatPicker = class _SeatPicker {
1819
2184
  const price = c.tiers?.length ? c.tiers[0].price : c.price;
1820
2185
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
1821
2186
  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>`;
1822
- }).join("");
2187
+ }).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>`;
1823
2188
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
1824
2189
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
1825
2190
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
@@ -1827,39 +2192,47 @@ var SeatPicker = class _SeatPicker {
1827
2192
  }
1828
2193
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
1829
2194
  evictTakenSelections() {
1830
- const ownLabels = new Set(this.controller.currentHold()?.labels ?? []);
2195
+ const ownLabels = /* @__PURE__ */ new Set([
2196
+ ...this.controller.currentHold()?.labels ?? [],
2197
+ ...this.holdingLabels
2198
+ ]);
1831
2199
  const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
1832
2200
  if (!gone.length) return;
1833
2201
  this.controller.deselect(gone.map((s) => s.id));
1834
- this.toast(`Seat ${gone[0].label} was just taken by another buyer.`);
2202
+ this.toast(`Seat ${gone[0].label} was just taken by another buyer.`, "error");
1835
2203
  }
1836
2204
  syncTray() {
1837
2205
  if (!this.els.tray) return;
1838
- const seats = this.controller.getSelection();
2206
+ const seats = this.committedSelection();
1839
2207
  const gaAreas = this.controller.getGAAreas();
1840
2208
  const heldItems = this.hold?.items ?? [];
1841
2209
  const parts = [];
2210
+ const nextTrayKeys = /* @__PURE__ */ new Set();
1842
2211
  if (!seats.length && !heldItems.length && !gaAreas.length) {
1843
2212
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
1844
2213
  } else if (!seats.length && !heldItems.length) {
1845
2214
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
1846
2215
  }
1847
2216
  for (const item of heldItems) {
2217
+ const itemKey = `held:${item.label}`;
2218
+ nextTrayKeys.add(itemKey);
1848
2219
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
1849
2220
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
1850
2221
  const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
1851
2222
  parts.push(
1852
- `<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="${(0, import_core2.t)("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>`
2223
+ `<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="${(0, import_core2.t)("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>`
1853
2224
  );
1854
2225
  }
1855
2226
  const heldLabels = new Set(heldItems.map((item) => item.label));
1856
2227
  const canView = this.seatViewEnabled();
1857
2228
  for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
2229
+ const itemKey = `seat:${s.id}`;
2230
+ nextTrayKeys.add(itemKey);
1858
2231
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
1859
2232
  const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("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>` : "";
1860
2233
  const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${(0, import_core2.t)("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>` : "";
1861
2234
  parts.push(
1862
- `<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>`
2235
+ `<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>`
1863
2236
  );
1864
2237
  }
1865
2238
  for (const area of gaAreas) {
@@ -1871,10 +2244,11 @@ var SeatPicker = class _SeatPicker {
1871
2244
  if (!this.hold) {
1872
2245
  const cats = this.controller.doc?.categories ?? [];
1873
2246
  parts.push(
1874
- `<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>`
2247
+ `<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>`
1875
2248
  );
1876
2249
  }
1877
2250
  this.els.tray.innerHTML = parts.join("");
2251
+ this.lastTrayKeys = nextTrayKeys;
1878
2252
  this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
1879
2253
  btn.addEventListener("click", () => {
1880
2254
  this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
@@ -1889,8 +2263,18 @@ var SeatPicker = class _SeatPicker {
1889
2263
  });
1890
2264
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
1891
2265
  btn.addEventListener("click", () => {
1892
- const id = btn.closest(".sl-chip").dataset.seat;
1893
- this.controller.deselect([id]);
2266
+ const chip = btn.closest(".sl-chip");
2267
+ if (chip.dataset.held) {
2268
+ void this.removeHeldLabel(decodeURIComponent(chip.dataset.held), chip);
2269
+ return;
2270
+ }
2271
+ const id = chip.dataset.seat;
2272
+ if (this.reducedMotion()) {
2273
+ this.controller.deselect([id]);
2274
+ return;
2275
+ }
2276
+ chip.classList.add("sl-leave");
2277
+ this.scheduleMotion(() => this.controller.deselect([id]), 150);
1894
2278
  });
1895
2279
  });
1896
2280
  this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
@@ -1919,39 +2303,85 @@ var SeatPicker = class _SeatPicker {
1919
2303
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
1920
2304
  const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
1921
2305
  const count = freshSeats.length + gaCount + heldCount;
2306
+ const pendingCount = this.pendingSelectionCount();
2307
+ const previousCount = this.lastTrayCount;
2308
+ const previousTotal = this.lastTrayTotal;
1922
2309
  this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
1923
2310
  this.els.total.textContent = count ? this.money(total) : "";
1924
- const cta = this.els.cta;
1925
- cta.disabled = count === 0;
1926
- cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
2311
+ this.syncCta(count, pendingCount);
2312
+ if (this.hold) {
2313
+ const securedCount = heldCount || this.hold.seats?.length || 0;
2314
+ if (this.els.holdTitle) {
2315
+ this.els.holdTitle.textContent = `${securedCount} ${securedCount === 1 ? "seat" : "seats"} secured`;
2316
+ }
2317
+ if (this.els.holdCopy) {
2318
+ this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected \u2014 secure before checkout.` : "Checkout timer is running.";
2319
+ }
2320
+ }
2321
+ if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
2322
+ if (total !== previousTotal) this.animateOnce(this.els.total, "sl-value-pop", 380);
2323
+ if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
1927
2324
  if (this.els.peek) {
1928
2325
  if (count) {
1929
- this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? "Continue" : "Review"}</span>`;
2326
+ 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>`;
1930
2327
  } else {
1931
2328
  const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
1932
2329
  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>`;
1933
2330
  }
1934
2331
  }
1935
- if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
1936
- this.root.dataset.sheet = "open";
1937
- }
1938
2332
  this.lastTrayCount = count;
2333
+ this.lastTrayTotal = total;
1939
2334
  this.opts.onSelectionChange?.(seats);
1940
2335
  }
2336
+ async removeHeldLabel(label, chip) {
2337
+ if (!label || this.releasingLabels.has(label)) return false;
2338
+ this.releasingLabels.add(label);
2339
+ chip?.setAttribute("aria-busy", "true");
2340
+ const button = chip?.querySelector(".rm");
2341
+ if (button) button.disabled = true;
2342
+ try {
2343
+ const preserveAcrossNavigation = this.handedOff;
2344
+ const released = await this.controller.releaseLabels([label]);
2345
+ if (!released) {
2346
+ this.toast(`Couldn't remove ${label}. Your hold is unchanged.`, "error");
2347
+ return false;
2348
+ }
2349
+ const remaining = this.controller.currentHold();
2350
+ this.hold = remaining ? { holdId: remaining.holdId, expiresAt: remaining.expiresAt, seats: remaining.seats, items: remaining.items } : null;
2351
+ this.handedOff = !!this.hold && preserveAcrossNavigation;
2352
+ this.bookedShown = false;
2353
+ this.ctaPhase = "idle";
2354
+ if (this.hold) {
2355
+ this.startHoldTimer(this.hold.expiresAt);
2356
+ } else {
2357
+ this.stopHoldTimer();
2358
+ this.forgetHold();
2359
+ }
2360
+ this.syncTray();
2361
+ this.emitHoldChange();
2362
+ this.toast(`${label} removed from your hold.`, "success");
2363
+ return true;
2364
+ } finally {
2365
+ this.releasingLabels.delete(label);
2366
+ chip?.removeAttribute("aria-busy");
2367
+ if (button?.isConnected) button.disabled = false;
2368
+ }
2369
+ }
1941
2370
  async handleCta() {
1942
- const cta = this.els.cta;
1943
- if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
1944
- const seats = this.hold.seats ?? this.controller.getSelection();
2371
+ const committed = this.committedSelection();
2372
+ if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
2373
+ const seats = this.hold.seats ?? committed;
1945
2374
  this.handedOff = true;
2375
+ this.setCtaPhase("checkout");
1946
2376
  this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
1947
2377
  return;
1948
2378
  }
1949
- cta.disabled = true;
1950
- cta.textContent = "Holding\u2026";
2379
+ this.holdingLabels = new Set(committed.map((seat) => seat.label));
2380
+ this.setCtaPhase("holding");
1951
2381
  try {
1952
2382
  let hold = null;
1953
2383
  const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
1954
- const chosenSeats = this.controller.getSelection();
2384
+ const chosenSeats = this.committedSelection();
1955
2385
  if (chosenSeats.length) {
1956
2386
  const h = await this.controller.hold(void 0, this.opts.holdTtlMs);
1957
2387
  hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
@@ -1961,31 +2391,43 @@ var SeatPicker = class _SeatPicker {
1961
2391
  hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : hold;
1962
2392
  }
1963
2393
  if (!hold) {
1964
- this.toast("One or more seats were just taken. Please pick again.");
2394
+ this.toast("One or more seats were just taken. Please pick again.", "error");
2395
+ this.setCtaPhase("idle");
1965
2396
  this.syncTray();
1966
2397
  return;
1967
2398
  }
1968
2399
  this.hold = hold;
1969
2400
  this.handedOff = true;
1970
2401
  this.startHoldTimer(hold.expiresAt);
1971
- this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? [], this.buildHandoff(hold));
2402
+ this.flashHeldSeats(hold);
2403
+ this.setCtaPhase("checkout");
2404
+ this.emitHoldChange();
2405
+ this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
1972
2406
  } catch (err) {
1973
2407
  this.opts.onError?.(err);
1974
- this.toast("One or more seats were just taken. Please pick again.");
2408
+ this.toast("One or more seats were just taken. Please pick again.", "error");
2409
+ this.setCtaPhase("idle");
1975
2410
  } finally {
2411
+ this.holdingLabels.clear();
2412
+ if (this.ctaPhase === "holding") this.ctaPhase = "idle";
1976
2413
  this.syncTray();
1977
2414
  }
1978
2415
  }
1979
2416
  startHoldTimer(expiresAt) {
1980
2417
  this.stopHoldTimer();
1981
2418
  this.holdExpiresAt = expiresAt;
2419
+ if (this.hold) this.rememberHold(this.hold);
1982
2420
  const pill = this.els.hold;
2421
+ pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
2422
+ const time = pill.querySelector('[data-ref="holdTime"]');
2423
+ this.els.holdNote?.classList.add("on");
1983
2424
  const tick = () => {
1984
2425
  const ms = Math.max(0, this.holdExpiresAt - Date.now());
1985
2426
  const m = Math.floor(ms / 6e4);
1986
2427
  const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
1987
- pill.textContent = `Held ${m}:${s}`;
2428
+ if (time) time.textContent = `${m}:${s}`;
1988
2429
  pill.classList.add("on");
2430
+ pill.classList.toggle("is-expiring", ms > 0 && ms <= EXTEND_PROMPT_MS);
1989
2431
  this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
1990
2432
  if (ms <= 0) this.stopHoldTimer();
1991
2433
  };
@@ -1995,7 +2437,8 @@ var SeatPicker = class _SeatPicker {
1995
2437
  stopHoldTimer() {
1996
2438
  if (this.holdTimer) clearInterval(this.holdTimer);
1997
2439
  this.holdTimer = null;
1998
- this.els.hold?.classList.remove("on");
2440
+ this.els.hold?.classList.remove("on", "is-expiring");
2441
+ this.els.holdNote?.classList.remove("on");
1999
2442
  this.setExtendPrompt(false, 0);
2000
2443
  }
2001
2444
  /** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
@@ -2020,13 +2463,15 @@ var SeatPicker = class _SeatPicker {
2020
2463
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
2021
2464
  this.holdExpiresAt = h.expiresAt;
2022
2465
  this.extendEl?.classList.remove("on");
2023
- this.toast("More time added \u2014 your seats are still held.");
2466
+ this.rememberHold(this.hold);
2467
+ this.emitHoldChange();
2468
+ this.toast("More time added \u2014 your seats are still held.", "success");
2024
2469
  } else {
2025
- this.toast("Couldn't add more time \u2014 please head to checkout now.");
2470
+ this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
2026
2471
  }
2027
2472
  } catch (err) {
2028
2473
  this.opts.onError?.(err);
2029
- this.toast("Couldn't add more time \u2014 please head to checkout now.");
2474
+ this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
2030
2475
  } finally {
2031
2476
  btn.disabled = false;
2032
2477
  btn.textContent = prev;
@@ -2050,6 +2495,7 @@ var SeatPicker = class _SeatPicker {
2050
2495
  this.bookedShown = true;
2051
2496
  const handoff = this.buildHandoff(this.hold);
2052
2497
  this.stopHoldTimer();
2498
+ this.forgetHold();
2053
2499
  const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
2054
2500
  if (this.els.bookedSub) {
2055
2501
  this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
@@ -2074,13 +2520,25 @@ var SeatPicker = class _SeatPicker {
2074
2520
  const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
2075
2521
  return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
2076
2522
  }
2077
- toast(msg) {
2523
+ emitHoldChange() {
2524
+ const hold = this.hold;
2525
+ this.opts.onHoldChange?.(
2526
+ hold,
2527
+ hold?.seats ?? [],
2528
+ hold ? this.buildHandoff(hold) : null
2529
+ );
2530
+ }
2531
+ toast(msg, tone = "neutral") {
2078
2532
  const el = this.els.toast;
2079
2533
  if (!el) return;
2080
2534
  el.textContent = msg;
2535
+ el.dataset.tone = tone;
2081
2536
  el.classList.add("on");
2082
2537
  if (this.toastTimer) clearTimeout(this.toastTimer);
2083
- this.toastTimer = setTimeout(() => el.classList.remove("on"), 4200);
2538
+ this.toastTimer = setTimeout(() => {
2539
+ el.classList.remove("on");
2540
+ el.dataset.tone = "neutral";
2541
+ }, 4200);
2084
2542
  }
2085
2543
  placeTooltip() {
2086
2544
  if (!this.tipEl) return;
@@ -2101,15 +2559,40 @@ var SeatPicker = class _SeatPicker {
2101
2559
  return;
2102
2560
  }
2103
2561
  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" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
2104
- 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;
2562
+ const location = [
2563
+ details.sectionLabel ? `Section ${details.sectionLabel}` : "",
2564
+ details.rowLabel ? `Row ${details.rowLabel}` : "",
2565
+ details.seatNumber ? `Seat ${details.seatNumber}` : details.label
2566
+ ].filter(Boolean).join(" \xB7 ");
2567
+ 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;
2105
2568
  this.tipEl.style.display = "block";
2106
2569
  this.placeTooltip();
2107
2570
  }
2108
2571
  // ---- public conveniences ----------------------------------------------------
2109
2572
  getSelection() {
2110
- return this.controller.getSelection();
2573
+ return this.committedSelection();
2574
+ }
2575
+ /** Current active/restored hold reflected in the tray. */
2576
+ getCurrentHold() {
2577
+ return this.hold;
2578
+ }
2579
+ /** Explicit host-driven hold restore (automatic session restore is on by default). */
2580
+ async resumeHold(holdId) {
2581
+ return this.resumeHoldFromServer(holdId, false);
2582
+ }
2583
+ /** Remove one server-held ticket while keeping the rest of the hold active. */
2584
+ async removeHeldTicket(label) {
2585
+ return this.removeHeldLabel(label);
2111
2586
  }
2112
2587
  async bestAvailable(qty, categoryKey) {
2588
+ if (this.bestAvailableBusy) return null;
2589
+ if (this.confirmSeat) this.cancelConfirm();
2590
+ this.bestAvailableBusy = true;
2591
+ const button = this.els.tray?.querySelector(".sl-ba-go");
2592
+ if (button) {
2593
+ button.disabled = true;
2594
+ button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
2595
+ }
2113
2596
  try {
2114
2597
  const h = await this.controller.bestAvailable(qty, categoryKey);
2115
2598
  if (h) {
@@ -2117,30 +2600,67 @@ var SeatPicker = class _SeatPicker {
2117
2600
  this.handedOff = false;
2118
2601
  this.bookedShown = false;
2119
2602
  this.startHoldTimer(h.expiresAt);
2603
+ this.flashHeldSeats(this.hold);
2120
2604
  this.syncTray();
2605
+ this.emitHoldChange();
2121
2606
  return this.hold;
2122
2607
  }
2123
2608
  return null;
2124
2609
  } catch (err) {
2125
2610
  this.opts.onError?.(err);
2611
+ this.toast("Those seats are no longer available. Try another quantity or category.", "error");
2126
2612
  return null;
2613
+ } finally {
2614
+ this.bestAvailableBusy = false;
2615
+ if (!this.hold && button?.isConnected) {
2616
+ button.disabled = false;
2617
+ button.textContent = "Best available";
2618
+ }
2127
2619
  }
2128
2620
  }
2129
2621
  async release() {
2130
- await this.controller.release();
2622
+ const tracked = this.hold;
2623
+ const controllerHold = this.controller.currentHold();
2624
+ let released = true;
2625
+ if (controllerHold) {
2626
+ released = await this.controller.release();
2627
+ } else if (tracked) {
2628
+ const labels = [.../* @__PURE__ */ new Set([
2629
+ ...(tracked.items ?? []).map((item) => item.label),
2630
+ ...(tracked.seats ?? []).map((seat) => seat.label)
2631
+ ])];
2632
+ if (labels.length) {
2633
+ try {
2634
+ await this.api.release(this.opts.event, labels, tracked.holdId);
2635
+ } catch (error) {
2636
+ this.opts.onError?.(error);
2637
+ released = false;
2638
+ }
2639
+ }
2640
+ }
2641
+ if (!released) {
2642
+ this.toast("Couldn't release your tickets. Your hold is unchanged.", "error");
2643
+ return;
2644
+ }
2131
2645
  this.hold = null;
2646
+ this.forgetHold();
2132
2647
  this.handedOff = false;
2133
2648
  this.bookedShown = false;
2649
+ this.ctaPhase = "idle";
2134
2650
  this.stopHoldTimer();
2135
2651
  this.gaQty.clear();
2136
2652
  this.syncTray();
2653
+ this.emitHoldChange();
2137
2654
  }
2138
2655
  destroy() {
2139
2656
  this.destroyed = true;
2657
+ if (this.hold && !this.handedOff) void this.controller.release();
2140
2658
  this.closeConfirm();
2141
2659
  this.closeSeatView();
2142
2660
  this.stopHoldTimer();
2143
2661
  if (this.toastTimer) clearTimeout(this.toastTimer);
2662
+ for (const timer of this.motionTimers) clearTimeout(timer);
2663
+ this.motionTimers.clear();
2144
2664
  this.ro?.disconnect();
2145
2665
  this.ro = null;
2146
2666
  if (this.escHandler) document.removeEventListener("keydown", this.escHandler);