@seatlayer/js 0.18.0 → 0.18.1
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 +425 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -5
- package/dist/index.d.ts +111 -5
- package/dist/index.js +426 -60
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -662,6 +662,7 @@ import {
|
|
|
662
662
|
PickerController as PickerController2,
|
|
663
663
|
expandChart,
|
|
664
664
|
generateSeatPanorama,
|
|
665
|
+
generateSeatThumb,
|
|
665
666
|
loadLocale as loadLocale2,
|
|
666
667
|
setStringOverrides as setStringOverrides2,
|
|
667
668
|
t as t2,
|
|
@@ -826,26 +827,43 @@ var CSS = `
|
|
|
826
827
|
.sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
|
|
827
828
|
overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
828
829
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
829
|
-
.sl-chip{position:relative;display:grid;grid-template-columns:
|
|
830
|
-
min-height:53px;
|
|
830
|
+
.sl-chip{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 34px;align-items:stretch;
|
|
831
|
+
flex:none;min-height:53px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);overflow:hidden;
|
|
831
832
|
background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
|
|
832
833
|
.sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
|
|
833
834
|
.sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
834
835
|
.sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
|
|
835
836
|
.sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
836
837
|
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
|
|
837
|
-
.sl-ticket-state{width:
|
|
838
|
+
.sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
838
839
|
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
839
840
|
.sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
|
|
840
|
-
.sl-ticket-state svg{width:
|
|
841
|
-
.sl-chip-main{min-width:0}
|
|
842
|
-
.sl-chip
|
|
843
|
-
.sl-chip-
|
|
841
|
+
.sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
842
|
+
.sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
|
|
843
|
+
.sl-chip-id{display:flex;gap:12px;min-width:0}
|
|
844
|
+
.sl-chip-id .fld{min-width:0}
|
|
845
|
+
.sl-chip-id .fld.sec{flex:1}
|
|
846
|
+
.sl-chip-id .fld.mid{flex:none;text-align:center}
|
|
847
|
+
.sl-chip-eb{display:block;font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
|
|
848
|
+
.sl-chip-id .val{display:block;font-weight:600;font-size:13px;line-height:1.25;white-space:nowrap}
|
|
849
|
+
.sl-chip-id .fld.sec .val{overflow:hidden;text-overflow:ellipsis}
|
|
850
|
+
.sl-chip-sub{display:flex;align-items:center;gap:6px;min-width:0}
|
|
844
851
|
.sl-chip .cat{color:var(--sl-muted);font-size:10.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
845
852
|
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
|
|
846
|
-
.sl-chip
|
|
847
|
-
.sl-chip .rm
|
|
853
|
+
.sl-chip-rail{display:flex;flex-direction:column;border-left:1px solid var(--sl-line)}
|
|
854
|
+
.sl-chip .rm,.sl-chip .view{flex:1;min-height:26px;border-radius:0;display:flex;align-items:center;justify-content:center;
|
|
855
|
+
color:var(--sl-muted);transition:color .15s,background .15s}
|
|
856
|
+
.sl-chip .view{border-top:1px solid var(--sl-line)}
|
|
857
|
+
.sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:#e5484d;background:color-mix(in srgb,#e5484d 9%,transparent)}
|
|
858
|
+
.sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
|
|
848
859
|
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
860
|
+
.sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
|
|
861
|
+
/* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
|
|
862
|
+
.sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
|
|
863
|
+
border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
|
|
864
|
+
font-size:11px;color:var(--sl-muted)}
|
|
865
|
+
.sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
|
|
866
|
+
.sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
849
867
|
|
|
850
868
|
/* GA rows */
|
|
851
869
|
.sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
|
|
@@ -921,6 +939,8 @@ var CSS = `
|
|
|
921
939
|
|
|
922
940
|
/* zoom column (flows within the bottom-right region) */
|
|
923
941
|
.sl-zoom{display:flex;flex-direction:column;gap:6px}
|
|
942
|
+
/* CSS-fallback full screen (iOS Safari has no element fullscreen API) */
|
|
943
|
+
.sl-picker.sl-fs{position:fixed;inset:0;z-index:2147483000;width:auto;height:auto;max-height:none;border-radius:0}
|
|
924
944
|
.sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
925
945
|
color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
926
946
|
.sl-zoom button:hover{border-color:var(--sl-muted)}
|
|
@@ -979,6 +999,39 @@ var CSS = `
|
|
|
979
999
|
.sl-booked.on .sl-booked-title{animation-delay:.22s}
|
|
980
1000
|
.sl-booked.on .sl-booked-sub{animation-delay:.3s}
|
|
981
1001
|
|
|
1002
|
+
/* sold-out overlay \u2014 every SEATED category's live availability is 0. Centered
|
|
1003
|
+
over the map; a stub (disabled) "Join waitlist" button, exactly like the page.
|
|
1004
|
+
Suppressed when GA areas exist (GA capacity isn't seat-counted). Clears live
|
|
1005
|
+
the moment WS frees a seat up. */
|
|
1006
|
+
.sl-soldout{position:absolute;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;
|
|
1007
|
+
justify-content:center;text-align:center;gap:8px;padding:24px;
|
|
1008
|
+
background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
|
|
1009
|
+
.sl-soldout.on{display:flex}
|
|
1010
|
+
.sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
|
|
1011
|
+
.sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
|
|
1012
|
+
.sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
|
|
1013
|
+
.sl-picker .sl-soldout-btn{margin-top:10px;min-height:40px;padding:10px 18px;border-radius:var(--sl-r-sm);
|
|
1014
|
+
background:var(--sl-surface);color:var(--sl-muted);border:1px solid var(--sl-line);font-weight:800;font-size:13px;
|
|
1015
|
+
cursor:not-allowed;opacity:.85}
|
|
1016
|
+
|
|
1017
|
+
/* sales-closed pill (header) \u2014 persistent read-only state when the event's sales
|
|
1018
|
+
window is closed at load or closes live mid-session. Neutral (not accent) so it
|
|
1019
|
+
reads as "unavailable", distinct from the accent hold pill next to it. */
|
|
1020
|
+
.sl-closed-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
|
|
1021
|
+
background:color-mix(in srgb,var(--sl-text) 12%,var(--sl-surface));color:var(--sl-text);
|
|
1022
|
+
font-weight:700;font-size:12px;white-space:nowrap}
|
|
1023
|
+
.sl-closed-pill.on{display:inline-flex}
|
|
1024
|
+
.sl-closed-pill svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1025
|
+
|
|
1026
|
+
/* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the small gold
|
|
1027
|
+
rounded logo mark + wordmark. Hidden when the host opts out or the org's paid
|
|
1028
|
+
theme sets hideBadge. */
|
|
1029
|
+
.sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
|
|
1030
|
+
font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
|
|
1031
|
+
.sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
1032
|
+
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
1033
|
+
.sl-powered-mark svg{width:11px;height:11px;fill:currentColor}
|
|
1034
|
+
|
|
982
1035
|
/* a11y filter chips (flow within the top-left region) */
|
|
983
1036
|
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
984
1037
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
@@ -1017,8 +1070,30 @@ var CSS = `
|
|
|
1017
1070
|
.sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
|
|
1018
1071
|
transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
1019
1072
|
|
|
1073
|
+
/* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
|
|
1074
|
+
a passing preview on hover, not the click/select action surface). Reuses the
|
|
1075
|
+
Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
|
|
1076
|
+
one visual language, just at three sizes. */
|
|
1077
|
+
.sl-tip{position:absolute;z-index:7;pointer-events:none;display:none;width:190px;overflow:hidden;
|
|
1078
|
+
background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:11px;
|
|
1079
|
+
box-shadow:0 12px 30px -14px rgba(0,0,0,.6)}
|
|
1080
|
+
.sl-tip-grid{display:grid;grid-template-columns:1.3fr .85fr .85fr;border-bottom:1px solid var(--sl-line)}
|
|
1081
|
+
.sl-tip-grid.one{grid-template-columns:1fr}
|
|
1082
|
+
.sl-tip-field{min-width:0;padding:6px 9px;border-right:1px solid var(--sl-line)}
|
|
1083
|
+
.sl-tip-field:last-child{border-right:0;text-align:center}
|
|
1084
|
+
.sl-tip-grid:not(.one) .sl-tip-field:nth-child(2){text-align:center}
|
|
1085
|
+
.sl-tip-key{display:block;font-size:7.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
|
|
1086
|
+
.sl-tip-val{display:block;margin-top:2px;color:var(--sl-text);font-size:13px;line-height:1.1;font-weight:750;
|
|
1087
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1088
|
+
.sl-tip-cat{display:flex;align-items:center;gap:7px;padding:6px 10px;font-size:11px;
|
|
1089
|
+
background:color-mix(in srgb,var(--sl-cat) 12%,var(--sl-surface))}
|
|
1090
|
+
.sl-tip-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
1091
|
+
.sl-tip-name{color:var(--sl-muted);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1092
|
+
.sl-tip-amt{margin-left:auto;font-weight:800;color:var(--sl-text);font-variant-numeric:tabular-nums;font-size:12px}
|
|
1093
|
+
.sl-tip-status{padding:5px 10px 7px;font-size:8.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
|
|
1094
|
+
|
|
1020
1095
|
/* Best available is a first-class shortcut, not an anonymous utility row. */
|
|
1021
|
-
.sl-ba{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
1096
|
+
.sl-ba{position:relative;flex:none;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
1022
1097
|
padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
|
|
1023
1098
|
background:linear-gradient(135deg,color-mix(in srgb,var(--sl-accent) 5%,var(--sl-surface)),color-mix(in srgb,var(--sl-accent) 11%,var(--sl-surface)))}
|
|
1024
1099
|
.sl-ba::after{content:'\u2726';position:absolute;right:10px;top:3px;color:color-mix(in srgb,var(--sl-accent) 20%,transparent);font-size:42px;line-height:1}
|
|
@@ -1054,12 +1129,6 @@ var CSS = `
|
|
|
1054
1129
|
/* per-seat ticket-tier select + view-from-seat button in tray chips */
|
|
1055
1130
|
.sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
|
|
1056
1131
|
font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
|
|
1057
|
-
.sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
1058
|
-
color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
|
|
1059
|
-
.sl-chip .view:hover{color:var(--sl-text)}
|
|
1060
|
-
.sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
|
|
1061
|
-
.sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1062
|
-
@media(pointer:coarse){.sl-chip .view{opacity:.58}}
|
|
1063
1132
|
|
|
1064
1133
|
/* arena: LOD rung pills (flow within the top-center region) */
|
|
1065
1134
|
.sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
@@ -1117,6 +1186,14 @@ var CSS = `
|
|
|
1117
1186
|
.sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
|
|
1118
1187
|
|
|
1119
1188
|
/* view-from-seat button on the confirm popover */
|
|
1189
|
+
/* Eager sightline preview inside the confirm card */
|
|
1190
|
+
.sl-confirm-thumbwrap{position:relative;display:block;width:100%;height:74px;margin:0 0 8px;padding:0!important;
|
|
1191
|
+
border-radius:9px;overflow:hidden;border:1px solid var(--sl-line);cursor:pointer}
|
|
1192
|
+
.sl-confirm-thumb{display:block;width:100%;height:100%;object-fit:cover}
|
|
1193
|
+
.sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
|
|
1194
|
+
font-size:10px;font-weight:700;color:#fff;background:rgba(10,14,22,0.72);border-radius:12px;padding:4px 9px;backdrop-filter:blur(3px)}
|
|
1195
|
+
.sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
|
|
1196
|
+
.sl-confirm-sight span{color:#22a06b;font-weight:800}
|
|
1120
1197
|
.sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1121
1198
|
color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
1122
1199
|
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
@@ -1200,6 +1277,22 @@ function resolveTokens(chart, host) {
|
|
|
1200
1277
|
"--sl-radius": `${host?.radius ?? 14}px`
|
|
1201
1278
|
};
|
|
1202
1279
|
}
|
|
1280
|
+
var CB_STORAGE_KEY = "seatmap.a11y.cb";
|
|
1281
|
+
function readStoredColorblind() {
|
|
1282
|
+
try {
|
|
1283
|
+
if (typeof window === "undefined") return null;
|
|
1284
|
+
const raw = window.localStorage.getItem(CB_STORAGE_KEY);
|
|
1285
|
+
return raw == null ? null : raw === "1";
|
|
1286
|
+
} catch {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function writeStoredColorblind(on) {
|
|
1291
|
+
try {
|
|
1292
|
+
window.localStorage.setItem(CB_STORAGE_KEY, on ? "1" : "0");
|
|
1293
|
+
} catch {
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1203
1296
|
var SeatPicker = class _SeatPicker {
|
|
1204
1297
|
constructor(options) {
|
|
1205
1298
|
this.root = null;
|
|
@@ -1232,11 +1325,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
1232
1325
|
this.confirmEl = null;
|
|
1233
1326
|
this.confirmSeat = null;
|
|
1234
1327
|
this.srEl = null;
|
|
1235
|
-
this.a11yFilter = "all";
|
|
1236
1328
|
this.baQty = 2;
|
|
1237
1329
|
this.baCat = "";
|
|
1238
1330
|
this.bestAvailableConfirm = false;
|
|
1239
1331
|
this.releasingHold = false;
|
|
1332
|
+
/** Event sales window is closed (read-only load state / live close). */
|
|
1333
|
+
this.salesClosed = false;
|
|
1334
|
+
/** Every seated category's live availability is 0 (sold-out overlay is up). */
|
|
1335
|
+
this.soldOut = false;
|
|
1336
|
+
this.soldoutEl = null;
|
|
1337
|
+
/** Resolved colorblind-safe state — stored preference wins over the option. */
|
|
1338
|
+
this.cbSafe = false;
|
|
1240
1339
|
// arena / multi-floor / seat-view chrome
|
|
1241
1340
|
this.rungsEl = null;
|
|
1242
1341
|
this.floorsEl = null;
|
|
@@ -1245,13 +1344,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1245
1344
|
this.viewCleanup = null;
|
|
1246
1345
|
this.allSeatsCache = null;
|
|
1247
1346
|
// F3 minimap
|
|
1248
|
-
this.miniEl = null;
|
|
1249
1347
|
this.miniCanvas = null;
|
|
1250
1348
|
this.miniBase = null;
|
|
1251
1349
|
this.miniTf = null;
|
|
1252
1350
|
// F4 price-band filter — active band's category keys (null = all prices)
|
|
1253
1351
|
this.priceBandKeys = null;
|
|
1254
|
-
this.priceFilterEl = null;
|
|
1255
1352
|
/** Last surfaced section summary (re-rendered when the price band changes). */
|
|
1256
1353
|
this.lastSection = null;
|
|
1257
1354
|
/** Section card collapsed to its slim pill (seat-picking has begun). */
|
|
@@ -1271,6 +1368,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1271
1368
|
this.ctaPhase = "idle";
|
|
1272
1369
|
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
1273
1370
|
this.a11yChipsEl = null;
|
|
1371
|
+
this.fsFallback = false;
|
|
1372
|
+
this.fsChangeHandler = null;
|
|
1373
|
+
this.fsEscHandler = null;
|
|
1274
1374
|
this.cbEl = null;
|
|
1275
1375
|
// modal plumbing (set by open())
|
|
1276
1376
|
this.modalScrim = null;
|
|
@@ -1278,20 +1378,22 @@ var SeatPicker = class _SeatPicker {
|
|
|
1278
1378
|
this.escHandler = null;
|
|
1279
1379
|
/** Set by open(): closes the modal (scroll restore + destroy + onClose). */
|
|
1280
1380
|
this.closeModal = null;
|
|
1381
|
+
this.lastCatAvail = null;
|
|
1281
1382
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
1282
1383
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
1283
1384
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
1284
1385
|
this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
|
|
1285
1386
|
this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
|
|
1286
|
-
this.api = new PubApi(this.apiBase);
|
|
1387
|
+
this.api = options.transport ?? new PubApi(this.apiBase);
|
|
1287
1388
|
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
1389
|
+
this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
|
|
1288
1390
|
this.controller = new PickerController2({
|
|
1289
1391
|
transport: this.api,
|
|
1290
1392
|
eventKey: options.event,
|
|
1291
1393
|
maxSelection: this.maxTickets,
|
|
1292
1394
|
currency: options.currency,
|
|
1293
1395
|
flashOnLiveChange: true,
|
|
1294
|
-
colorblindSafe:
|
|
1396
|
+
colorblindSafe: this.cbSafe,
|
|
1295
1397
|
onSelectionChange: () => {
|
|
1296
1398
|
this.syncTray();
|
|
1297
1399
|
if (this.committedSelection().length) this.collapseSectionCard();
|
|
@@ -1317,6 +1419,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1317
1419
|
},
|
|
1318
1420
|
confirmSelection: this.opts.confirmSelection,
|
|
1319
1421
|
onSelect: (seat) => {
|
|
1422
|
+
if (this.salesClosed) {
|
|
1423
|
+
this.controller.deselect([seat.id]);
|
|
1424
|
+
this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1320
1427
|
this.flashPickedSeat(seat.id);
|
|
1321
1428
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
1322
1429
|
},
|
|
@@ -1339,9 +1446,68 @@ var SeatPicker = class _SeatPicker {
|
|
|
1339
1446
|
onHint: (m) => {
|
|
1340
1447
|
if (m) this.toast(m);
|
|
1341
1448
|
},
|
|
1449
|
+
// Server declared the event closed mid-session (409 event_closed) — keep
|
|
1450
|
+
// the toast (raised by handleCta), and add the persistent read-only state.
|
|
1451
|
+
onSalesClosed: () => this.setSalesClosed(true),
|
|
1342
1452
|
onError: (err) => this.opts.onError?.(err)
|
|
1343
1453
|
});
|
|
1344
1454
|
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Eager sightline preview for the confirm card: a cheap generated forward
|
|
1457
|
+
* view (or the organizer's real photo) plus a "Nm to stage · clear
|
|
1458
|
+
* sightline" line — the premium at-a-glance moment; click opens the 360.
|
|
1459
|
+
*/
|
|
1460
|
+
confirmThumbHtml(seat) {
|
|
1461
|
+
const doc = this.controller.doc;
|
|
1462
|
+
if (!doc) return "";
|
|
1463
|
+
let url = seat.viewUrl ?? "";
|
|
1464
|
+
let distance = null;
|
|
1465
|
+
if (!url) {
|
|
1466
|
+
try {
|
|
1467
|
+
const thumb = generateSeatThumb(seat, doc.focalPoint);
|
|
1468
|
+
url = thumb.url;
|
|
1469
|
+
distance = thumb.distanceM ?? null;
|
|
1470
|
+
} catch {
|
|
1471
|
+
return "";
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
|
|
1475
|
+
return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${url}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button><div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${sight}</div>`;
|
|
1476
|
+
}
|
|
1477
|
+
/** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
|
|
1478
|
+
toggleFullscreen() {
|
|
1479
|
+
const root = this.root;
|
|
1480
|
+
if (!root) return;
|
|
1481
|
+
const active = !!document.fullscreenElement || this.fsFallback;
|
|
1482
|
+
if (!active) {
|
|
1483
|
+
if (root.requestFullscreen) {
|
|
1484
|
+
root.requestFullscreen().catch(() => this.setFsFallback(true));
|
|
1485
|
+
} else {
|
|
1486
|
+
this.setFsFallback(true);
|
|
1487
|
+
}
|
|
1488
|
+
} else if (document.fullscreenElement) {
|
|
1489
|
+
void document.exitFullscreen().catch(() => {
|
|
1490
|
+
});
|
|
1491
|
+
} else {
|
|
1492
|
+
this.setFsFallback(false);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
setFsFallback(on) {
|
|
1496
|
+
if (this.fsFallback === on) return;
|
|
1497
|
+
this.fsFallback = on;
|
|
1498
|
+
this.root?.classList.toggle("sl-fs", on);
|
|
1499
|
+
this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
|
|
1500
|
+
if (on && !this.fsEscHandler) {
|
|
1501
|
+
this.fsEscHandler = (e) => {
|
|
1502
|
+
if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
|
|
1503
|
+
};
|
|
1504
|
+
window.addEventListener("keydown", this.fsEscHandler);
|
|
1505
|
+
} else if (!on && this.fsEscHandler) {
|
|
1506
|
+
window.removeEventListener("keydown", this.fsEscHandler);
|
|
1507
|
+
this.fsEscHandler = null;
|
|
1508
|
+
}
|
|
1509
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1510
|
+
}
|
|
1345
1511
|
/**
|
|
1346
1512
|
* Close the picker. In modal mode (SeatPicker.open()) this dismisses the
|
|
1347
1513
|
* modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
|
|
@@ -1436,6 +1602,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1436
1602
|
<div class="sl-head-meta" data-ref="meta"></div>
|
|
1437
1603
|
</div>
|
|
1438
1604
|
<span class="sl-hold-pill" data-ref="hold"></span>
|
|
1605
|
+
<span class="sl-closed-pill" data-ref="closedPill" role="status">
|
|
1606
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
|
|
1607
|
+
<span data-ref="closedPillText"></span>
|
|
1608
|
+
</span>
|
|
1439
1609
|
<button type="button" class="sl-close" data-ref="close" aria-label="Close">
|
|
1440
1610
|
<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>
|
|
1441
1611
|
</button>
|
|
@@ -1449,6 +1619,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1449
1619
|
<button type="button" aria-label="Fit to screen" data-ref="zfit">
|
|
1450
1620
|
<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>
|
|
1451
1621
|
</button>
|
|
1622
|
+
<button type="button" aria-label="Full screen" aria-pressed="false" data-ref="zfs">
|
|
1623
|
+
<svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
|
1624
|
+
</button>
|
|
1452
1625
|
</div>
|
|
1453
1626
|
<div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
|
|
1454
1627
|
<div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
|
|
@@ -1467,6 +1640,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1467
1640
|
<div class="sl-filters" data-ref="filters"></div>
|
|
1468
1641
|
<div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
|
|
1469
1642
|
<div class="sl-prices" data-ref="prices"></div>
|
|
1643
|
+
<div class="sl-live" data-ref="live" role="status" aria-live="polite"><span class="dot" aria-hidden="true"></span><span data-ref="liveText">Live availability \u2014 seats update in real time</span></div>
|
|
1470
1644
|
<div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
|
|
1471
1645
|
<div class="sl-tray" data-ref="tray"></div>
|
|
1472
1646
|
<div class="sl-foot" data-ref="foot">
|
|
@@ -1500,6 +1674,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
1500
1674
|
this.els.zin.addEventListener("click", () => this.controller.zoomIn());
|
|
1501
1675
|
this.els.zout.addEventListener("click", () => this.controller.zoomOut());
|
|
1502
1676
|
this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
|
|
1677
|
+
this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
|
|
1678
|
+
this.fsChangeHandler = () => {
|
|
1679
|
+
if (!document.fullscreenElement) this.setFsFallback(false);
|
|
1680
|
+
this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback));
|
|
1681
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1682
|
+
};
|
|
1683
|
+
document.addEventListener("fullscreenchange", this.fsChangeHandler);
|
|
1503
1684
|
const head = this.els.sheetHead;
|
|
1504
1685
|
if (head) {
|
|
1505
1686
|
const toggle = this.els.sheetToggle;
|
|
@@ -1543,7 +1724,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1543
1724
|
}
|
|
1544
1725
|
this.tipEl = document.createElement("div");
|
|
1545
1726
|
this.tipEl.setAttribute("role", "tooltip");
|
|
1546
|
-
this.tipEl.
|
|
1727
|
+
this.tipEl.className = "sl-tip";
|
|
1547
1728
|
this.els.map.appendChild(this.tipEl);
|
|
1548
1729
|
this.els.map.addEventListener("mousemove", (e) => {
|
|
1549
1730
|
const r = this.els.map.getBoundingClientRect();
|
|
@@ -1568,6 +1749,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1568
1749
|
return this;
|
|
1569
1750
|
}
|
|
1570
1751
|
this.els.boot.remove();
|
|
1752
|
+
this.salesClosed = !!info.salesClosed;
|
|
1571
1753
|
this.buildRegions();
|
|
1572
1754
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
1573
1755
|
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
@@ -1587,6 +1769,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1587
1769
|
this.els.name.textContent = info.eventName ?? "";
|
|
1588
1770
|
const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
1589
1771
|
this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
|
|
1772
|
+
this.buildBadge(chartTheme);
|
|
1590
1773
|
const present = /* @__PURE__ */ new Set();
|
|
1591
1774
|
if (this.controller.doc) {
|
|
1592
1775
|
for (const seat of expandChart(this.controller.doc)) {
|
|
@@ -1602,12 +1785,29 @@ var SeatPicker = class _SeatPicker {
|
|
|
1602
1785
|
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
1603
1786
|
this.regions["top-left"].appendChild(chips);
|
|
1604
1787
|
this.a11yChipsEl = chips;
|
|
1788
|
+
const active = /* @__PURE__ */ new Set();
|
|
1789
|
+
const syncChips = () => {
|
|
1790
|
+
chips.querySelectorAll("button").forEach((b) => {
|
|
1791
|
+
const f = b.dataset.f;
|
|
1792
|
+
const on = f === "all" ? active.size === 0 : active.has(f);
|
|
1793
|
+
b.classList.toggle("on", on);
|
|
1794
|
+
b.setAttribute("aria-pressed", String(on));
|
|
1795
|
+
});
|
|
1796
|
+
const filter = active.size ? [...active] : null;
|
|
1797
|
+
this.controller.setAccessibilityFilter(filter);
|
|
1798
|
+
if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
|
|
1799
|
+
this.controller.setRung("seats");
|
|
1800
|
+
this.collapseSectionCard();
|
|
1801
|
+
this.syncRung();
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1605
1804
|
chips.querySelectorAll("button").forEach((btn) => {
|
|
1606
1805
|
btn.addEventListener("click", () => {
|
|
1607
1806
|
const f = btn.dataset.f;
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1807
|
+
if (f === "all") active.clear();
|
|
1808
|
+
else if (active.has(f)) active.delete(f);
|
|
1809
|
+
else active.add(f);
|
|
1810
|
+
syncChips();
|
|
1611
1811
|
});
|
|
1612
1812
|
});
|
|
1613
1813
|
}
|
|
@@ -1616,14 +1816,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1616
1816
|
cb.className = "sl-cbbtn";
|
|
1617
1817
|
this.cbEl = cb;
|
|
1618
1818
|
cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
|
|
1619
|
-
cb.setAttribute("aria-pressed", String(
|
|
1819
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1620
1820
|
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>';
|
|
1621
1821
|
this.els.zfit.parentElement.appendChild(cb);
|
|
1622
|
-
let cbOn = !!this.opts.colorblindSafe;
|
|
1623
1822
|
cb.addEventListener("click", () => {
|
|
1624
|
-
|
|
1625
|
-
cb.setAttribute("aria-pressed", String(
|
|
1626
|
-
this.controller.setColorblindSafe(
|
|
1823
|
+
this.cbSafe = !this.cbSafe;
|
|
1824
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1825
|
+
this.controller.setColorblindSafe(this.cbSafe);
|
|
1826
|
+
writeStoredColorblind(this.cbSafe);
|
|
1627
1827
|
});
|
|
1628
1828
|
this.srEl = document.createElement("div");
|
|
1629
1829
|
this.srEl.className = "sl-sr";
|
|
@@ -1634,9 +1834,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1634
1834
|
this.buildPriceFilter();
|
|
1635
1835
|
this.buildExtendPrompt();
|
|
1636
1836
|
this.buildBookedOverlay();
|
|
1837
|
+
this.buildSoldoutOverlay();
|
|
1637
1838
|
this.dockLayoutChrome();
|
|
1638
1839
|
await this.restoreRememberedHold();
|
|
1639
1840
|
if (this.destroyed) return this;
|
|
1841
|
+
if (this.salesClosed) this.applySalesClosed();
|
|
1640
1842
|
this.syncPrices();
|
|
1641
1843
|
this.syncTray();
|
|
1642
1844
|
return this;
|
|
@@ -1688,6 +1890,85 @@ var SeatPicker = class _SeatPicker {
|
|
|
1688
1890
|
this.bookedEl = el;
|
|
1689
1891
|
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
1690
1892
|
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Localized string with a literal fallback. `t()` returns the key itself for
|
|
1895
|
+
* unknown keys, so this collapses that to `fallback` — while still honoring a
|
|
1896
|
+
* host `messages` override (which makes `t()` return the override, not the key).
|
|
1897
|
+
*/
|
|
1898
|
+
tf(key, fallback) {
|
|
1899
|
+
const v = t2(key);
|
|
1900
|
+
return v === key ? fallback : v;
|
|
1901
|
+
}
|
|
1902
|
+
/** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
|
|
1903
|
+
buildSoldoutOverlay() {
|
|
1904
|
+
if (!this.els.map) return;
|
|
1905
|
+
const el = document.createElement("div");
|
|
1906
|
+
el.className = "sl-soldout";
|
|
1907
|
+
el.setAttribute("role", "status");
|
|
1908
|
+
const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
|
|
1909
|
+
el.innerHTML = `<div class="sl-soldout-eyebrow">${name}</div><div class="sl-soldout-title">${this.tf("picker.soldOutTitle", "Sold out")}</div><p class="sl-soldout-copy">${this.tf("picker.soldOutCopy", "Every seat is gone. Join the waitlist and we\u2019ll email you if seats are released.")}</p><button type="button" class="sl-soldout-btn" disabled>${this.tf("picker.waitlist", "Join waitlist")}</button>`;
|
|
1910
|
+
this.els.map.appendChild(el);
|
|
1911
|
+
this.soldoutEl = el;
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Recompute the sold-out state on every price/availability sync. Sold-out ⇔
|
|
1915
|
+
* every SEATED category's live free count is 0. Suppressed when the chart has
|
|
1916
|
+
* GA areas (GA capacity isn't per-seat, so seated counts would read 0 and
|
|
1917
|
+
* falsely block standing room) — mirrors the public page. Clears live when WS
|
|
1918
|
+
* frees a seat up.
|
|
1919
|
+
*/
|
|
1920
|
+
syncSoldout(categories, left) {
|
|
1921
|
+
const hasGA = this.controller.getGAAreas().length > 0;
|
|
1922
|
+
const soldOut = this.isSoldOut(categories, left, hasGA);
|
|
1923
|
+
if (soldOut === this.soldOut) return;
|
|
1924
|
+
this.soldOut = soldOut;
|
|
1925
|
+
this.soldoutEl?.classList.toggle("on", soldOut);
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Pure sold-out predicate: every SEATED category's free count is 0, there is at
|
|
1929
|
+
* least one seated category, and there are no GA areas (GA capacity isn't
|
|
1930
|
+
* per-seat, so seated counts read 0 and would falsely block standing room).
|
|
1931
|
+
* `left` is seeded implicitly — a missing key means a fully-booked tier (0 free).
|
|
1932
|
+
*/
|
|
1933
|
+
isSoldOut(categories, left, hasGA) {
|
|
1934
|
+
return !hasGA && categories.length > 0 && categories.every((c) => (left[c.key] ?? 0) === 0);
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Sales-closed read-only state (Gap 3): persistent header pill, disabled CTA
|
|
1938
|
+
* with a closed label, and frozen best-available / GA controls. `setSalesClosed`
|
|
1939
|
+
* is the reactive entry (live 409 event_closed); `applySalesClosed` is the
|
|
1940
|
+
* idempotent DOM apply used at load and on transition.
|
|
1941
|
+
*/
|
|
1942
|
+
setSalesClosed(closed) {
|
|
1943
|
+
if (this.salesClosed === closed) return;
|
|
1944
|
+
this.salesClosed = closed;
|
|
1945
|
+
this.applySalesClosed();
|
|
1946
|
+
}
|
|
1947
|
+
applySalesClosed() {
|
|
1948
|
+
const pill = this.els.closedPill;
|
|
1949
|
+
if (pill) {
|
|
1950
|
+
pill.classList.toggle("on", this.salesClosed);
|
|
1951
|
+
const text = this.els.closedPillText ?? pill;
|
|
1952
|
+
text.textContent = this.tf("picker.salesClosedPill", "Sales are closed");
|
|
1953
|
+
}
|
|
1954
|
+
this.root?.setAttribute("data-sales-closed", String(this.salesClosed));
|
|
1955
|
+
this.syncCta();
|
|
1956
|
+
this.syncTray();
|
|
1957
|
+
}
|
|
1958
|
+
/** The badge is hidden when the host opts out OR the org's theme sets hideBadge. */
|
|
1959
|
+
badgeHidden(chartTheme) {
|
|
1960
|
+
return !!(this.opts.hideBadge || chartTheme?.hideBadge);
|
|
1961
|
+
}
|
|
1962
|
+
/** Attribution badge in the side-panel foot (Gap 7). Hidden per host/theme. */
|
|
1963
|
+
buildBadge(chartTheme) {
|
|
1964
|
+
if (this.badgeHidden(chartTheme)) return;
|
|
1965
|
+
const foot = this.els.foot;
|
|
1966
|
+
if (!foot) return;
|
|
1967
|
+
const el = document.createElement("div");
|
|
1968
|
+
el.className = "sl-powered";
|
|
1969
|
+
el.innerHTML = `<span class="sl-powered-mark" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 15c0-1.1.9-2 2-2h12a2 2 0 0 1 2 2v3h-3v-2H7v2H4v-3Z"/><rect x="7" y="7" width="10" height="5" rx="1.6"/></svg></span><span>${this.tf("picker.poweredBy", "Powered by SeatLayer")}</span>`;
|
|
1970
|
+
foot.appendChild(el);
|
|
1971
|
+
}
|
|
1691
1972
|
// ---- Feature 6: chrome anchor regions -------------------------------------
|
|
1692
1973
|
/**
|
|
1693
1974
|
* Create the positioned flex containers that own every persistent map overlay.
|
|
@@ -1800,13 +2081,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1800
2081
|
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1801
2082
|
}
|
|
1802
2083
|
return gaAreas.reduce(
|
|
1803
|
-
(sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
2084
|
+
(sum, area) => sum + this.paidPrice(area.categoryKey, null, area.price) * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
1804
2085
|
0
|
|
1805
2086
|
);
|
|
1806
2087
|
}
|
|
1807
2088
|
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1808
2089
|
const cta = this.els.cta;
|
|
1809
2090
|
if (!cta) return;
|
|
2091
|
+
if (this.salesClosed) {
|
|
2092
|
+
cta.disabled = true;
|
|
2093
|
+
cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
1810
2096
|
if (this.confirmSeat) {
|
|
1811
2097
|
cta.disabled = true;
|
|
1812
2098
|
cta.textContent = "Confirm or cancel this seat";
|
|
@@ -1942,7 +2228,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
1942
2228
|
canvas.style.height = `${h}px`;
|
|
1943
2229
|
wrap.appendChild(canvas);
|
|
1944
2230
|
(this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
|
|
1945
|
-
this.miniEl = wrap;
|
|
1946
2231
|
this.miniCanvas = canvas;
|
|
1947
2232
|
const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
|
|
1948
2233
|
const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
|
|
@@ -2055,9 +2340,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
2055
2340
|
this.controller.overview();
|
|
2056
2341
|
}
|
|
2057
2342
|
// ---- F4 price-band filter -------------------------------------------------
|
|
2058
|
-
/** Effective price of a category
|
|
2343
|
+
/** Effective display price of a category: host pricing override → first tier → base. */
|
|
2059
2344
|
catPrice(c) {
|
|
2060
|
-
|
|
2345
|
+
const chart = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
2346
|
+
if (chart === void 0 || !c.key) return chart;
|
|
2347
|
+
return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
|
|
2061
2348
|
}
|
|
2062
2349
|
/** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
|
|
2063
2350
|
priceBands() {
|
|
@@ -2102,7 +2389,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
2102
2389
|
select.setAttribute("aria-label", "Filter and focus seats by price");
|
|
2103
2390
|
select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
|
|
2104
2391
|
this.els.pricesSec.appendChild(select);
|
|
2105
|
-
this.priceFilterEl = select;
|
|
2106
2392
|
select.addEventListener("change", () => {
|
|
2107
2393
|
const band = bands.find((candidate) => candidate.id === select.value);
|
|
2108
2394
|
const keys = band?.keys ?? null;
|
|
@@ -2206,7 +2492,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
2206
2492
|
renderSectionCard(summary) {
|
|
2207
2493
|
if (!this.els.map) return;
|
|
2208
2494
|
this.secCardEl?.remove();
|
|
2209
|
-
const
|
|
2495
|
+
const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
|
|
2496
|
+
const paidMin = Math.min(...paid);
|
|
2497
|
+
const paidMax = Math.max(...paid);
|
|
2498
|
+
const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
|
|
2210
2499
|
const leftLabel = tCount("picker.seatsLeftInSection", summary.seatsLeft);
|
|
2211
2500
|
const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${t2("picker.closeSectionSummary")}">\u2715</button>`;
|
|
2212
2501
|
const card = document.createElement("div");
|
|
@@ -2236,7 +2525,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2236
2525
|
card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
|
|
2237
2526
|
const mix = summary.categories.map((c) => {
|
|
2238
2527
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2239
|
-
return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(c.price)}</span></span>`;
|
|
2528
|
+
return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(this.paidPrice(c.key, null, c.price))}</span></span>`;
|
|
2240
2529
|
}).join("");
|
|
2241
2530
|
card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
|
|
2242
2531
|
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
@@ -2303,7 +2592,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2303
2592
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2304
2593
|
const status = this.controller.getStatus(seat.id) ?? "free";
|
|
2305
2594
|
const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
|
|
2306
|
-
const price = cat
|
|
2595
|
+
const price = cat ? this.catPrice(cat) : void 0;
|
|
2307
2596
|
this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
|
|
2308
2597
|
}
|
|
2309
2598
|
// ---- seat candidate confirmation ------------------------------------------
|
|
@@ -2318,7 +2607,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
2318
2607
|
if (this.tipEl) this.tipEl.style.display = "none";
|
|
2319
2608
|
const details = this.controller.seatDetails(seat.id);
|
|
2320
2609
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2321
|
-
const
|
|
2610
|
+
const chartPrice = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
|
|
2611
|
+
const price = chartPrice != null ? this.paidPrice(seat.categoryKey, details?.tierId ?? cat?.tiers?.[0]?.id ?? null, chartPrice) : void 0;
|
|
2322
2612
|
const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
|
|
2323
2613
|
"&": "&",
|
|
2324
2614
|
"<": "<",
|
|
@@ -2331,7 +2621,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2331
2621
|
el.setAttribute("aria-modal", "true");
|
|
2332
2622
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
2333
2623
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
2334
|
-
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
|
|
2624
|
+
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(this.rowShort(details))}</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() ? this.confirmThumbHtml(seat) : "") + `<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>`;
|
|
2335
2625
|
this.els.map.appendChild(el);
|
|
2336
2626
|
this.confirmEl = el;
|
|
2337
2627
|
this.reanchorConfirm();
|
|
@@ -2496,18 +2786,35 @@ var SeatPicker = class _SeatPicker {
|
|
|
2496
2786
|
}
|
|
2497
2787
|
// ---- chrome sync ----------------------------------------------------------
|
|
2498
2788
|
money(n) {
|
|
2789
|
+
const formatter = this.opts.pricing?.formatter;
|
|
2790
|
+
if (formatter) return formatter(n, this.currency);
|
|
2499
2791
|
try {
|
|
2500
2792
|
return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
|
|
2501
2793
|
} catch {
|
|
2502
2794
|
return `${n} ${this.currency}`;
|
|
2503
2795
|
}
|
|
2504
2796
|
}
|
|
2797
|
+
/**
|
|
2798
|
+
* The price the buyer will actually pay for a category (+tier): the host's
|
|
2799
|
+
* `pricing` override when present, else the chart's stored price. Every
|
|
2800
|
+
* price the widget DISPLAYS or hands off must flow through here — a map
|
|
2801
|
+
* that shows one price while checkout charges another destroys trust.
|
|
2802
|
+
*/
|
|
2803
|
+
paidPrice(categoryKey, tierId, fallback) {
|
|
2804
|
+
const entry = categoryKey ? this.opts.pricing?.prices?.[categoryKey] : void 0;
|
|
2805
|
+
if (entry === void 0) return fallback;
|
|
2806
|
+
if (typeof entry === "number") return entry;
|
|
2807
|
+
if (tierId && entry.tiers?.[tierId] !== void 0) return entry.tiers[tierId];
|
|
2808
|
+
return entry.base ?? fallback;
|
|
2809
|
+
}
|
|
2505
2810
|
syncPrices() {
|
|
2506
2811
|
const doc = this.controller.doc;
|
|
2507
2812
|
if (!doc || !this.els.prices) return;
|
|
2508
2813
|
const left = this.controller.categoryAvailability();
|
|
2814
|
+
this.narrateAvailability(doc.categories, left);
|
|
2815
|
+
this.syncSoldout(doc.categories, left);
|
|
2509
2816
|
this.els.prices.innerHTML = doc.categories.map((c) => {
|
|
2510
|
-
const price =
|
|
2817
|
+
const price = this.catPrice(c);
|
|
2511
2818
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2512
2819
|
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>`;
|
|
2513
2820
|
}).join("") + `<div class="sl-status-key" aria-label="Seat status legend"><span class="sl-status-item"><i class="sl-status-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></i>Temporarily held</span><span class="sl-status-item"><i class="sl-status-icon sold" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 17L17 7"/></svg></i>Sold</span></div>`;
|
|
@@ -2516,6 +2823,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2516
2823
|
row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
|
|
2517
2824
|
});
|
|
2518
2825
|
}
|
|
2826
|
+
/**
|
|
2827
|
+
* Live-activity strip: turn WS availability deltas into one quiet line of
|
|
2828
|
+
* social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
|
|
2829
|
+
* counts on every status change — no per-seat payload needed. Skips the very
|
|
2830
|
+
* first computation (initial load is not "activity").
|
|
2831
|
+
*/
|
|
2832
|
+
narrateAvailability(categories, left) {
|
|
2833
|
+
const textEl = this.els.liveText;
|
|
2834
|
+
const prev = this.lastCatAvail;
|
|
2835
|
+
this.lastCatAvail = { ...left };
|
|
2836
|
+
if (!textEl || !prev) return;
|
|
2837
|
+
for (const cat of categories) {
|
|
2838
|
+
const before = prev[cat.key];
|
|
2839
|
+
const now = left[cat.key] ?? 0;
|
|
2840
|
+
if (before === void 0 || now >= before) continue;
|
|
2841
|
+
const taken = before - now;
|
|
2842
|
+
textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2519
2846
|
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
|
2520
2847
|
evictTakenSelections() {
|
|
2521
2848
|
const ownLabels = /* @__PURE__ */ new Set([
|
|
@@ -2544,15 +2871,23 @@ var SeatPicker = class _SeatPicker {
|
|
|
2544
2871
|
const cats = this.controller.doc?.categories ?? [];
|
|
2545
2872
|
parts.push(this.bestAvailableConfirm ? `<div class="sl-ba" role="alert"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Replace your current choices?</div><div class="sl-ba-replace"><b>We\u2019ll find ${this.baQty} seats together.</b><span>Your manually selected tickets will be removed only after a new group is secured.</span></div><div class="sl-ba-actions"><button type="button" data-ba-cancel>Keep mine</button><button type="button" class="replace" data-ba-replace>Find new seats</button></div></div>` : `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + `<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 the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`);
|
|
2546
2873
|
}
|
|
2874
|
+
const idGrid = (seatId, label) => {
|
|
2875
|
+
const d = seatId ? this.controller.seatDetails(seatId) : null;
|
|
2876
|
+
if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
|
|
2877
|
+
return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
|
|
2878
|
+
}
|
|
2879
|
+
return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Section</span><span class="val">${d.sectionLabel ?? "\u2014"}</span></span>` + (d.rowLabel ? `<span class="fld mid"><span class="sl-chip-eb">Row</span><span class="val">${this.rowShort(d)}</span></span>` : "") + (d.seatNumber ? `<span class="fld mid"><span class="sl-chip-eb">Seat</span><span class="val">${d.seatNumber}</span></span>` : "") + `</div>`;
|
|
2880
|
+
};
|
|
2881
|
+
const iconRail = (rmAria, viewLabel) => `<div class="sl-chip-rail"><button type="button" class="rm" aria-label="${rmAria}"><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>` + (viewLabel ? `<button type="button" class="view" data-view-label="${viewLabel}" aria-label="${t2("picker.viewFromSeat", { label: viewLabel })}"><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>`;
|
|
2547
2882
|
for (const item of heldItems) {
|
|
2548
2883
|
const itemKey = `held:${item.label}`;
|
|
2549
2884
|
nextTrayKeys.add(itemKey);
|
|
2550
2885
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
2551
2886
|
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
2552
|
-
const
|
|
2553
|
-
const
|
|
2887
|
+
const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
|
|
2888
|
+
const canView2 = this.seatViewEnabled() && !!heldSeat;
|
|
2554
2889
|
parts.push(
|
|
2555
|
-
`<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"><span class="sl-ticket-state held" aria-label="Held for you" title="Held for you"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></span><
|
|
2890
|
+
`<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"${heldSeat ? ` data-locate="${heldSeat.id}"` : ""}><div class="sl-chip-main">` + idGrid(heldSeat?.id ?? null, item.label) + `<div class="sl-chip-sub"><span class="sl-ticket-state held" aria-label="Held for you" title="Held for you"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></span><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="amt">${this.money(this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1))}</span></div></div>` + iconRail(`Remove held ticket ${item.label}`, canView2 ? item.label : null) + `</div>`
|
|
2556
2891
|
);
|
|
2557
2892
|
}
|
|
2558
2893
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
@@ -2561,16 +2896,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2561
2896
|
const itemKey = `seat:${s.id}`;
|
|
2562
2897
|
nextTrayKeys.add(itemKey);
|
|
2563
2898
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
2564
|
-
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
|
|
2565
|
-
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${t2("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
|
|
2899
|
+
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(this.paidPrice(s.categoryKey, ti.id, ti.price))}</option>`).join("") + `</select>` : "";
|
|
2566
2900
|
parts.push(
|
|
2567
|
-
`<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}"><span class="sl-ticket-state" aria-label="Selected" title="Selected"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><
|
|
2901
|
+
`<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}" data-locate="${s.id}"><div class="sl-chip-main">` + idGrid(s.id, s.label) + `<div class="sl-chip-sub"><span class="sl-ticket-state" aria-label="Selected" title="Selected"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><span class="cat">${cat?.label ?? s.categoryKey}</span>${tierSelect}<span class="amt">${this.money(this.paidPrice(s.categoryKey, s.tierId ?? null, s.price))}</span></div></div>` + iconRail(`Remove ${s.label}`, canView ? s.label : null) + `</div>`
|
|
2568
2902
|
);
|
|
2569
2903
|
}
|
|
2570
2904
|
for (const area of gaAreas) {
|
|
2571
2905
|
const qty = this.gaQty.get(area.id) ?? 0;
|
|
2572
2906
|
parts.push(
|
|
2573
|
-
`<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>`
|
|
2907
|
+
`<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(this.paidPrice(area.categoryKey, null, 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>`
|
|
2574
2908
|
);
|
|
2575
2909
|
}
|
|
2576
2910
|
this.els.tray.innerHTML = parts.join("");
|
|
@@ -2610,7 +2944,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2610
2944
|
return;
|
|
2611
2945
|
}
|
|
2612
2946
|
const id = chip.dataset.seat;
|
|
2613
|
-
const label =
|
|
2947
|
+
const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
|
|
2614
2948
|
const remove = () => {
|
|
2615
2949
|
this.controller.deselect([id]);
|
|
2616
2950
|
this.toast(`${label} removed.`, "neutral", {
|
|
@@ -2641,6 +2975,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
2641
2975
|
if (seat) this.openSeatView(seat);
|
|
2642
2976
|
});
|
|
2643
2977
|
});
|
|
2978
|
+
this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
|
|
2979
|
+
const locate = () => this.controller.flashSeat(chip.dataset.locate, this.cssVar("--sl-accent") || "#f4b740");
|
|
2980
|
+
chip.addEventListener("mouseenter", locate);
|
|
2981
|
+
chip.addEventListener("focusin", locate);
|
|
2982
|
+
});
|
|
2644
2983
|
this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
|
|
2645
2984
|
btn.addEventListener("click", () => {
|
|
2646
2985
|
const areaEl = btn.closest(".sl-ga");
|
|
@@ -2653,12 +2992,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
2653
2992
|
this.syncTray();
|
|
2654
2993
|
});
|
|
2655
2994
|
});
|
|
2995
|
+
if (this.salesClosed) {
|
|
2996
|
+
this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
|
|
2997
|
+
el.disabled = true;
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
2656
3000
|
const gaTotal = this.pendingGATotal(gaAreas);
|
|
2657
3001
|
const gaCount = this.pendingGACount();
|
|
2658
|
-
const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
|
|
3002
|
+
const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
|
|
2659
3003
|
const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
2660
3004
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
2661
|
-
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
3005
|
+
const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
|
|
2662
3006
|
const count = freshSeats.length + gaCount + heldCount;
|
|
2663
3007
|
const pendingCount = this.pendingSelectionCount();
|
|
2664
3008
|
const previousCount = this.lastTrayCount;
|
|
@@ -2754,6 +3098,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2754
3098
|
}
|
|
2755
3099
|
}
|
|
2756
3100
|
async handleCta() {
|
|
3101
|
+
if (this.salesClosed) return;
|
|
2757
3102
|
if (this.totalTicketCount() > this.maxTickets) {
|
|
2758
3103
|
this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
|
|
2759
3104
|
return;
|
|
@@ -2797,6 +3142,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2797
3142
|
this.opts.onError?.(err);
|
|
2798
3143
|
const problem = err;
|
|
2799
3144
|
const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
|
|
3145
|
+
if (problem.reason === "event_closed") this.setSalesClosed(true);
|
|
2800
3146
|
const message = problem.reason === "event_closed" ? "Seat sales have closed for this event." : labels.length ? `${labels.join(", ")} ${labels.length === 1 ? "is" : "are"} no longer available. Choose another ${labels.length === 1 ? "seat" : "group"}.` : "One or more seats were just taken. Please pick again.";
|
|
2801
3147
|
this.toast(message, "error");
|
|
2802
3148
|
this.setCtaPhase("idle");
|
|
@@ -2905,7 +3251,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2905
3251
|
objectType: it.objectType,
|
|
2906
3252
|
categoryKey: it.categoryKey,
|
|
2907
3253
|
tierId: it.tierId,
|
|
2908
|
-
unitPrice: it.unitPrice,
|
|
3254
|
+
unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
|
|
2909
3255
|
currency: it.currency ?? this.currency,
|
|
2910
3256
|
quantity: it.quantity ?? 1
|
|
2911
3257
|
}));
|
|
@@ -2958,19 +3304,37 @@ var SeatPicker = class _SeatPicker {
|
|
|
2958
3304
|
this.tipEl.style.left = `${Math.max(8, x)}px`;
|
|
2959
3305
|
this.tipEl.style.top = `${Math.max(8, y)}px`;
|
|
2960
3306
|
}
|
|
3307
|
+
/**
|
|
3308
|
+
* Row label without the redundant section prefix. Charts commonly name row
|
|
3309
|
+
* objects "104-A" while the Section column already shows "104" — so the Row
|
|
3310
|
+
* cell repeats the section and, in the compact hover card, truncates to
|
|
3311
|
+
* "10…". Strip a leading "<section><sep>" so Row reads a clean "A". Only when
|
|
3312
|
+
* the prefix is exact (won't touch "1040-A" under section "104"); otherwise
|
|
3313
|
+
* the label is shown verbatim.
|
|
3314
|
+
*/
|
|
3315
|
+
rowShort(details) {
|
|
3316
|
+
const row = details?.rowLabel;
|
|
3317
|
+
const sec = details?.sectionLabel;
|
|
3318
|
+
if (!row || !sec) return row;
|
|
3319
|
+
for (const sep of ["-", " ", "\xB7", "/", "_"]) {
|
|
3320
|
+
const prefix = `${sec}${sep}`;
|
|
3321
|
+
if (row.startsWith(prefix) && row.length > prefix.length) return row.slice(prefix.length);
|
|
3322
|
+
}
|
|
3323
|
+
return row;
|
|
3324
|
+
}
|
|
2961
3325
|
updateTooltip(details) {
|
|
2962
3326
|
if (!this.tipEl) return;
|
|
2963
3327
|
if (!details) {
|
|
2964
3328
|
this.tipEl.style.display = "none";
|
|
2965
3329
|
return;
|
|
2966
3330
|
}
|
|
2967
|
-
const
|
|
2968
|
-
const
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
this.tipEl.innerHTML = `<div
|
|
3331
|
+
const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
3332
|
+
const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
|
|
3333
|
+
const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
|
|
3334
|
+
const grid = hasLoc ? `<div class="sl-tip-grid"><div class="sl-tip-field"><span class="sl-tip-key">Section</span><span class="sl-tip-val">${esc2(details.sectionLabel)}</span></div><div class="sl-tip-field"><span class="sl-tip-key">Row</span><span class="sl-tip-val">${esc2(this.rowShort(details))}</span></div><div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.seatNumber ?? details.label)}</span></div></div>` : `<div class="sl-tip-grid one"><div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.label)}</span></div></div>`;
|
|
3335
|
+
const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
|
|
3336
|
+
this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
|
|
3337
|
+
this.tipEl.innerHTML = grid + `<div class="sl-tip-cat"><span class="sl-tip-dot" style="background:${details.categoryColor}"></span><span class="sl-tip-name">${esc2(details.categoryLabel)}</span><span class="sl-tip-amt">${price}</span></div>` + statusLine;
|
|
2974
3338
|
this.tipEl.style.display = "block";
|
|
2975
3339
|
this.placeTooltip();
|
|
2976
3340
|
}
|
|
@@ -2991,7 +3355,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2991
3355
|
return this.removeHeldLabel(label);
|
|
2992
3356
|
}
|
|
2993
3357
|
async bestAvailable(qty, categoryKey) {
|
|
2994
|
-
if (this.bestAvailableBusy) return null;
|
|
3358
|
+
if (this.salesClosed || this.bestAvailableBusy) return null;
|
|
2995
3359
|
qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
|
|
2996
3360
|
if (this.confirmSeat) this.cancelConfirm();
|
|
2997
3361
|
this.bestAvailableConfirm = false;
|
|
@@ -3072,6 +3436,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
3072
3436
|
this.ro?.disconnect();
|
|
3073
3437
|
this.ro = null;
|
|
3074
3438
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
3439
|
+
if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
|
|
3440
|
+
if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
|
|
3075
3441
|
this.controller.destroy();
|
|
3076
3442
|
this.root?.remove();
|
|
3077
3443
|
this.root = null;
|