@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.cjs
CHANGED
|
@@ -850,26 +850,43 @@ var CSS = `
|
|
|
850
850
|
.sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
|
|
851
851
|
overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
852
852
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
853
|
-
.sl-chip{position:relative;display:grid;grid-template-columns:
|
|
854
|
-
min-height:53px;
|
|
853
|
+
.sl-chip{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 34px;align-items:stretch;
|
|
854
|
+
flex:none;min-height:53px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);overflow:hidden;
|
|
855
855
|
background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
|
|
856
856
|
.sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
|
|
857
857
|
.sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
858
858
|
.sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
|
|
859
859
|
.sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
860
860
|
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
|
|
861
|
-
.sl-ticket-state{width:
|
|
861
|
+
.sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
862
862
|
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
863
863
|
.sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
|
|
864
|
-
.sl-ticket-state svg{width:
|
|
865
|
-
.sl-chip-main{min-width:0}
|
|
866
|
-
.sl-chip
|
|
867
|
-
.sl-chip-
|
|
864
|
+
.sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
865
|
+
.sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
|
|
866
|
+
.sl-chip-id{display:flex;gap:12px;min-width:0}
|
|
867
|
+
.sl-chip-id .fld{min-width:0}
|
|
868
|
+
.sl-chip-id .fld.sec{flex:1}
|
|
869
|
+
.sl-chip-id .fld.mid{flex:none;text-align:center}
|
|
870
|
+
.sl-chip-eb{display:block;font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
|
|
871
|
+
.sl-chip-id .val{display:block;font-weight:600;font-size:13px;line-height:1.25;white-space:nowrap}
|
|
872
|
+
.sl-chip-id .fld.sec .val{overflow:hidden;text-overflow:ellipsis}
|
|
873
|
+
.sl-chip-sub{display:flex;align-items:center;gap:6px;min-width:0}
|
|
868
874
|
.sl-chip .cat{color:var(--sl-muted);font-size:10.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
869
875
|
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
|
|
870
|
-
.sl-chip
|
|
871
|
-
.sl-chip .rm
|
|
876
|
+
.sl-chip-rail{display:flex;flex-direction:column;border-left:1px solid var(--sl-line)}
|
|
877
|
+
.sl-chip .rm,.sl-chip .view{flex:1;min-height:26px;border-radius:0;display:flex;align-items:center;justify-content:center;
|
|
878
|
+
color:var(--sl-muted);transition:color .15s,background .15s}
|
|
879
|
+
.sl-chip .view{border-top:1px solid var(--sl-line)}
|
|
880
|
+
.sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:#e5484d;background:color-mix(in srgb,#e5484d 9%,transparent)}
|
|
881
|
+
.sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
|
|
872
882
|
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
883
|
+
.sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
|
|
884
|
+
/* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
|
|
885
|
+
.sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
|
|
886
|
+
border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
|
|
887
|
+
font-size:11px;color:var(--sl-muted)}
|
|
888
|
+
.sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
|
|
889
|
+
.sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
873
890
|
|
|
874
891
|
/* GA rows */
|
|
875
892
|
.sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
|
|
@@ -945,6 +962,8 @@ var CSS = `
|
|
|
945
962
|
|
|
946
963
|
/* zoom column (flows within the bottom-right region) */
|
|
947
964
|
.sl-zoom{display:flex;flex-direction:column;gap:6px}
|
|
965
|
+
/* CSS-fallback full screen (iOS Safari has no element fullscreen API) */
|
|
966
|
+
.sl-picker.sl-fs{position:fixed;inset:0;z-index:2147483000;width:auto;height:auto;max-height:none;border-radius:0}
|
|
948
967
|
.sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
949
968
|
color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
950
969
|
.sl-zoom button:hover{border-color:var(--sl-muted)}
|
|
@@ -1003,6 +1022,39 @@ var CSS = `
|
|
|
1003
1022
|
.sl-booked.on .sl-booked-title{animation-delay:.22s}
|
|
1004
1023
|
.sl-booked.on .sl-booked-sub{animation-delay:.3s}
|
|
1005
1024
|
|
|
1025
|
+
/* sold-out overlay \u2014 every SEATED category's live availability is 0. Centered
|
|
1026
|
+
over the map; a stub (disabled) "Join waitlist" button, exactly like the page.
|
|
1027
|
+
Suppressed when GA areas exist (GA capacity isn't seat-counted). Clears live
|
|
1028
|
+
the moment WS frees a seat up. */
|
|
1029
|
+
.sl-soldout{position:absolute;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;
|
|
1030
|
+
justify-content:center;text-align:center;gap:8px;padding:24px;
|
|
1031
|
+
background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
|
|
1032
|
+
.sl-soldout.on{display:flex}
|
|
1033
|
+
.sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
|
|
1034
|
+
.sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
|
|
1035
|
+
.sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
|
|
1036
|
+
.sl-picker .sl-soldout-btn{margin-top:10px;min-height:40px;padding:10px 18px;border-radius:var(--sl-r-sm);
|
|
1037
|
+
background:var(--sl-surface);color:var(--sl-muted);border:1px solid var(--sl-line);font-weight:800;font-size:13px;
|
|
1038
|
+
cursor:not-allowed;opacity:.85}
|
|
1039
|
+
|
|
1040
|
+
/* sales-closed pill (header) \u2014 persistent read-only state when the event's sales
|
|
1041
|
+
window is closed at load or closes live mid-session. Neutral (not accent) so it
|
|
1042
|
+
reads as "unavailable", distinct from the accent hold pill next to it. */
|
|
1043
|
+
.sl-closed-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
|
|
1044
|
+
background:color-mix(in srgb,var(--sl-text) 12%,var(--sl-surface));color:var(--sl-text);
|
|
1045
|
+
font-weight:700;font-size:12px;white-space:nowrap}
|
|
1046
|
+
.sl-closed-pill.on{display:inline-flex}
|
|
1047
|
+
.sl-closed-pill svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1048
|
+
|
|
1049
|
+
/* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the small gold
|
|
1050
|
+
rounded logo mark + wordmark. Hidden when the host opts out or the org's paid
|
|
1051
|
+
theme sets hideBadge. */
|
|
1052
|
+
.sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
|
|
1053
|
+
font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
|
|
1054
|
+
.sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
1055
|
+
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
1056
|
+
.sl-powered-mark svg{width:11px;height:11px;fill:currentColor}
|
|
1057
|
+
|
|
1006
1058
|
/* a11y filter chips (flow within the top-left region) */
|
|
1007
1059
|
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
1008
1060
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
@@ -1041,8 +1093,30 @@ var CSS = `
|
|
|
1041
1093
|
.sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
|
|
1042
1094
|
transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
1043
1095
|
|
|
1096
|
+
/* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
|
|
1097
|
+
a passing preview on hover, not the click/select action surface). Reuses the
|
|
1098
|
+
Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
|
|
1099
|
+
one visual language, just at three sizes. */
|
|
1100
|
+
.sl-tip{position:absolute;z-index:7;pointer-events:none;display:none;width:190px;overflow:hidden;
|
|
1101
|
+
background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:11px;
|
|
1102
|
+
box-shadow:0 12px 30px -14px rgba(0,0,0,.6)}
|
|
1103
|
+
.sl-tip-grid{display:grid;grid-template-columns:1.3fr .85fr .85fr;border-bottom:1px solid var(--sl-line)}
|
|
1104
|
+
.sl-tip-grid.one{grid-template-columns:1fr}
|
|
1105
|
+
.sl-tip-field{min-width:0;padding:6px 9px;border-right:1px solid var(--sl-line)}
|
|
1106
|
+
.sl-tip-field:last-child{border-right:0;text-align:center}
|
|
1107
|
+
.sl-tip-grid:not(.one) .sl-tip-field:nth-child(2){text-align:center}
|
|
1108
|
+
.sl-tip-key{display:block;font-size:7.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
|
|
1109
|
+
.sl-tip-val{display:block;margin-top:2px;color:var(--sl-text);font-size:13px;line-height:1.1;font-weight:750;
|
|
1110
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1111
|
+
.sl-tip-cat{display:flex;align-items:center;gap:7px;padding:6px 10px;font-size:11px;
|
|
1112
|
+
background:color-mix(in srgb,var(--sl-cat) 12%,var(--sl-surface))}
|
|
1113
|
+
.sl-tip-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
1114
|
+
.sl-tip-name{color:var(--sl-muted);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1115
|
+
.sl-tip-amt{margin-left:auto;font-weight:800;color:var(--sl-text);font-variant-numeric:tabular-nums;font-size:12px}
|
|
1116
|
+
.sl-tip-status{padding:5px 10px 7px;font-size:8.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
|
|
1117
|
+
|
|
1044
1118
|
/* Best available is a first-class shortcut, not an anonymous utility row. */
|
|
1045
|
-
.sl-ba{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
1119
|
+
.sl-ba{position:relative;flex:none;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
1046
1120
|
padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
|
|
1047
1121
|
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)))}
|
|
1048
1122
|
.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}
|
|
@@ -1078,12 +1152,6 @@ var CSS = `
|
|
|
1078
1152
|
/* per-seat ticket-tier select + view-from-seat button in tray chips */
|
|
1079
1153
|
.sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
|
|
1080
1154
|
font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
|
|
1081
|
-
.sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
1082
|
-
color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
|
|
1083
|
-
.sl-chip .view:hover{color:var(--sl-text)}
|
|
1084
|
-
.sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
|
|
1085
|
-
.sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1086
|
-
@media(pointer:coarse){.sl-chip .view{opacity:.58}}
|
|
1087
1155
|
|
|
1088
1156
|
/* arena: LOD rung pills (flow within the top-center region) */
|
|
1089
1157
|
.sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
@@ -1141,6 +1209,14 @@ var CSS = `
|
|
|
1141
1209
|
.sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
|
|
1142
1210
|
|
|
1143
1211
|
/* view-from-seat button on the confirm popover */
|
|
1212
|
+
/* Eager sightline preview inside the confirm card */
|
|
1213
|
+
.sl-confirm-thumbwrap{position:relative;display:block;width:100%;height:74px;margin:0 0 8px;padding:0!important;
|
|
1214
|
+
border-radius:9px;overflow:hidden;border:1px solid var(--sl-line);cursor:pointer}
|
|
1215
|
+
.sl-confirm-thumb{display:block;width:100%;height:100%;object-fit:cover}
|
|
1216
|
+
.sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
|
|
1217
|
+
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)}
|
|
1218
|
+
.sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
|
|
1219
|
+
.sl-confirm-sight span{color:#22a06b;font-weight:800}
|
|
1144
1220
|
.sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1145
1221
|
color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
1146
1222
|
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
@@ -1224,6 +1300,22 @@ function resolveTokens(chart, host) {
|
|
|
1224
1300
|
"--sl-radius": `${host?.radius ?? 14}px`
|
|
1225
1301
|
};
|
|
1226
1302
|
}
|
|
1303
|
+
var CB_STORAGE_KEY = "seatmap.a11y.cb";
|
|
1304
|
+
function readStoredColorblind() {
|
|
1305
|
+
try {
|
|
1306
|
+
if (typeof window === "undefined") return null;
|
|
1307
|
+
const raw = window.localStorage.getItem(CB_STORAGE_KEY);
|
|
1308
|
+
return raw == null ? null : raw === "1";
|
|
1309
|
+
} catch {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
function writeStoredColorblind(on) {
|
|
1314
|
+
try {
|
|
1315
|
+
window.localStorage.setItem(CB_STORAGE_KEY, on ? "1" : "0");
|
|
1316
|
+
} catch {
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1227
1319
|
var SeatPicker = class _SeatPicker {
|
|
1228
1320
|
constructor(options) {
|
|
1229
1321
|
this.root = null;
|
|
@@ -1256,11 +1348,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
1256
1348
|
this.confirmEl = null;
|
|
1257
1349
|
this.confirmSeat = null;
|
|
1258
1350
|
this.srEl = null;
|
|
1259
|
-
this.a11yFilter = "all";
|
|
1260
1351
|
this.baQty = 2;
|
|
1261
1352
|
this.baCat = "";
|
|
1262
1353
|
this.bestAvailableConfirm = false;
|
|
1263
1354
|
this.releasingHold = false;
|
|
1355
|
+
/** Event sales window is closed (read-only load state / live close). */
|
|
1356
|
+
this.salesClosed = false;
|
|
1357
|
+
/** Every seated category's live availability is 0 (sold-out overlay is up). */
|
|
1358
|
+
this.soldOut = false;
|
|
1359
|
+
this.soldoutEl = null;
|
|
1360
|
+
/** Resolved colorblind-safe state — stored preference wins over the option. */
|
|
1361
|
+
this.cbSafe = false;
|
|
1264
1362
|
// arena / multi-floor / seat-view chrome
|
|
1265
1363
|
this.rungsEl = null;
|
|
1266
1364
|
this.floorsEl = null;
|
|
@@ -1269,13 +1367,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1269
1367
|
this.viewCleanup = null;
|
|
1270
1368
|
this.allSeatsCache = null;
|
|
1271
1369
|
// F3 minimap
|
|
1272
|
-
this.miniEl = null;
|
|
1273
1370
|
this.miniCanvas = null;
|
|
1274
1371
|
this.miniBase = null;
|
|
1275
1372
|
this.miniTf = null;
|
|
1276
1373
|
// F4 price-band filter — active band's category keys (null = all prices)
|
|
1277
1374
|
this.priceBandKeys = null;
|
|
1278
|
-
this.priceFilterEl = null;
|
|
1279
1375
|
/** Last surfaced section summary (re-rendered when the price band changes). */
|
|
1280
1376
|
this.lastSection = null;
|
|
1281
1377
|
/** Section card collapsed to its slim pill (seat-picking has begun). */
|
|
@@ -1295,6 +1391,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1295
1391
|
this.ctaPhase = "idle";
|
|
1296
1392
|
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
1297
1393
|
this.a11yChipsEl = null;
|
|
1394
|
+
this.fsFallback = false;
|
|
1395
|
+
this.fsChangeHandler = null;
|
|
1396
|
+
this.fsEscHandler = null;
|
|
1298
1397
|
this.cbEl = null;
|
|
1299
1398
|
// modal plumbing (set by open())
|
|
1300
1399
|
this.modalScrim = null;
|
|
@@ -1302,20 +1401,22 @@ var SeatPicker = class _SeatPicker {
|
|
|
1302
1401
|
this.escHandler = null;
|
|
1303
1402
|
/** Set by open(): closes the modal (scroll restore + destroy + onClose). */
|
|
1304
1403
|
this.closeModal = null;
|
|
1404
|
+
this.lastCatAvail = null;
|
|
1305
1405
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
1306
1406
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
1307
1407
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
1308
1408
|
this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
|
|
1309
1409
|
this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
|
|
1310
|
-
this.api = new PubApi(this.apiBase);
|
|
1410
|
+
this.api = options.transport ?? new PubApi(this.apiBase);
|
|
1311
1411
|
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
1412
|
+
this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
|
|
1312
1413
|
this.controller = new import_core2.PickerController({
|
|
1313
1414
|
transport: this.api,
|
|
1314
1415
|
eventKey: options.event,
|
|
1315
1416
|
maxSelection: this.maxTickets,
|
|
1316
1417
|
currency: options.currency,
|
|
1317
1418
|
flashOnLiveChange: true,
|
|
1318
|
-
colorblindSafe:
|
|
1419
|
+
colorblindSafe: this.cbSafe,
|
|
1319
1420
|
onSelectionChange: () => {
|
|
1320
1421
|
this.syncTray();
|
|
1321
1422
|
if (this.committedSelection().length) this.collapseSectionCard();
|
|
@@ -1341,6 +1442,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1341
1442
|
},
|
|
1342
1443
|
confirmSelection: this.opts.confirmSelection,
|
|
1343
1444
|
onSelect: (seat) => {
|
|
1445
|
+
if (this.salesClosed) {
|
|
1446
|
+
this.controller.deselect([seat.id]);
|
|
1447
|
+
this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1344
1450
|
this.flashPickedSeat(seat.id);
|
|
1345
1451
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
1346
1452
|
},
|
|
@@ -1363,9 +1469,68 @@ var SeatPicker = class _SeatPicker {
|
|
|
1363
1469
|
onHint: (m) => {
|
|
1364
1470
|
if (m) this.toast(m);
|
|
1365
1471
|
},
|
|
1472
|
+
// Server declared the event closed mid-session (409 event_closed) — keep
|
|
1473
|
+
// the toast (raised by handleCta), and add the persistent read-only state.
|
|
1474
|
+
onSalesClosed: () => this.setSalesClosed(true),
|
|
1366
1475
|
onError: (err) => this.opts.onError?.(err)
|
|
1367
1476
|
});
|
|
1368
1477
|
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Eager sightline preview for the confirm card: a cheap generated forward
|
|
1480
|
+
* view (or the organizer's real photo) plus a "Nm to stage · clear
|
|
1481
|
+
* sightline" line — the premium at-a-glance moment; click opens the 360.
|
|
1482
|
+
*/
|
|
1483
|
+
confirmThumbHtml(seat) {
|
|
1484
|
+
const doc = this.controller.doc;
|
|
1485
|
+
if (!doc) return "";
|
|
1486
|
+
let url = seat.viewUrl ?? "";
|
|
1487
|
+
let distance = null;
|
|
1488
|
+
if (!url) {
|
|
1489
|
+
try {
|
|
1490
|
+
const thumb = (0, import_core2.generateSeatThumb)(seat, doc.focalPoint);
|
|
1491
|
+
url = thumb.url;
|
|
1492
|
+
distance = thumb.distanceM ?? null;
|
|
1493
|
+
} catch {
|
|
1494
|
+
return "";
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
|
|
1498
|
+
return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("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>`;
|
|
1499
|
+
}
|
|
1500
|
+
/** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
|
|
1501
|
+
toggleFullscreen() {
|
|
1502
|
+
const root = this.root;
|
|
1503
|
+
if (!root) return;
|
|
1504
|
+
const active = !!document.fullscreenElement || this.fsFallback;
|
|
1505
|
+
if (!active) {
|
|
1506
|
+
if (root.requestFullscreen) {
|
|
1507
|
+
root.requestFullscreen().catch(() => this.setFsFallback(true));
|
|
1508
|
+
} else {
|
|
1509
|
+
this.setFsFallback(true);
|
|
1510
|
+
}
|
|
1511
|
+
} else if (document.fullscreenElement) {
|
|
1512
|
+
void document.exitFullscreen().catch(() => {
|
|
1513
|
+
});
|
|
1514
|
+
} else {
|
|
1515
|
+
this.setFsFallback(false);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
setFsFallback(on) {
|
|
1519
|
+
if (this.fsFallback === on) return;
|
|
1520
|
+
this.fsFallback = on;
|
|
1521
|
+
this.root?.classList.toggle("sl-fs", on);
|
|
1522
|
+
this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
|
|
1523
|
+
if (on && !this.fsEscHandler) {
|
|
1524
|
+
this.fsEscHandler = (e) => {
|
|
1525
|
+
if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
|
|
1526
|
+
};
|
|
1527
|
+
window.addEventListener("keydown", this.fsEscHandler);
|
|
1528
|
+
} else if (!on && this.fsEscHandler) {
|
|
1529
|
+
window.removeEventListener("keydown", this.fsEscHandler);
|
|
1530
|
+
this.fsEscHandler = null;
|
|
1531
|
+
}
|
|
1532
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1533
|
+
}
|
|
1369
1534
|
/**
|
|
1370
1535
|
* Close the picker. In modal mode (SeatPicker.open()) this dismisses the
|
|
1371
1536
|
* modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
|
|
@@ -1460,6 +1625,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1460
1625
|
<div class="sl-head-meta" data-ref="meta"></div>
|
|
1461
1626
|
</div>
|
|
1462
1627
|
<span class="sl-hold-pill" data-ref="hold"></span>
|
|
1628
|
+
<span class="sl-closed-pill" data-ref="closedPill" role="status">
|
|
1629
|
+
<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>
|
|
1630
|
+
<span data-ref="closedPillText"></span>
|
|
1631
|
+
</span>
|
|
1463
1632
|
<button type="button" class="sl-close" data-ref="close" aria-label="Close">
|
|
1464
1633
|
<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>
|
|
1465
1634
|
</button>
|
|
@@ -1473,6 +1642,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1473
1642
|
<button type="button" aria-label="Fit to screen" data-ref="zfit">
|
|
1474
1643
|
<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>
|
|
1475
1644
|
</button>
|
|
1645
|
+
<button type="button" aria-label="Full screen" aria-pressed="false" data-ref="zfs">
|
|
1646
|
+
<svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
|
1647
|
+
</button>
|
|
1476
1648
|
</div>
|
|
1477
1649
|
<div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
|
|
1478
1650
|
<div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
|
|
@@ -1491,6 +1663,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1491
1663
|
<div class="sl-filters" data-ref="filters"></div>
|
|
1492
1664
|
<div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
|
|
1493
1665
|
<div class="sl-prices" data-ref="prices"></div>
|
|
1666
|
+
<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>
|
|
1494
1667
|
<div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
|
|
1495
1668
|
<div class="sl-tray" data-ref="tray"></div>
|
|
1496
1669
|
<div class="sl-foot" data-ref="foot">
|
|
@@ -1524,6 +1697,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
1524
1697
|
this.els.zin.addEventListener("click", () => this.controller.zoomIn());
|
|
1525
1698
|
this.els.zout.addEventListener("click", () => this.controller.zoomOut());
|
|
1526
1699
|
this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
|
|
1700
|
+
this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
|
|
1701
|
+
this.fsChangeHandler = () => {
|
|
1702
|
+
if (!document.fullscreenElement) this.setFsFallback(false);
|
|
1703
|
+
this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback));
|
|
1704
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1705
|
+
};
|
|
1706
|
+
document.addEventListener("fullscreenchange", this.fsChangeHandler);
|
|
1527
1707
|
const head = this.els.sheetHead;
|
|
1528
1708
|
if (head) {
|
|
1529
1709
|
const toggle = this.els.sheetToggle;
|
|
@@ -1567,7 +1747,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1567
1747
|
}
|
|
1568
1748
|
this.tipEl = document.createElement("div");
|
|
1569
1749
|
this.tipEl.setAttribute("role", "tooltip");
|
|
1570
|
-
this.tipEl.
|
|
1750
|
+
this.tipEl.className = "sl-tip";
|
|
1571
1751
|
this.els.map.appendChild(this.tipEl);
|
|
1572
1752
|
this.els.map.addEventListener("mousemove", (e) => {
|
|
1573
1753
|
const r = this.els.map.getBoundingClientRect();
|
|
@@ -1592,6 +1772,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1592
1772
|
return this;
|
|
1593
1773
|
}
|
|
1594
1774
|
this.els.boot.remove();
|
|
1775
|
+
this.salesClosed = !!info.salesClosed;
|
|
1595
1776
|
this.buildRegions();
|
|
1596
1777
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
1597
1778
|
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
@@ -1611,6 +1792,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1611
1792
|
this.els.name.textContent = info.eventName ?? "";
|
|
1612
1793
|
const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
1613
1794
|
this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
|
|
1795
|
+
this.buildBadge(chartTheme);
|
|
1614
1796
|
const present = /* @__PURE__ */ new Set();
|
|
1615
1797
|
if (this.controller.doc) {
|
|
1616
1798
|
for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
|
|
@@ -1626,12 +1808,29 @@ var SeatPicker = class _SeatPicker {
|
|
|
1626
1808
|
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
1627
1809
|
this.regions["top-left"].appendChild(chips);
|
|
1628
1810
|
this.a11yChipsEl = chips;
|
|
1811
|
+
const active = /* @__PURE__ */ new Set();
|
|
1812
|
+
const syncChips = () => {
|
|
1813
|
+
chips.querySelectorAll("button").forEach((b) => {
|
|
1814
|
+
const f = b.dataset.f;
|
|
1815
|
+
const on = f === "all" ? active.size === 0 : active.has(f);
|
|
1816
|
+
b.classList.toggle("on", on);
|
|
1817
|
+
b.setAttribute("aria-pressed", String(on));
|
|
1818
|
+
});
|
|
1819
|
+
const filter = active.size ? [...active] : null;
|
|
1820
|
+
this.controller.setAccessibilityFilter(filter);
|
|
1821
|
+
if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
|
|
1822
|
+
this.controller.setRung("seats");
|
|
1823
|
+
this.collapseSectionCard();
|
|
1824
|
+
this.syncRung();
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1629
1827
|
chips.querySelectorAll("button").forEach((btn) => {
|
|
1630
1828
|
btn.addEventListener("click", () => {
|
|
1631
1829
|
const f = btn.dataset.f;
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1830
|
+
if (f === "all") active.clear();
|
|
1831
|
+
else if (active.has(f)) active.delete(f);
|
|
1832
|
+
else active.add(f);
|
|
1833
|
+
syncChips();
|
|
1635
1834
|
});
|
|
1636
1835
|
});
|
|
1637
1836
|
}
|
|
@@ -1640,14 +1839,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1640
1839
|
cb.className = "sl-cbbtn";
|
|
1641
1840
|
this.cbEl = cb;
|
|
1642
1841
|
cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
|
|
1643
|
-
cb.setAttribute("aria-pressed", String(
|
|
1842
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1644
1843
|
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>';
|
|
1645
1844
|
this.els.zfit.parentElement.appendChild(cb);
|
|
1646
|
-
let cbOn = !!this.opts.colorblindSafe;
|
|
1647
1845
|
cb.addEventListener("click", () => {
|
|
1648
|
-
|
|
1649
|
-
cb.setAttribute("aria-pressed", String(
|
|
1650
|
-
this.controller.setColorblindSafe(
|
|
1846
|
+
this.cbSafe = !this.cbSafe;
|
|
1847
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1848
|
+
this.controller.setColorblindSafe(this.cbSafe);
|
|
1849
|
+
writeStoredColorblind(this.cbSafe);
|
|
1651
1850
|
});
|
|
1652
1851
|
this.srEl = document.createElement("div");
|
|
1653
1852
|
this.srEl.className = "sl-sr";
|
|
@@ -1658,9 +1857,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1658
1857
|
this.buildPriceFilter();
|
|
1659
1858
|
this.buildExtendPrompt();
|
|
1660
1859
|
this.buildBookedOverlay();
|
|
1860
|
+
this.buildSoldoutOverlay();
|
|
1661
1861
|
this.dockLayoutChrome();
|
|
1662
1862
|
await this.restoreRememberedHold();
|
|
1663
1863
|
if (this.destroyed) return this;
|
|
1864
|
+
if (this.salesClosed) this.applySalesClosed();
|
|
1664
1865
|
this.syncPrices();
|
|
1665
1866
|
this.syncTray();
|
|
1666
1867
|
return this;
|
|
@@ -1712,6 +1913,85 @@ var SeatPicker = class _SeatPicker {
|
|
|
1712
1913
|
this.bookedEl = el;
|
|
1713
1914
|
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
1714
1915
|
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Localized string with a literal fallback. `t()` returns the key itself for
|
|
1918
|
+
* unknown keys, so this collapses that to `fallback` — while still honoring a
|
|
1919
|
+
* host `messages` override (which makes `t()` return the override, not the key).
|
|
1920
|
+
*/
|
|
1921
|
+
tf(key, fallback) {
|
|
1922
|
+
const v = (0, import_core2.t)(key);
|
|
1923
|
+
return v === key ? fallback : v;
|
|
1924
|
+
}
|
|
1925
|
+
/** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
|
|
1926
|
+
buildSoldoutOverlay() {
|
|
1927
|
+
if (!this.els.map) return;
|
|
1928
|
+
const el = document.createElement("div");
|
|
1929
|
+
el.className = "sl-soldout";
|
|
1930
|
+
el.setAttribute("role", "status");
|
|
1931
|
+
const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
|
|
1932
|
+
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>`;
|
|
1933
|
+
this.els.map.appendChild(el);
|
|
1934
|
+
this.soldoutEl = el;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Recompute the sold-out state on every price/availability sync. Sold-out ⇔
|
|
1938
|
+
* every SEATED category's live free count is 0. Suppressed when the chart has
|
|
1939
|
+
* GA areas (GA capacity isn't per-seat, so seated counts would read 0 and
|
|
1940
|
+
* falsely block standing room) — mirrors the public page. Clears live when WS
|
|
1941
|
+
* frees a seat up.
|
|
1942
|
+
*/
|
|
1943
|
+
syncSoldout(categories, left) {
|
|
1944
|
+
const hasGA = this.controller.getGAAreas().length > 0;
|
|
1945
|
+
const soldOut = this.isSoldOut(categories, left, hasGA);
|
|
1946
|
+
if (soldOut === this.soldOut) return;
|
|
1947
|
+
this.soldOut = soldOut;
|
|
1948
|
+
this.soldoutEl?.classList.toggle("on", soldOut);
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Pure sold-out predicate: every SEATED category's free count is 0, there is at
|
|
1952
|
+
* least one seated category, and there are no GA areas (GA capacity isn't
|
|
1953
|
+
* per-seat, so seated counts read 0 and would falsely block standing room).
|
|
1954
|
+
* `left` is seeded implicitly — a missing key means a fully-booked tier (0 free).
|
|
1955
|
+
*/
|
|
1956
|
+
isSoldOut(categories, left, hasGA) {
|
|
1957
|
+
return !hasGA && categories.length > 0 && categories.every((c) => (left[c.key] ?? 0) === 0);
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Sales-closed read-only state (Gap 3): persistent header pill, disabled CTA
|
|
1961
|
+
* with a closed label, and frozen best-available / GA controls. `setSalesClosed`
|
|
1962
|
+
* is the reactive entry (live 409 event_closed); `applySalesClosed` is the
|
|
1963
|
+
* idempotent DOM apply used at load and on transition.
|
|
1964
|
+
*/
|
|
1965
|
+
setSalesClosed(closed) {
|
|
1966
|
+
if (this.salesClosed === closed) return;
|
|
1967
|
+
this.salesClosed = closed;
|
|
1968
|
+
this.applySalesClosed();
|
|
1969
|
+
}
|
|
1970
|
+
applySalesClosed() {
|
|
1971
|
+
const pill = this.els.closedPill;
|
|
1972
|
+
if (pill) {
|
|
1973
|
+
pill.classList.toggle("on", this.salesClosed);
|
|
1974
|
+
const text = this.els.closedPillText ?? pill;
|
|
1975
|
+
text.textContent = this.tf("picker.salesClosedPill", "Sales are closed");
|
|
1976
|
+
}
|
|
1977
|
+
this.root?.setAttribute("data-sales-closed", String(this.salesClosed));
|
|
1978
|
+
this.syncCta();
|
|
1979
|
+
this.syncTray();
|
|
1980
|
+
}
|
|
1981
|
+
/** The badge is hidden when the host opts out OR the org's theme sets hideBadge. */
|
|
1982
|
+
badgeHidden(chartTheme) {
|
|
1983
|
+
return !!(this.opts.hideBadge || chartTheme?.hideBadge);
|
|
1984
|
+
}
|
|
1985
|
+
/** Attribution badge in the side-panel foot (Gap 7). Hidden per host/theme. */
|
|
1986
|
+
buildBadge(chartTheme) {
|
|
1987
|
+
if (this.badgeHidden(chartTheme)) return;
|
|
1988
|
+
const foot = this.els.foot;
|
|
1989
|
+
if (!foot) return;
|
|
1990
|
+
const el = document.createElement("div");
|
|
1991
|
+
el.className = "sl-powered";
|
|
1992
|
+
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>`;
|
|
1993
|
+
foot.appendChild(el);
|
|
1994
|
+
}
|
|
1715
1995
|
// ---- Feature 6: chrome anchor regions -------------------------------------
|
|
1716
1996
|
/**
|
|
1717
1997
|
* Create the positioned flex containers that own every persistent map overlay.
|
|
@@ -1824,13 +2104,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1824
2104
|
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1825
2105
|
}
|
|
1826
2106
|
return gaAreas.reduce(
|
|
1827
|
-
(sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
2107
|
+
(sum, area) => sum + this.paidPrice(area.categoryKey, null, area.price) * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
1828
2108
|
0
|
|
1829
2109
|
);
|
|
1830
2110
|
}
|
|
1831
2111
|
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1832
2112
|
const cta = this.els.cta;
|
|
1833
2113
|
if (!cta) return;
|
|
2114
|
+
if (this.salesClosed) {
|
|
2115
|
+
cta.disabled = true;
|
|
2116
|
+
cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
1834
2119
|
if (this.confirmSeat) {
|
|
1835
2120
|
cta.disabled = true;
|
|
1836
2121
|
cta.textContent = "Confirm or cancel this seat";
|
|
@@ -1966,7 +2251,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
1966
2251
|
canvas.style.height = `${h}px`;
|
|
1967
2252
|
wrap.appendChild(canvas);
|
|
1968
2253
|
(this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
|
|
1969
|
-
this.miniEl = wrap;
|
|
1970
2254
|
this.miniCanvas = canvas;
|
|
1971
2255
|
const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
|
|
1972
2256
|
const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
|
|
@@ -2079,9 +2363,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
2079
2363
|
this.controller.overview();
|
|
2080
2364
|
}
|
|
2081
2365
|
// ---- F4 price-band filter -------------------------------------------------
|
|
2082
|
-
/** Effective price of a category
|
|
2366
|
+
/** Effective display price of a category: host pricing override → first tier → base. */
|
|
2083
2367
|
catPrice(c) {
|
|
2084
|
-
|
|
2368
|
+
const chart = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
2369
|
+
if (chart === void 0 || !c.key) return chart;
|
|
2370
|
+
return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
|
|
2085
2371
|
}
|
|
2086
2372
|
/** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
|
|
2087
2373
|
priceBands() {
|
|
@@ -2126,7 +2412,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
2126
2412
|
select.setAttribute("aria-label", "Filter and focus seats by price");
|
|
2127
2413
|
select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
|
|
2128
2414
|
this.els.pricesSec.appendChild(select);
|
|
2129
|
-
this.priceFilterEl = select;
|
|
2130
2415
|
select.addEventListener("change", () => {
|
|
2131
2416
|
const band = bands.find((candidate) => candidate.id === select.value);
|
|
2132
2417
|
const keys = band?.keys ?? null;
|
|
@@ -2230,7 +2515,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
2230
2515
|
renderSectionCard(summary) {
|
|
2231
2516
|
if (!this.els.map) return;
|
|
2232
2517
|
this.secCardEl?.remove();
|
|
2233
|
-
const
|
|
2518
|
+
const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
|
|
2519
|
+
const paidMin = Math.min(...paid);
|
|
2520
|
+
const paidMax = Math.max(...paid);
|
|
2521
|
+
const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
|
|
2234
2522
|
const leftLabel = (0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft);
|
|
2235
2523
|
const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button>`;
|
|
2236
2524
|
const card = document.createElement("div");
|
|
@@ -2260,7 +2548,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2260
2548
|
card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
|
|
2261
2549
|
const mix = summary.categories.map((c) => {
|
|
2262
2550
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2263
|
-
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>`;
|
|
2551
|
+
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>`;
|
|
2264
2552
|
}).join("");
|
|
2265
2553
|
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 ${(0, import_core2.t)("picker.overview")}</button><span class="sl-seccard-hint">${(0, import_core2.t)("picker.tapSeatHint")}</span></div>`;
|
|
2266
2554
|
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
@@ -2327,7 +2615,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2327
2615
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2328
2616
|
const status = this.controller.getStatus(seat.id) ?? "free";
|
|
2329
2617
|
const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
|
|
2330
|
-
const price = cat
|
|
2618
|
+
const price = cat ? this.catPrice(cat) : void 0;
|
|
2331
2619
|
this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
|
|
2332
2620
|
}
|
|
2333
2621
|
// ---- seat candidate confirmation ------------------------------------------
|
|
@@ -2342,7 +2630,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
2342
2630
|
if (this.tipEl) this.tipEl.style.display = "none";
|
|
2343
2631
|
const details = this.controller.seatDetails(seat.id);
|
|
2344
2632
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2345
|
-
const
|
|
2633
|
+
const chartPrice = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
|
|
2634
|
+
const price = chartPrice != null ? this.paidPrice(seat.categoryKey, details?.tierId ?? cat?.tiers?.[0]?.id ?? null, chartPrice) : void 0;
|
|
2346
2635
|
const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
|
|
2347
2636
|
"&": "&",
|
|
2348
2637
|
"<": "<",
|
|
@@ -2355,7 +2644,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2355
2644
|
el.setAttribute("aria-modal", "true");
|
|
2356
2645
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
2357
2646
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
2358
|
-
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
|
|
2647
|
+
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>`;
|
|
2359
2648
|
this.els.map.appendChild(el);
|
|
2360
2649
|
this.confirmEl = el;
|
|
2361
2650
|
this.reanchorConfirm();
|
|
@@ -2520,18 +2809,35 @@ var SeatPicker = class _SeatPicker {
|
|
|
2520
2809
|
}
|
|
2521
2810
|
// ---- chrome sync ----------------------------------------------------------
|
|
2522
2811
|
money(n) {
|
|
2812
|
+
const formatter = this.opts.pricing?.formatter;
|
|
2813
|
+
if (formatter) return formatter(n, this.currency);
|
|
2523
2814
|
try {
|
|
2524
2815
|
return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
|
|
2525
2816
|
} catch {
|
|
2526
2817
|
return `${n} ${this.currency}`;
|
|
2527
2818
|
}
|
|
2528
2819
|
}
|
|
2820
|
+
/**
|
|
2821
|
+
* The price the buyer will actually pay for a category (+tier): the host's
|
|
2822
|
+
* `pricing` override when present, else the chart's stored price. Every
|
|
2823
|
+
* price the widget DISPLAYS or hands off must flow through here — a map
|
|
2824
|
+
* that shows one price while checkout charges another destroys trust.
|
|
2825
|
+
*/
|
|
2826
|
+
paidPrice(categoryKey, tierId, fallback) {
|
|
2827
|
+
const entry = categoryKey ? this.opts.pricing?.prices?.[categoryKey] : void 0;
|
|
2828
|
+
if (entry === void 0) return fallback;
|
|
2829
|
+
if (typeof entry === "number") return entry;
|
|
2830
|
+
if (tierId && entry.tiers?.[tierId] !== void 0) return entry.tiers[tierId];
|
|
2831
|
+
return entry.base ?? fallback;
|
|
2832
|
+
}
|
|
2529
2833
|
syncPrices() {
|
|
2530
2834
|
const doc = this.controller.doc;
|
|
2531
2835
|
if (!doc || !this.els.prices) return;
|
|
2532
2836
|
const left = this.controller.categoryAvailability();
|
|
2837
|
+
this.narrateAvailability(doc.categories, left);
|
|
2838
|
+
this.syncSoldout(doc.categories, left);
|
|
2533
2839
|
this.els.prices.innerHTML = doc.categories.map((c) => {
|
|
2534
|
-
const price =
|
|
2840
|
+
const price = this.catPrice(c);
|
|
2535
2841
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2536
2842
|
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>`;
|
|
2537
2843
|
}).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>`;
|
|
@@ -2540,6 +2846,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2540
2846
|
row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
|
|
2541
2847
|
});
|
|
2542
2848
|
}
|
|
2849
|
+
/**
|
|
2850
|
+
* Live-activity strip: turn WS availability deltas into one quiet line of
|
|
2851
|
+
* social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
|
|
2852
|
+
* counts on every status change — no per-seat payload needed. Skips the very
|
|
2853
|
+
* first computation (initial load is not "activity").
|
|
2854
|
+
*/
|
|
2855
|
+
narrateAvailability(categories, left) {
|
|
2856
|
+
const textEl = this.els.liveText;
|
|
2857
|
+
const prev = this.lastCatAvail;
|
|
2858
|
+
this.lastCatAvail = { ...left };
|
|
2859
|
+
if (!textEl || !prev) return;
|
|
2860
|
+
for (const cat of categories) {
|
|
2861
|
+
const before = prev[cat.key];
|
|
2862
|
+
const now = left[cat.key] ?? 0;
|
|
2863
|
+
if (before === void 0 || now >= before) continue;
|
|
2864
|
+
const taken = before - now;
|
|
2865
|
+
textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2543
2869
|
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
|
2544
2870
|
evictTakenSelections() {
|
|
2545
2871
|
const ownLabels = /* @__PURE__ */ new Set([
|
|
@@ -2568,15 +2894,23 @@ var SeatPicker = class _SeatPicker {
|
|
|
2568
2894
|
const cats = this.controller.doc?.categories ?? [];
|
|
2569
2895
|
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>`);
|
|
2570
2896
|
}
|
|
2897
|
+
const idGrid = (seatId, label) => {
|
|
2898
|
+
const d = seatId ? this.controller.seatDetails(seatId) : null;
|
|
2899
|
+
if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
|
|
2900
|
+
return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
|
|
2901
|
+
}
|
|
2902
|
+
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>`;
|
|
2903
|
+
};
|
|
2904
|
+
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="${(0, import_core2.t)("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>`;
|
|
2571
2905
|
for (const item of heldItems) {
|
|
2572
2906
|
const itemKey = `held:${item.label}`;
|
|
2573
2907
|
nextTrayKeys.add(itemKey);
|
|
2574
2908
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
2575
2909
|
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
2576
|
-
const
|
|
2577
|
-
const
|
|
2910
|
+
const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
|
|
2911
|
+
const canView2 = this.seatViewEnabled() && !!heldSeat;
|
|
2578
2912
|
parts.push(
|
|
2579
|
-
`<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><
|
|
2913
|
+
`<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>`
|
|
2580
2914
|
);
|
|
2581
2915
|
}
|
|
2582
2916
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
@@ -2585,16 +2919,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2585
2919
|
const itemKey = `seat:${s.id}`;
|
|
2586
2920
|
nextTrayKeys.add(itemKey);
|
|
2587
2921
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
2588
|
-
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
|
|
2589
|
-
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
|
|
2922
|
+
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(this.paidPrice(s.categoryKey, ti.id, ti.price))}</option>`).join("") + `</select>` : "";
|
|
2590
2923
|
parts.push(
|
|
2591
|
-
`<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><
|
|
2924
|
+
`<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>`
|
|
2592
2925
|
);
|
|
2593
2926
|
}
|
|
2594
2927
|
for (const area of gaAreas) {
|
|
2595
2928
|
const qty = this.gaQty.get(area.id) ?? 0;
|
|
2596
2929
|
parts.push(
|
|
2597
|
-
`<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>`
|
|
2930
|
+
`<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>`
|
|
2598
2931
|
);
|
|
2599
2932
|
}
|
|
2600
2933
|
this.els.tray.innerHTML = parts.join("");
|
|
@@ -2634,7 +2967,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2634
2967
|
return;
|
|
2635
2968
|
}
|
|
2636
2969
|
const id = chip.dataset.seat;
|
|
2637
|
-
const label =
|
|
2970
|
+
const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
|
|
2638
2971
|
const remove = () => {
|
|
2639
2972
|
this.controller.deselect([id]);
|
|
2640
2973
|
this.toast(`${label} removed.`, "neutral", {
|
|
@@ -2665,6 +2998,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
2665
2998
|
if (seat) this.openSeatView(seat);
|
|
2666
2999
|
});
|
|
2667
3000
|
});
|
|
3001
|
+
this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
|
|
3002
|
+
const locate = () => this.controller.flashSeat(chip.dataset.locate, this.cssVar("--sl-accent") || "#f4b740");
|
|
3003
|
+
chip.addEventListener("mouseenter", locate);
|
|
3004
|
+
chip.addEventListener("focusin", locate);
|
|
3005
|
+
});
|
|
2668
3006
|
this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
|
|
2669
3007
|
btn.addEventListener("click", () => {
|
|
2670
3008
|
const areaEl = btn.closest(".sl-ga");
|
|
@@ -2677,12 +3015,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
2677
3015
|
this.syncTray();
|
|
2678
3016
|
});
|
|
2679
3017
|
});
|
|
3018
|
+
if (this.salesClosed) {
|
|
3019
|
+
this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
|
|
3020
|
+
el.disabled = true;
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
2680
3023
|
const gaTotal = this.pendingGATotal(gaAreas);
|
|
2681
3024
|
const gaCount = this.pendingGACount();
|
|
2682
|
-
const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
|
|
3025
|
+
const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
|
|
2683
3026
|
const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
2684
3027
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
2685
|
-
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
3028
|
+
const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
|
|
2686
3029
|
const count = freshSeats.length + gaCount + heldCount;
|
|
2687
3030
|
const pendingCount = this.pendingSelectionCount();
|
|
2688
3031
|
const previousCount = this.lastTrayCount;
|
|
@@ -2778,6 +3121,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2778
3121
|
}
|
|
2779
3122
|
}
|
|
2780
3123
|
async handleCta() {
|
|
3124
|
+
if (this.salesClosed) return;
|
|
2781
3125
|
if (this.totalTicketCount() > this.maxTickets) {
|
|
2782
3126
|
this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
|
|
2783
3127
|
return;
|
|
@@ -2821,6 +3165,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2821
3165
|
this.opts.onError?.(err);
|
|
2822
3166
|
const problem = err;
|
|
2823
3167
|
const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
|
|
3168
|
+
if (problem.reason === "event_closed") this.setSalesClosed(true);
|
|
2824
3169
|
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.";
|
|
2825
3170
|
this.toast(message, "error");
|
|
2826
3171
|
this.setCtaPhase("idle");
|
|
@@ -2929,7 +3274,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2929
3274
|
objectType: it.objectType,
|
|
2930
3275
|
categoryKey: it.categoryKey,
|
|
2931
3276
|
tierId: it.tierId,
|
|
2932
|
-
unitPrice: it.unitPrice,
|
|
3277
|
+
unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
|
|
2933
3278
|
currency: it.currency ?? this.currency,
|
|
2934
3279
|
quantity: it.quantity ?? 1
|
|
2935
3280
|
}));
|
|
@@ -2982,19 +3327,37 @@ var SeatPicker = class _SeatPicker {
|
|
|
2982
3327
|
this.tipEl.style.left = `${Math.max(8, x)}px`;
|
|
2983
3328
|
this.tipEl.style.top = `${Math.max(8, y)}px`;
|
|
2984
3329
|
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Row label without the redundant section prefix. Charts commonly name row
|
|
3332
|
+
* objects "104-A" while the Section column already shows "104" — so the Row
|
|
3333
|
+
* cell repeats the section and, in the compact hover card, truncates to
|
|
3334
|
+
* "10…". Strip a leading "<section><sep>" so Row reads a clean "A". Only when
|
|
3335
|
+
* the prefix is exact (won't touch "1040-A" under section "104"); otherwise
|
|
3336
|
+
* the label is shown verbatim.
|
|
3337
|
+
*/
|
|
3338
|
+
rowShort(details) {
|
|
3339
|
+
const row = details?.rowLabel;
|
|
3340
|
+
const sec = details?.sectionLabel;
|
|
3341
|
+
if (!row || !sec) return row;
|
|
3342
|
+
for (const sep of ["-", " ", "\xB7", "/", "_"]) {
|
|
3343
|
+
const prefix = `${sec}${sep}`;
|
|
3344
|
+
if (row.startsWith(prefix) && row.length > prefix.length) return row.slice(prefix.length);
|
|
3345
|
+
}
|
|
3346
|
+
return row;
|
|
3347
|
+
}
|
|
2985
3348
|
updateTooltip(details) {
|
|
2986
3349
|
if (!this.tipEl) return;
|
|
2987
3350
|
if (!details) {
|
|
2988
3351
|
this.tipEl.style.display = "none";
|
|
2989
3352
|
return;
|
|
2990
3353
|
}
|
|
2991
|
-
const
|
|
2992
|
-
const
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
this.tipEl.innerHTML = `<div
|
|
3354
|
+
const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
3355
|
+
const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
|
|
3356
|
+
const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
|
|
3357
|
+
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>`;
|
|
3358
|
+
const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
|
|
3359
|
+
this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
|
|
3360
|
+
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;
|
|
2998
3361
|
this.tipEl.style.display = "block";
|
|
2999
3362
|
this.placeTooltip();
|
|
3000
3363
|
}
|
|
@@ -3015,7 +3378,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3015
3378
|
return this.removeHeldLabel(label);
|
|
3016
3379
|
}
|
|
3017
3380
|
async bestAvailable(qty, categoryKey) {
|
|
3018
|
-
if (this.bestAvailableBusy) return null;
|
|
3381
|
+
if (this.salesClosed || this.bestAvailableBusy) return null;
|
|
3019
3382
|
qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
|
|
3020
3383
|
if (this.confirmSeat) this.cancelConfirm();
|
|
3021
3384
|
this.bestAvailableConfirm = false;
|
|
@@ -3096,6 +3459,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
3096
3459
|
this.ro?.disconnect();
|
|
3097
3460
|
this.ro = null;
|
|
3098
3461
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
3462
|
+
if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
|
|
3463
|
+
if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
|
|
3099
3464
|
this.controller.destroy();
|
|
3100
3465
|
this.root?.remove();
|
|
3101
3466
|
this.root = null;
|