@seatlayer/js 0.5.0 → 0.7.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 +694 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +139 -1
- package/dist/index.d.ts +139 -1
- package/dist/index.js +699 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -373,9 +373,708 @@ var EmbeddedDesigner = class {
|
|
|
373
373
|
this.designerOrigin = "";
|
|
374
374
|
}
|
|
375
375
|
};
|
|
376
|
+
|
|
377
|
+
// src/SeatPicker.ts
|
|
378
|
+
import {
|
|
379
|
+
PickerController as PickerController2,
|
|
380
|
+
expandChart,
|
|
381
|
+
loadLocale as loadLocale2,
|
|
382
|
+
setStringOverrides as setStringOverrides2,
|
|
383
|
+
t as t2
|
|
384
|
+
} from "@seatlayer/core";
|
|
385
|
+
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
386
|
+
var DEFAULT_MAX_SELECTION2 = 10;
|
|
387
|
+
function resolveContainer3(container) {
|
|
388
|
+
if (typeof container === "string") {
|
|
389
|
+
const el = document.querySelector(container);
|
|
390
|
+
if (!el) throw new Error(`seatmap: container "${container}" not found`);
|
|
391
|
+
return el;
|
|
392
|
+
}
|
|
393
|
+
if (!(container instanceof HTMLElement)) {
|
|
394
|
+
throw new Error("seatmap: container must be a CSS selector or an HTMLElement");
|
|
395
|
+
}
|
|
396
|
+
return container;
|
|
397
|
+
}
|
|
398
|
+
var STYLE_ID = "seatlayer-picker-style";
|
|
399
|
+
var CSS = `
|
|
400
|
+
.sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
|
|
401
|
+
background:var(--sl-bg);color:var(--sl-text);font-family:var(--sl-font);border-radius:var(--sl-radius);
|
|
402
|
+
--sl-r-sm:calc(var(--sl-radius) * .55)}
|
|
403
|
+
.sl-picker *{box-sizing:border-box;margin:0;padding:0}
|
|
404
|
+
.sl-picker button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
|
405
|
+
|
|
406
|
+
/* header */
|
|
407
|
+
.sl-head{display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
|
|
408
|
+
.sl-logo{width:34px;height:34px;border-radius:9px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
409
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:800;font-size:15px;overflow:hidden}
|
|
410
|
+
.sl-logo img{width:100%;height:100%;object-fit:cover;display:block}
|
|
411
|
+
.sl-head-info{min-width:0;flex:1}
|
|
412
|
+
.sl-head-name{font-weight:700;font-size:15px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
413
|
+
.sl-head-meta{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);margin-top:3px;
|
|
414
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
415
|
+
.sl-hold-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
|
|
416
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums}
|
|
417
|
+
.sl-hold-pill.on{display:inline-flex}
|
|
418
|
+
.sl-close{width:32px;height:32px;border-radius:999px;flex:none;display:none;align-items:center;justify-content:center;
|
|
419
|
+
border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
|
|
420
|
+
.sl-close:hover{color:var(--sl-text);border-color:var(--sl-muted)}
|
|
421
|
+
.sl-close.on{display:inline-flex}
|
|
422
|
+
.sl-close svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round}
|
|
423
|
+
|
|
424
|
+
/* body */
|
|
425
|
+
.sl-body{display:flex;flex:1;min-height:0}
|
|
426
|
+
.sl-map{position:relative;flex:1;min-width:0}
|
|
427
|
+
.sl-map-host{position:absolute;inset:0}
|
|
428
|
+
.sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow-y:auto}
|
|
429
|
+
|
|
430
|
+
/* narrow (container < 640px): side panel becomes a bottom sheet */
|
|
431
|
+
.sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
|
|
432
|
+
.sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
|
|
433
|
+
.sl-picker[data-layout="narrow"] .sl-side{width:100%;max-height:46%;border-left:0;border-top:1px solid var(--sl-line)}
|
|
434
|
+
.sl-picker[data-layout="narrow"] .sl-tray{flex:none}
|
|
435
|
+
.sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
|
|
436
|
+
|
|
437
|
+
/* price panel */
|
|
438
|
+
.sl-sec{padding:14px 16px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
|
|
439
|
+
.sl-prices{padding:4px 16px 10px;border-bottom:1px solid var(--sl-line)}
|
|
440
|
+
.sl-price-row{display:flex;align-items:center;gap:8px;padding:5px 0;font-size:13px}
|
|
441
|
+
.sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
|
|
442
|
+
.sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
443
|
+
.sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
|
|
444
|
+
.sl-price-amt{font-weight:800;font-variant-numeric:tabular-nums}
|
|
445
|
+
|
|
446
|
+
/* tray */
|
|
447
|
+
.sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
448
|
+
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
449
|
+
.sl-chip{display:flex;align-items:center;gap:9px;padding:9px 11px;border:1px solid var(--sl-line);
|
|
450
|
+
border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px}
|
|
451
|
+
.sl-chip b{font-weight:800}
|
|
452
|
+
.sl-chip .cat{color:var(--sl-muted);font-size:11.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
453
|
+
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums}
|
|
454
|
+
.sl-chip .rm{width:22px;height:22px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
|
|
455
|
+
.sl-chip .rm:hover{color:var(--sl-text)}
|
|
456
|
+
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
457
|
+
|
|
458
|
+
/* GA rows */
|
|
459
|
+
.sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
|
|
460
|
+
.sl-ga-info{flex:1;min-width:0}
|
|
461
|
+
.sl-ga-name{font-weight:700;font-size:13px}
|
|
462
|
+
.sl-ga-sub{font-size:11px;color:var(--sl-muted);margin-top:2px}
|
|
463
|
+
.sl-ga-qty{display:flex;align-items:center;gap:8px}
|
|
464
|
+
.sl-ga-qty button{width:26px;height:26px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
465
|
+
font-size:15px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
466
|
+
.sl-ga-qty button:hover{border-color:var(--sl-muted)}
|
|
467
|
+
.sl-ga-qty span{min-width:16px;text-align:center;font-weight:800;font-variant-numeric:tabular-nums}
|
|
468
|
+
|
|
469
|
+
/* footer */
|
|
470
|
+
.sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
|
|
471
|
+
.sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
|
|
472
|
+
.sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
|
|
473
|
+
.sl-cta{width:100%;padding:13px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;
|
|
474
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);transition:filter .15s,opacity .15s}
|
|
475
|
+
.sl-cta:hover{filter:brightness(1.08)}
|
|
476
|
+
.sl-cta:disabled{opacity:.45;cursor:not-allowed}
|
|
477
|
+
|
|
478
|
+
/* zoom column */
|
|
479
|
+
.sl-zoom{position:absolute;right:12px;bottom:12px;display:flex;flex-direction:column;gap:6px;z-index:5}
|
|
480
|
+
.sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
481
|
+
color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
482
|
+
.sl-zoom button:hover{border-color:var(--sl-muted)}
|
|
483
|
+
.sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
484
|
+
|
|
485
|
+
/* toast + boot states */
|
|
486
|
+
.sl-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%) translateY(6px);z-index:8;max-width:88%;
|
|
487
|
+
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
|
|
488
|
+
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;white-space:nowrap;
|
|
489
|
+
overflow:hidden;text-overflow:ellipsis}
|
|
490
|
+
.sl-toast.on{opacity:1;transform:translateX(-50%) translateY(0)}
|
|
491
|
+
.sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
492
|
+
gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
|
|
493
|
+
.sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
|
|
494
|
+
animation:slspin .8s linear infinite}
|
|
495
|
+
@keyframes slspin{to{transform:rotate(360deg)}}
|
|
496
|
+
.sl-boot-title{font-weight:800;font-size:15px;color:var(--sl-text)}
|
|
497
|
+
.sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
|
|
498
|
+
color:var(--sl-accent-ink);font-weight:700;font-size:13px}
|
|
499
|
+
|
|
500
|
+
/* a11y filter chips (over the map, top-left) */
|
|
501
|
+
.sl-chips{position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:6px;flex-wrap:wrap;max-width:70%}
|
|
502
|
+
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
503
|
+
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
|
|
504
|
+
.sl-chip-f:hover{color:var(--sl-text)}
|
|
505
|
+
.sl-chip-f.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
|
|
506
|
+
|
|
507
|
+
/* confirm popover */
|
|
508
|
+
.sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
509
|
+
border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px))}
|
|
510
|
+
.sl-confirm-label{font-weight:800;font-size:14px}
|
|
511
|
+
.sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
|
|
512
|
+
.sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
|
|
513
|
+
.sl-confirm-row{display:flex;gap:8px;margin-top:10px}
|
|
514
|
+
.sl-confirm-row button{flex:1;padding:8px;border-radius:8px;font-weight:700;font-size:12.5px}
|
|
515
|
+
.sl-confirm-add{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
516
|
+
.sl-confirm-cancel{border:1px solid var(--sl-line);color:var(--sl-muted)}
|
|
517
|
+
.sl-confirm-cancel:hover{color:var(--sl-text)}
|
|
518
|
+
|
|
519
|
+
/* best-available row */
|
|
520
|
+
.sl-ba{display:flex;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
|
|
521
|
+
.sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
|
|
522
|
+
font:inherit;font-size:12px;padding:5px 6px;max-width:110px}
|
|
523
|
+
.sl-ba-qty{display:flex;align-items:center;gap:7px}
|
|
524
|
+
.sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
525
|
+
font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
|
|
526
|
+
.sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
|
|
527
|
+
.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}
|
|
528
|
+
.sl-ba-go:hover{border-color:var(--sl-muted)}
|
|
529
|
+
|
|
530
|
+
/* screen-reader live region */
|
|
531
|
+
.sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
532
|
+
|
|
533
|
+
/* modal host */
|
|
534
|
+
.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}
|
|
535
|
+
.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)}
|
|
536
|
+
@media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
|
|
537
|
+
`;
|
|
538
|
+
function ensureStyle() {
|
|
539
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
540
|
+
const el = document.createElement("style");
|
|
541
|
+
el.id = STYLE_ID;
|
|
542
|
+
el.textContent = CSS;
|
|
543
|
+
document.head.appendChild(el);
|
|
544
|
+
}
|
|
545
|
+
function resolveTokens(chart, host) {
|
|
546
|
+
const accent = host?.accent ?? chart?.accent ?? "#f4b740";
|
|
547
|
+
const accentInk = host?.accentInk ?? chart?.accentInk ?? "#1a1200";
|
|
548
|
+
return {
|
|
549
|
+
"--sl-accent": accent,
|
|
550
|
+
"--sl-accent-ink": accentInk,
|
|
551
|
+
"--sl-bg": host?.background ?? chart?.background ?? "#0f1522",
|
|
552
|
+
"--sl-surface": host?.surface ?? "#1a2234",
|
|
553
|
+
"--sl-text": host?.text ?? chart?.textColor ?? "#eef1f8",
|
|
554
|
+
"--sl-muted": host?.muted ?? "#8b93a7",
|
|
555
|
+
"--sl-line": host?.line ?? "rgba(139,147,167,.22)",
|
|
556
|
+
"--sl-font": host?.fontFamily ?? chart?.fontFamily ?? "-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,sans-serif",
|
|
557
|
+
"--sl-radius": `${host?.radius ?? 14}px`
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
var SeatPicker = class _SeatPicker {
|
|
561
|
+
constructor(options) {
|
|
562
|
+
this.root = null;
|
|
563
|
+
this.mapHost = null;
|
|
564
|
+
this.rendered = false;
|
|
565
|
+
this.destroyed = false;
|
|
566
|
+
// chrome refs
|
|
567
|
+
this.els = {};
|
|
568
|
+
this.ro = null;
|
|
569
|
+
this.holdTimer = null;
|
|
570
|
+
this.toastTimer = null;
|
|
571
|
+
// state
|
|
572
|
+
this.currency = "USD";
|
|
573
|
+
this.hold = null;
|
|
574
|
+
this.gaQty = /* @__PURE__ */ new Map();
|
|
575
|
+
this.tipEl = null;
|
|
576
|
+
this.tipPos = { x: 0, y: 0 };
|
|
577
|
+
this.confirmEl = null;
|
|
578
|
+
this.confirmSeat = null;
|
|
579
|
+
this.srEl = null;
|
|
580
|
+
this.a11yFilter = "all";
|
|
581
|
+
this.baQty = 2;
|
|
582
|
+
this.baCat = "";
|
|
583
|
+
// modal plumbing (set by open())
|
|
584
|
+
this.modalScrim = null;
|
|
585
|
+
this.prevFocus = null;
|
|
586
|
+
this.escHandler = null;
|
|
587
|
+
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
588
|
+
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
589
|
+
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
590
|
+
this.opts = options;
|
|
591
|
+
const api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
|
|
592
|
+
this.controller = new PickerController2({
|
|
593
|
+
transport: api,
|
|
594
|
+
eventKey: options.event,
|
|
595
|
+
maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
|
|
596
|
+
currency: options.currency,
|
|
597
|
+
flashOnLiveChange: true,
|
|
598
|
+
colorblindSafe: options.colorblindSafe,
|
|
599
|
+
onSelectionChange: () => this.syncTray(),
|
|
600
|
+
onStatusChange: () => {
|
|
601
|
+
this.syncPrices();
|
|
602
|
+
this.evictTakenSelections();
|
|
603
|
+
},
|
|
604
|
+
onHoldExpired: () => {
|
|
605
|
+
this.hold = null;
|
|
606
|
+
this.stopHoldTimer();
|
|
607
|
+
this.gaQty.clear();
|
|
608
|
+
this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
609
|
+
this.syncTray();
|
|
610
|
+
this.opts.onHoldExpired?.();
|
|
611
|
+
},
|
|
612
|
+
confirmSelection: options.confirmSelection,
|
|
613
|
+
onSelect: (seat) => {
|
|
614
|
+
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
615
|
+
},
|
|
616
|
+
onViewChange: () => this.reanchorConfirm(),
|
|
617
|
+
onFocusSeat: (seat) => this.announceSeat(seat),
|
|
618
|
+
onSeatHover: (d) => this.updateTooltip(d),
|
|
619
|
+
onHint: (m) => {
|
|
620
|
+
if (m) this.toast(m);
|
|
621
|
+
},
|
|
622
|
+
onError: (err) => this.opts.onError?.(err)
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/** Mount the full picker as a document-level modal. Resolves after render. */
|
|
626
|
+
static async open(options) {
|
|
627
|
+
ensureStyle();
|
|
628
|
+
const scrim = document.createElement("div");
|
|
629
|
+
scrim.className = "sl-modal-scrim";
|
|
630
|
+
const frame = document.createElement("div");
|
|
631
|
+
frame.className = "sl-modal-frame";
|
|
632
|
+
scrim.appendChild(frame);
|
|
633
|
+
document.body.appendChild(scrim);
|
|
634
|
+
const prevOverflow = document.body.style.overflow;
|
|
635
|
+
document.body.style.overflow = "hidden";
|
|
636
|
+
const picker = new _SeatPicker({ ...options, container: frame });
|
|
637
|
+
picker.modalScrim = scrim;
|
|
638
|
+
picker.prevFocus = document.activeElement;
|
|
639
|
+
const close = () => {
|
|
640
|
+
document.body.style.overflow = prevOverflow;
|
|
641
|
+
picker.destroy();
|
|
642
|
+
options.onClose?.();
|
|
643
|
+
};
|
|
644
|
+
scrim.addEventListener("mousedown", (e) => {
|
|
645
|
+
if (e.target === scrim) close();
|
|
646
|
+
});
|
|
647
|
+
picker.escHandler = (e) => {
|
|
648
|
+
if (e.key === "Escape") close();
|
|
649
|
+
};
|
|
650
|
+
document.addEventListener("keydown", picker.escHandler);
|
|
651
|
+
await picker.render();
|
|
652
|
+
picker.els.close?.classList.add("on");
|
|
653
|
+
picker.els.close?.addEventListener("click", close);
|
|
654
|
+
return picker;
|
|
655
|
+
}
|
|
656
|
+
async render() {
|
|
657
|
+
if (this.rendered) return this;
|
|
658
|
+
this.rendered = true;
|
|
659
|
+
ensureStyle();
|
|
660
|
+
await loadLocale2(this.opts.locale);
|
|
661
|
+
if (this.opts.messages) setStringOverrides2(this.opts.messages);
|
|
662
|
+
const mount = resolveContainer3(this.opts.container);
|
|
663
|
+
const root = document.createElement("div");
|
|
664
|
+
root.className = "sl-picker";
|
|
665
|
+
this.root = root;
|
|
666
|
+
mount.appendChild(root);
|
|
667
|
+
Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
668
|
+
root.innerHTML = `
|
|
669
|
+
<div class="sl-head">
|
|
670
|
+
<div class="sl-logo" data-ref="logo"></div>
|
|
671
|
+
<div class="sl-head-info">
|
|
672
|
+
<div class="sl-head-name" data-ref="name"></div>
|
|
673
|
+
<div class="sl-head-meta" data-ref="meta"></div>
|
|
674
|
+
</div>
|
|
675
|
+
<span class="sl-hold-pill" data-ref="hold"></span>
|
|
676
|
+
<button type="button" class="sl-close" data-ref="close" aria-label="Close">
|
|
677
|
+
<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>
|
|
678
|
+
</button>
|
|
679
|
+
</div>
|
|
680
|
+
<div class="sl-body">
|
|
681
|
+
<div class="sl-map">
|
|
682
|
+
<div class="sl-map-host" data-ref="map"></div>
|
|
683
|
+
<div class="sl-zoom">
|
|
684
|
+
<button type="button" aria-label="Zoom in" data-ref="zin">+</button>
|
|
685
|
+
<button type="button" aria-label="Zoom out" data-ref="zout">\u2212</button>
|
|
686
|
+
<button type="button" aria-label="Fit to screen" data-ref="zfit">
|
|
687
|
+
<svg viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
|
|
688
|
+
</button>
|
|
689
|
+
</div>
|
|
690
|
+
<div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
|
|
691
|
+
<div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
|
|
692
|
+
</div>
|
|
693
|
+
<div class="sl-side">
|
|
694
|
+
<div class="sl-sec" data-ref="pricesSec">Prices</div>
|
|
695
|
+
<div class="sl-prices" data-ref="prices"></div>
|
|
696
|
+
<div class="sl-sec">Your seats</div>
|
|
697
|
+
<div class="sl-tray" data-ref="tray"></div>
|
|
698
|
+
<div class="sl-foot">
|
|
699
|
+
<div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
|
|
700
|
+
<button type="button" class="sl-cta" data-ref="cta" disabled></button>
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
</div>`;
|
|
704
|
+
root.querySelectorAll("[data-ref]").forEach((el) => {
|
|
705
|
+
this.els[el.dataset.ref] = el;
|
|
706
|
+
});
|
|
707
|
+
this.mapHost = this.els.map;
|
|
708
|
+
this.ro = new ResizeObserver(() => {
|
|
709
|
+
const w = root.clientWidth;
|
|
710
|
+
root.dataset.layout = w < 640 ? "narrow" : "wide";
|
|
711
|
+
});
|
|
712
|
+
this.ro.observe(root);
|
|
713
|
+
this.els.zin.addEventListener("click", () => this.controller.zoomIn());
|
|
714
|
+
this.els.zout.addEventListener("click", () => this.controller.zoomOut());
|
|
715
|
+
this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
|
|
716
|
+
this.tipEl = document.createElement("div");
|
|
717
|
+
this.tipEl.setAttribute("role", "tooltip");
|
|
718
|
+
this.tipEl.style.cssText = "position:absolute;z-index:7;pointer-events:none;display:none;max-width:240px;background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:10px;padding:9px 12px;font-size:12px;line-height:1.45;";
|
|
719
|
+
this.els.map.appendChild(this.tipEl);
|
|
720
|
+
this.els.map.addEventListener("mousemove", (e) => {
|
|
721
|
+
const r = this.els.map.getBoundingClientRect();
|
|
722
|
+
this.tipPos = { x: e.clientX - r.left, y: e.clientY - r.top };
|
|
723
|
+
if (this.tipEl && this.tipEl.style.display !== "none") this.placeTooltip();
|
|
724
|
+
});
|
|
725
|
+
this.els.cta.addEventListener("click", () => void this.handleCta());
|
|
726
|
+
const canvasHost = document.createElement("div");
|
|
727
|
+
canvasHost.style.cssText = "position:absolute;inset:0";
|
|
728
|
+
this.mapHost.appendChild(canvasHost);
|
|
729
|
+
const info = await this.controller.render(canvasHost);
|
|
730
|
+
if (this.destroyed) return this;
|
|
731
|
+
if (!info) {
|
|
732
|
+
this.els.boot.innerHTML = '<div class="sl-boot-title">The seat map didn\u2019t load</div><div>Check your connection and try again.</div><button type="button" class="sl-boot-retry">Try again</button>';
|
|
733
|
+
this.els.boot.querySelector("button").addEventListener("click", () => {
|
|
734
|
+
const container = this.opts.container;
|
|
735
|
+
const opts = this.opts;
|
|
736
|
+
this.destroy();
|
|
737
|
+
void new _SeatPicker({ ...opts, container }).render();
|
|
738
|
+
});
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
741
|
+
this.els.boot.remove();
|
|
742
|
+
const chartTheme = this.controller.doc?.theme;
|
|
743
|
+
Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
744
|
+
this.currency = info.currency ?? this.opts.currency ?? "USD";
|
|
745
|
+
const logoUrl = this.opts.theme?.logoUrl ?? chartTheme?.logoUrl;
|
|
746
|
+
if (logoUrl) this.els.logo.innerHTML = `<img src="${logoUrl}" alt="">`;
|
|
747
|
+
else this.els.logo.textContent = (this.opts.theme?.brandName ?? chartTheme?.brandName ?? info.eventName ?? "?").slice(0, 1).toUpperCase();
|
|
748
|
+
this.els.name.textContent = info.eventName ?? "";
|
|
749
|
+
const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
750
|
+
this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
|
|
751
|
+
const present = /* @__PURE__ */ new Set();
|
|
752
|
+
if (this.controller.doc) {
|
|
753
|
+
for (const seat of expandChart(this.controller.doc)) {
|
|
754
|
+
for (const type of seat.accessibility ?? []) present.add(type);
|
|
755
|
+
if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (present.size) {
|
|
759
|
+
const chips = document.createElement("div");
|
|
760
|
+
chips.className = "sl-chips";
|
|
761
|
+
const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
|
|
762
|
+
const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
|
|
763
|
+
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
764
|
+
this.els.map.appendChild(chips);
|
|
765
|
+
chips.querySelectorAll("button").forEach((btn) => {
|
|
766
|
+
btn.addEventListener("click", () => {
|
|
767
|
+
const f = btn.dataset.f;
|
|
768
|
+
this.a11yFilter = f;
|
|
769
|
+
chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
|
|
770
|
+
this.controller.setAccessibilityFilter(f === "all" ? null : [f]);
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
const cb = document.createElement("button");
|
|
775
|
+
cb.type = "button";
|
|
776
|
+
cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
|
|
777
|
+
cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
|
|
778
|
+
cb.innerHTML = '<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>';
|
|
779
|
+
this.els.zfit.parentElement.appendChild(cb);
|
|
780
|
+
let cbOn = !!this.opts.colorblindSafe;
|
|
781
|
+
cb.addEventListener("click", () => {
|
|
782
|
+
cbOn = !cbOn;
|
|
783
|
+
cb.setAttribute("aria-pressed", String(cbOn));
|
|
784
|
+
this.controller.setColorblindSafe(cbOn);
|
|
785
|
+
});
|
|
786
|
+
this.srEl = document.createElement("div");
|
|
787
|
+
this.srEl.className = "sl-sr";
|
|
788
|
+
this.srEl.setAttribute("aria-live", "polite");
|
|
789
|
+
root.appendChild(this.srEl);
|
|
790
|
+
this.syncPrices();
|
|
791
|
+
this.syncTray();
|
|
792
|
+
return this;
|
|
793
|
+
}
|
|
794
|
+
/** aria-live readout when keyboard focus lands on a seat. */
|
|
795
|
+
announceSeat(seat) {
|
|
796
|
+
if (!this.srEl) return;
|
|
797
|
+
if (!seat) {
|
|
798
|
+
this.srEl.textContent = "";
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
802
|
+
const status = this.controller.getStatus(seat.id) ?? "free";
|
|
803
|
+
const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
|
|
804
|
+
const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
|
|
805
|
+
this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
|
|
806
|
+
}
|
|
807
|
+
// ---- confirm popover (opt-in confirmSelection mode) ------------------------
|
|
808
|
+
showConfirm(seat) {
|
|
809
|
+
this.closeConfirm();
|
|
810
|
+
if (this.tipEl) this.tipEl.style.display = "none";
|
|
811
|
+
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
812
|
+
const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
|
|
813
|
+
const el = document.createElement("div");
|
|
814
|
+
el.className = "sl-confirm";
|
|
815
|
+
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><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>`;
|
|
816
|
+
this.els.map.appendChild(el);
|
|
817
|
+
this.confirmEl = el;
|
|
818
|
+
this.confirmSeat = seat;
|
|
819
|
+
this.reanchorConfirm();
|
|
820
|
+
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
|
|
821
|
+
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
|
|
822
|
+
this.controller.deselect([seat.id]);
|
|
823
|
+
this.closeConfirm();
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
reanchorConfirm() {
|
|
827
|
+
if (!this.confirmEl || !this.confirmSeat) return;
|
|
828
|
+
const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
|
|
829
|
+
this.confirmEl.style.left = `${p.x}px`;
|
|
830
|
+
this.confirmEl.style.top = `${p.y}px`;
|
|
831
|
+
}
|
|
832
|
+
closeConfirm() {
|
|
833
|
+
this.confirmEl?.remove();
|
|
834
|
+
this.confirmEl = null;
|
|
835
|
+
this.confirmSeat = null;
|
|
836
|
+
}
|
|
837
|
+
// ---- chrome sync ----------------------------------------------------------
|
|
838
|
+
money(n) {
|
|
839
|
+
try {
|
|
840
|
+
return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
|
|
841
|
+
} catch {
|
|
842
|
+
return `${n} ${this.currency}`;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
syncPrices() {
|
|
846
|
+
const doc = this.controller.doc;
|
|
847
|
+
if (!doc || !this.els.prices) return;
|
|
848
|
+
const left = this.controller.categoryAvailability();
|
|
849
|
+
this.els.prices.innerHTML = doc.categories.map((c) => {
|
|
850
|
+
const price = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
851
|
+
return `<div class="sl-price-row" 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>`;
|
|
852
|
+
}).join("");
|
|
853
|
+
this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
|
|
854
|
+
row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
|
|
855
|
+
row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
|
859
|
+
evictTakenSelections() {
|
|
860
|
+
const ownLabels = new Set(this.controller.currentHold()?.labels ?? []);
|
|
861
|
+
const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
|
|
862
|
+
if (!gone.length) return;
|
|
863
|
+
this.controller.deselect(gone.map((s) => s.id));
|
|
864
|
+
this.toast(`Seat ${gone[0].label} was just taken by another buyer.`);
|
|
865
|
+
}
|
|
866
|
+
syncTray() {
|
|
867
|
+
if (!this.els.tray) return;
|
|
868
|
+
const seats = this.controller.getSelection();
|
|
869
|
+
const gaAreas = this.controller.getGAAreas();
|
|
870
|
+
const heldItems = this.hold?.items ?? [];
|
|
871
|
+
const parts = [];
|
|
872
|
+
if (!seats.length && !heldItems.length && !gaAreas.length) {
|
|
873
|
+
parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
|
|
874
|
+
} else if (!seats.length && !heldItems.length) {
|
|
875
|
+
parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
|
|
876
|
+
}
|
|
877
|
+
for (const item of heldItems) {
|
|
878
|
+
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
879
|
+
parts.push(
|
|
880
|
+
`<div class="sl-chip"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span></div>`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
884
|
+
for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
|
|
885
|
+
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
886
|
+
parts.push(
|
|
887
|
+
`<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span><span class="amt">${this.money(s.price)}</span><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>`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
for (const area of gaAreas) {
|
|
891
|
+
const qty = this.gaQty.get(area.id) ?? 0;
|
|
892
|
+
parts.push(
|
|
893
|
+
`<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.label}</div><div class="sl-ga-sub">${this.money(area.price)} \xB7 ${area.available} left</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="Fewer">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="More">+</button></div></div>`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
if (!this.hold) {
|
|
897
|
+
const cats = this.controller.doc?.categories ?? [];
|
|
898
|
+
parts.push(
|
|
899
|
+
`<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>`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
this.els.tray.innerHTML = parts.join("");
|
|
903
|
+
this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
|
|
904
|
+
btn.addEventListener("click", () => {
|
|
905
|
+
this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
|
|
906
|
+
this.syncTray();
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
|
|
910
|
+
this.baCat = e.target.value;
|
|
911
|
+
});
|
|
912
|
+
this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
|
|
913
|
+
void this.bestAvailable(this.baQty, this.baCat || void 0);
|
|
914
|
+
});
|
|
915
|
+
this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
|
|
916
|
+
btn.addEventListener("click", () => {
|
|
917
|
+
const id = btn.closest(".sl-chip").dataset.seat;
|
|
918
|
+
this.controller.deselect([id]);
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
|
|
922
|
+
btn.addEventListener("click", () => {
|
|
923
|
+
const areaEl = btn.closest(".sl-ga");
|
|
924
|
+
const id = areaEl.dataset.ga;
|
|
925
|
+
const area = gaAreas.find((a) => a.id === id);
|
|
926
|
+
const next = Math.max(0, Math.min(area?.available ?? 0, (this.gaQty.get(id) ?? 0) + Number(btn.dataset.d)));
|
|
927
|
+
this.gaQty.set(id, next);
|
|
928
|
+
this.syncTray();
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
const gaTotal = gaAreas.reduce((sum, a) => sum + a.price * (this.gaQty.get(a.id) ?? 0), 0);
|
|
932
|
+
const gaCount = [...this.gaQty.values()].reduce((a, b) => a + b, 0);
|
|
933
|
+
const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
|
|
934
|
+
const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
935
|
+
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
936
|
+
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
937
|
+
const count = freshSeats.length + gaCount + heldCount;
|
|
938
|
+
this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
|
|
939
|
+
this.els.total.textContent = count ? this.money(total) : "";
|
|
940
|
+
const cta = this.els.cta;
|
|
941
|
+
cta.disabled = count === 0;
|
|
942
|
+
cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
|
|
943
|
+
this.opts.onSelectionChange?.(seats);
|
|
944
|
+
}
|
|
945
|
+
async handleCta() {
|
|
946
|
+
const cta = this.els.cta;
|
|
947
|
+
if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
948
|
+
this.opts.onCheckout?.(this.hold, this.controller.getSelection());
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
cta.disabled = true;
|
|
952
|
+
cta.textContent = "Holding\u2026";
|
|
953
|
+
try {
|
|
954
|
+
let hold = null;
|
|
955
|
+
const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
|
|
956
|
+
const chosenSeats = this.controller.getSelection();
|
|
957
|
+
if (chosenSeats.length) {
|
|
958
|
+
const h = await this.controller.hold(void 0, this.opts.holdTtlMs);
|
|
959
|
+
hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
|
|
960
|
+
}
|
|
961
|
+
for (const [areaId, qty] of gaEntries) {
|
|
962
|
+
const h = await this.controller.holdGA(areaId, qty, { ttlMs: this.opts.holdTtlMs });
|
|
963
|
+
hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : hold;
|
|
964
|
+
}
|
|
965
|
+
if (!hold) {
|
|
966
|
+
this.toast("One or more seats were just taken. Please pick again.");
|
|
967
|
+
this.syncTray();
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
this.hold = hold;
|
|
971
|
+
this.startHoldTimer(hold.expiresAt);
|
|
972
|
+
this.opts.onCheckout?.(hold, chosenSeats);
|
|
973
|
+
} catch (err) {
|
|
974
|
+
this.opts.onError?.(err);
|
|
975
|
+
this.toast("One or more seats were just taken. Please pick again.");
|
|
976
|
+
} finally {
|
|
977
|
+
this.syncTray();
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
startHoldTimer(expiresAt) {
|
|
981
|
+
this.stopHoldTimer();
|
|
982
|
+
const pill = this.els.hold;
|
|
983
|
+
const tick = () => {
|
|
984
|
+
const ms = Math.max(0, expiresAt - Date.now());
|
|
985
|
+
const m = Math.floor(ms / 6e4);
|
|
986
|
+
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
987
|
+
pill.textContent = `Held ${m}:${s}`;
|
|
988
|
+
pill.classList.add("on");
|
|
989
|
+
if (ms <= 0) this.stopHoldTimer();
|
|
990
|
+
};
|
|
991
|
+
tick();
|
|
992
|
+
this.holdTimer = setInterval(tick, 500);
|
|
993
|
+
}
|
|
994
|
+
stopHoldTimer() {
|
|
995
|
+
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
996
|
+
this.holdTimer = null;
|
|
997
|
+
this.els.hold?.classList.remove("on");
|
|
998
|
+
}
|
|
999
|
+
toast(msg) {
|
|
1000
|
+
const el = this.els.toast;
|
|
1001
|
+
if (!el) return;
|
|
1002
|
+
el.textContent = msg;
|
|
1003
|
+
el.classList.add("on");
|
|
1004
|
+
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
1005
|
+
this.toastTimer = setTimeout(() => el.classList.remove("on"), 4200);
|
|
1006
|
+
}
|
|
1007
|
+
placeTooltip() {
|
|
1008
|
+
if (!this.tipEl) return;
|
|
1009
|
+
const hw = this.els.map.clientWidth;
|
|
1010
|
+
const tw = this.tipEl.offsetWidth;
|
|
1011
|
+
const th = this.tipEl.offsetHeight;
|
|
1012
|
+
let x = this.tipPos.x + 14;
|
|
1013
|
+
let y = this.tipPos.y - th - 12;
|
|
1014
|
+
if (x + tw > hw - 8) x = this.tipPos.x - tw - 14;
|
|
1015
|
+
if (y < 8) y = this.tipPos.y + 18;
|
|
1016
|
+
this.tipEl.style.left = `${Math.max(8, x)}px`;
|
|
1017
|
+
this.tipEl.style.top = `${Math.max(8, y)}px`;
|
|
1018
|
+
}
|
|
1019
|
+
updateTooltip(details) {
|
|
1020
|
+
if (!this.tipEl) return;
|
|
1021
|
+
if (!details) {
|
|
1022
|
+
this.tipEl.style.display = "none";
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
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>`;
|
|
1026
|
+
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;
|
|
1027
|
+
this.tipEl.style.display = "block";
|
|
1028
|
+
this.placeTooltip();
|
|
1029
|
+
}
|
|
1030
|
+
// ---- public conveniences ----------------------------------------------------
|
|
1031
|
+
getSelection() {
|
|
1032
|
+
return this.controller.getSelection();
|
|
1033
|
+
}
|
|
1034
|
+
async bestAvailable(qty, categoryKey) {
|
|
1035
|
+
try {
|
|
1036
|
+
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
1037
|
+
if (h) {
|
|
1038
|
+
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1039
|
+
this.startHoldTimer(h.expiresAt);
|
|
1040
|
+
this.syncTray();
|
|
1041
|
+
return this.hold;
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
this.opts.onError?.(err);
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
async release() {
|
|
1050
|
+
await this.controller.release();
|
|
1051
|
+
this.hold = null;
|
|
1052
|
+
this.stopHoldTimer();
|
|
1053
|
+
this.gaQty.clear();
|
|
1054
|
+
this.syncTray();
|
|
1055
|
+
}
|
|
1056
|
+
destroy() {
|
|
1057
|
+
this.destroyed = true;
|
|
1058
|
+
this.closeConfirm();
|
|
1059
|
+
this.stopHoldTimer();
|
|
1060
|
+
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
1061
|
+
this.ro?.disconnect();
|
|
1062
|
+
this.ro = null;
|
|
1063
|
+
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
1064
|
+
this.controller.destroy();
|
|
1065
|
+
this.root?.remove();
|
|
1066
|
+
this.root = null;
|
|
1067
|
+
if (this.modalScrim) {
|
|
1068
|
+
this.modalScrim.remove();
|
|
1069
|
+
this.modalScrim = null;
|
|
1070
|
+
this.prevFocus?.focus?.();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
376
1074
|
export {
|
|
377
1075
|
ApiError,
|
|
378
1076
|
EmbeddedDesigner,
|
|
1077
|
+
SeatPicker,
|
|
379
1078
|
SeatingChart
|
|
380
1079
|
};
|
|
381
1080
|
//# sourceMappingURL=index.js.map
|