@seatlayer/js 0.36.2 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +928 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +326 -9
- package/dist/index.d.ts +326 -9
- package/dist/index.js +923 -46
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6160,6 +6160,78 @@ function accessLine(access) {
|
|
|
6160
6160
|
function accessIntentLabel(intent) {
|
|
6161
6161
|
return intent === "internal" ? "Internal selling \u2014 our own staff sell these" : intent === "server" ? "Server integration \u2014 our backend lets buyers in" : intent === "hosted_link" ? "Hosted access link \u2014 SeatLayer issues the link" : "No buyer access yet \u2014 the allocation is just protected";
|
|
6162
6162
|
}
|
|
6163
|
+
var ACCESS_LINK_DEFAULTS = {
|
|
6164
|
+
maxRedemptions: 100,
|
|
6165
|
+
maxQuantity: 4
|
|
6166
|
+
};
|
|
6167
|
+
function accessLinkBadge(link) {
|
|
6168
|
+
switch (link.status ?? link.state) {
|
|
6169
|
+
case "active":
|
|
6170
|
+
return { text: "Active", kind: "active" };
|
|
6171
|
+
case "expired":
|
|
6172
|
+
return { text: "Expired", kind: "archived" };
|
|
6173
|
+
case "exhausted":
|
|
6174
|
+
return { text: "All used", kind: "paused" };
|
|
6175
|
+
case "rotated":
|
|
6176
|
+
return { text: "Replaced", kind: "archived" };
|
|
6177
|
+
default:
|
|
6178
|
+
return { text: "Revoked", kind: "archived" };
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
function accessLinkIsLive(link) {
|
|
6182
|
+
return link.state === "active" && link.status === "active";
|
|
6183
|
+
}
|
|
6184
|
+
function formatMoment(ms) {
|
|
6185
|
+
if (!Number.isFinite(ms)) return "\u2014";
|
|
6186
|
+
return new Date(ms).toLocaleString(void 0, {
|
|
6187
|
+
day: "numeric",
|
|
6188
|
+
month: "short",
|
|
6189
|
+
year: "numeric",
|
|
6190
|
+
hour: "numeric",
|
|
6191
|
+
minute: "2-digit"
|
|
6192
|
+
});
|
|
6193
|
+
}
|
|
6194
|
+
function accessLinkPolicyLines(link) {
|
|
6195
|
+
return [
|
|
6196
|
+
{ k: "Expires", v: formatMoment(link.expiresAt) },
|
|
6197
|
+
{
|
|
6198
|
+
k: "Redemptions",
|
|
6199
|
+
v: `${link.redemptions.toLocaleString()} of ${link.maxRedemptions.toLocaleString()} used`
|
|
6200
|
+
},
|
|
6201
|
+
{
|
|
6202
|
+
k: "Seats per buyer",
|
|
6203
|
+
v: `${link.maxQuantity.toLocaleString()} seat${link.maxQuantity === 1 ? "" : "s"} maximum`
|
|
6204
|
+
},
|
|
6205
|
+
{
|
|
6206
|
+
k: "Covers",
|
|
6207
|
+
v: link.includePublic ? "This channel's allocation and Public sale seats" : "This channel's allocation only"
|
|
6208
|
+
}
|
|
6209
|
+
];
|
|
6210
|
+
}
|
|
6211
|
+
function accessLinkErrorCopy(err) {
|
|
6212
|
+
const fromServer = err?.serverMessage?.trim();
|
|
6213
|
+
switch (err?.code) {
|
|
6214
|
+
case "invalid_expiry":
|
|
6215
|
+
case "invalid_max_redemptions":
|
|
6216
|
+
case "invalid_max_quantity":
|
|
6217
|
+
case "invalid_session_ttl":
|
|
6218
|
+
case "invalid_label":
|
|
6219
|
+
return fromServer || "That setting is outside what a hosted link allows. Adjust it and try again.";
|
|
6220
|
+
case "too_many_access_links":
|
|
6221
|
+
return fromServer || "This channel already has as many live links as it can hold. Revoke one before creating another.";
|
|
6222
|
+
case "access_link_not_active":
|
|
6223
|
+
return "That link is no longer active, so it cannot be rotated or revoked.";
|
|
6224
|
+
case "channel_unavailable":
|
|
6225
|
+
return "This channel is paused or archived, so it cannot let new buyers in. Resume it first.";
|
|
6226
|
+
case "end_active_sessions_required":
|
|
6227
|
+
return "Choose what happens to the buyers who already came in through this link.";
|
|
6228
|
+
case "not_found":
|
|
6229
|
+
return "That link is no longer here. Refresh and try again.";
|
|
6230
|
+
default:
|
|
6231
|
+
if (err?.status === 403) return "Hosted access links need channel-management permission.";
|
|
6232
|
+
return fromServer || "That did not go through. Try again.";
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6163
6235
|
function dropReviewRows(details) {
|
|
6164
6236
|
return (details?.channels ?? []).map((channel) => ({
|
|
6165
6237
|
kind: "skip",
|
|
@@ -6176,13 +6248,14 @@ function stateBadge(state) {
|
|
|
6176
6248
|
|
|
6177
6249
|
// src/manageApi.ts
|
|
6178
6250
|
var ManageApiError = class extends Error {
|
|
6179
|
-
constructor(status, message, code, conflicts, details) {
|
|
6251
|
+
constructor(status, message, code, conflicts, details, serverMessage) {
|
|
6180
6252
|
super(message);
|
|
6181
6253
|
this.name = "ManageApiError";
|
|
6182
6254
|
this.status = status;
|
|
6183
6255
|
this.code = code;
|
|
6184
6256
|
this.conflicts = conflicts;
|
|
6185
6257
|
this.details = details;
|
|
6258
|
+
this.serverMessage = serverMessage;
|
|
6186
6259
|
}
|
|
6187
6260
|
};
|
|
6188
6261
|
async function parse(res) {
|
|
@@ -6195,7 +6268,8 @@ async function parse(res) {
|
|
|
6195
6268
|
err?.error ?? `request_failed_${res.status}`,
|
|
6196
6269
|
err?.code,
|
|
6197
6270
|
err?.conflicts,
|
|
6198
|
-
err?.details
|
|
6271
|
+
err?.details,
|
|
6272
|
+
typeof err?.message === "string" ? err.message : void 0
|
|
6199
6273
|
);
|
|
6200
6274
|
}
|
|
6201
6275
|
return data;
|
|
@@ -6391,6 +6465,58 @@ var ManageApi = class {
|
|
|
6391
6465
|
body: { accessIntent }
|
|
6392
6466
|
});
|
|
6393
6467
|
}
|
|
6468
|
+
// ---- hosted access links (M8) ----
|
|
6469
|
+
/**
|
|
6470
|
+
* Mint a hosted access link. The 201 is the ONE and ONLY time `url` and
|
|
6471
|
+
* `capability` exist outside the buyer's browser — SeatLayer keeps a hash, so
|
|
6472
|
+
* there is no route, cache, or support escalation that can produce this string
|
|
6473
|
+
* again. Callers must reveal it immediately and then let it go.
|
|
6474
|
+
*
|
|
6475
|
+
* Every omitted field takes the server's default: expiry = when the event
|
|
6476
|
+
* starts, 100 redemptions, 4 seats per buyer, this channel's allocation only.
|
|
6477
|
+
* Platform bounds are enforced server-side and reported as 422 with the rule
|
|
6478
|
+
* spelled out in `ManageApiError.serverMessage`.
|
|
6479
|
+
*
|
|
6480
|
+
* Side effect by design: this also declares the channel's access intent as
|
|
6481
|
+
* `hosted_link`, so the rail stops saying "no buyer access configured".
|
|
6482
|
+
*/
|
|
6483
|
+
createAccessLink(key, channelId, input = {}) {
|
|
6484
|
+
return this.auth(
|
|
6485
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links`,
|
|
6486
|
+
{ method: "POST", body: input }
|
|
6487
|
+
);
|
|
6488
|
+
}
|
|
6489
|
+
/** Status only — label, expiry, redemptions, per-buyer cap, lineage, and the
|
|
6490
|
+
* live session count. Never the url, never the capability. Needs `:view`. */
|
|
6491
|
+
accessLinks(key, channelId) {
|
|
6492
|
+
return this.auth(
|
|
6493
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links`
|
|
6494
|
+
);
|
|
6495
|
+
}
|
|
6496
|
+
/**
|
|
6497
|
+
* Rotate — the ONLY recovery for a link nobody kept. The old URL stops opening
|
|
6498
|
+
* immediately and the response is a fresh one-time reveal.
|
|
6499
|
+
*
|
|
6500
|
+
* `endActiveSessions` is REQUIRED, not defaulted: the organizer must say
|
|
6501
|
+
* whether buyers already inside finish their checkout or lose access now. The
|
|
6502
|
+
* server answers 422 `end_active_sessions_required` if it is omitted, and that
|
|
6503
|
+
* refusal is correct — a UI must not pick either branch on their behalf.
|
|
6504
|
+
*/
|
|
6505
|
+
rotateAccessLink(key, channelId, linkId, endActiveSessions) {
|
|
6506
|
+
return this.auth(
|
|
6507
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links/${encodeURIComponent(linkId)}/rotate`,
|
|
6508
|
+
{ method: "POST", body: { endActiveSessions } }
|
|
6509
|
+
);
|
|
6510
|
+
}
|
|
6511
|
+
/** Revoke. The link stops opening immediately; `endActiveSessions` decides
|
|
6512
|
+
* whether the buyers already inside keep their sessions. */
|
|
6513
|
+
revokeAccessLink(key, channelId, linkId, endActiveSessions = false) {
|
|
6514
|
+
const qs = endActiveSessions ? "?endActiveSessions=1" : "";
|
|
6515
|
+
return this.auth(
|
|
6516
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links/${encodeURIComponent(linkId)}${qs}`,
|
|
6517
|
+
{ method: "DELETE" }
|
|
6518
|
+
);
|
|
6519
|
+
}
|
|
6394
6520
|
// ---- reports (token) ----
|
|
6395
6521
|
report(key) {
|
|
6396
6522
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
|
|
@@ -6421,6 +6547,11 @@ var ManageApi = class {
|
|
|
6421
6547
|
var POLL_MS = 1e4;
|
|
6422
6548
|
var MAX_FLAGS = 8;
|
|
6423
6549
|
var SEAT_LIST_PAGE = 300;
|
|
6550
|
+
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
6551
|
+
var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
6552
|
+
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
6553
|
+
var PREVIEW_UNAVAILABLE_STROKE = "#4b5669";
|
|
6554
|
+
var ALLOCATION_STROKE = "#101723";
|
|
6424
6555
|
var CHANNELS_CSS = `
|
|
6425
6556
|
.slm{--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
6426
6557
|
--slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
|
|
@@ -6434,6 +6565,8 @@ var CHANNELS_CSS = `
|
|
|
6434
6565
|
background:rgba(14,16,23,.88);border:1px solid var(--slm-line);font-size:10px;font-weight:800;letter-spacing:.04em;
|
|
6435
6566
|
transform:translate(-50%,-50%);white-space:nowrap}
|
|
6436
6567
|
.slm-ch-flag .mk{width:14px;height:14px;border-radius:4px;display:grid;place-items:center;font-size:8.5px;font-weight:800;color:#0e1017}
|
|
6568
|
+
.slm-ch-section-target{position:absolute;pointer-events:auto;padding:0;border:0;border-radius:8px;background:transparent;cursor:zoom-in}
|
|
6569
|
+
.slm-ch-section-target:focus-visible{outline:2px solid var(--slm-accent);outline-offset:-3px;background:color-mix(in srgb,var(--slm-accent) 12%,transparent)}
|
|
6437
6570
|
|
|
6438
6571
|
/* preview banner \u2014 raised with the organizer chrome dim, as one transition */
|
|
6439
6572
|
.slm-ch-banner{position:absolute;left:50%;top:14px;z-index:6;display:flex;align-items:center;gap:9px;padding:8px 14px;
|
|
@@ -6453,7 +6586,7 @@ var CHANNELS_CSS = `
|
|
|
6453
6586
|
transition:transform var(--slm-mo-slow) var(--slm-mo-out),opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
6454
6587
|
.slm-ch-staged.on{transform:none;opacity:1}
|
|
6455
6588
|
.slm-ch-staged.done{background:rgba(31,122,77,.96);border-color:#1f7a4d}
|
|
6456
|
-
.slm-ch-staged.shake{animation:slm-ch-shake
|
|
6589
|
+
.slm-ch-staged.shake{animation:slm-ch-shake var(--slm-mo-slow) var(--slm-mo-in-out) 2}
|
|
6457
6590
|
.slm-ch-staged b{font-variant-numeric:tabular-nums}
|
|
6458
6591
|
.slm-ch-staged .grow{flex:1}
|
|
6459
6592
|
.slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
|
|
@@ -6470,6 +6603,11 @@ var CHANNELS_CSS = `
|
|
|
6470
6603
|
.slm-ch-viewseg button{flex:1;padding:6px 8px;min-height:34px;border-radius:7px;font-size:11px;font-weight:800;color:var(--slm-muted)}
|
|
6471
6604
|
.slm-ch-viewseg button.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
6472
6605
|
.slm-ch-viewseg button:disabled{opacity:.5;cursor:not-allowed}
|
|
6606
|
+
.slm-ch-mapnav{margin:-2px 0 12px;padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
6607
|
+
.slm-ch-mapnav-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--slm-muted)}
|
|
6608
|
+
.slm-ch-mapnav-head button{color:var(--slm-accent);font-size:11px;font-weight:800;letter-spacing:0;text-transform:none;min-height:30px}
|
|
6609
|
+
.slm-ch-mapnav .slm-ch-viewseg{margin:8px 0 5px}
|
|
6610
|
+
.slm-ch-mapnav p{margin:0;font-size:11px;line-height:1.45;color:var(--slm-muted)}
|
|
6473
6611
|
.slm-ch-list{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
|
6474
6612
|
.slm-ch-row{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
6475
6613
|
text-align:left;width:100%;display:block;transition:border-color var(--slm-mo-quick) var(--slm-mo-out)}
|
|
@@ -6490,7 +6628,7 @@ var CHANNELS_CSS = `
|
|
|
6490
6628
|
font-variant-numeric:tabular-nums}
|
|
6491
6629
|
.slm-ch-counts b{color:var(--slm-text);font-weight:800}
|
|
6492
6630
|
.slm-ch-counts .free b{color:#5bd39b}
|
|
6493
|
-
.slm-ch-counts b.bump{animation:slm-ch-bump
|
|
6631
|
+
.slm-ch-counts b.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6494
6632
|
@keyframes slm-ch-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08)}}
|
|
6495
6633
|
.slm-ch-access{margin-top:6px;font-size:10.5px;color:var(--slm-muted)}
|
|
6496
6634
|
.slm-ch-more{flex:none;color:var(--slm-muted);font-weight:800;padding:0 4px;min-height:28px}
|
|
@@ -6500,12 +6638,13 @@ var CHANNELS_CSS = `
|
|
|
6500
6638
|
font-weight:800;color:#0e1017}
|
|
6501
6639
|
.slm-ch-selsrc-row b{min-width:30px;text-align:right;font-weight:800}
|
|
6502
6640
|
.slm-ch-selsrc-row span{color:var(--slm-muted)}
|
|
6503
|
-
.slm-ch-selnum.bump{animation:slm-ch-bump
|
|
6641
|
+
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6504
6642
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
6505
6643
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
6506
6644
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
6507
6645
|
line-height:1.5;margin-bottom:12px}
|
|
6508
6646
|
.slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
|
|
6647
|
+
.slm-ch-alert.info{background:rgba(110,123,255,.12);border:1px solid rgba(110,123,255,.44);color:#c5cbff}
|
|
6509
6648
|
.slm-ch-alert.err{background:rgba(229,72,77,.1);border:1px solid rgba(229,72,77,.45);color:#f1a4a6}
|
|
6510
6649
|
.slm-ch-alert b{color:#fff}
|
|
6511
6650
|
.slm-ch-alert button{display:block;margin-top:6px;color:#fff;font-weight:800;min-height:36px}
|
|
@@ -6543,6 +6682,26 @@ var CHANNELS_CSS = `
|
|
|
6543
6682
|
border-radius:10px;background:rgba(244,183,64,.06);font-family:ui-monospace,Menlo,monospace;font-size:11px;
|
|
6544
6683
|
overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
6545
6684
|
.slm-ch-err{color:#f1a4a6;font-size:11.5px;margin-top:6px}
|
|
6685
|
+
|
|
6686
|
+
/* hosted access links \u2014 STATUS only; there is no Copy control on this card */
|
|
6687
|
+
.slm-ch-link{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
6688
|
+
margin-bottom:8px}
|
|
6689
|
+
.slm-ch-link .lk-head{display:flex;align-items:center;gap:8px}
|
|
6690
|
+
.slm-ch-link .lk-name{flex:1;min-width:0;font-size:12.5px;font-weight:800;overflow:hidden;text-overflow:ellipsis;
|
|
6691
|
+
white-space:nowrap}
|
|
6692
|
+
.slm-ch-lkrow{display:flex;gap:8px;margin-top:5px;font-size:11px;color:var(--slm-muted)}
|
|
6693
|
+
.slm-ch-lkrow .k{flex:none;min-width:104px}
|
|
6694
|
+
.slm-ch-lkrow .v{color:var(--slm-text);font-variant-numeric:tabular-nums}
|
|
6695
|
+
.slm-ch-meter{height:5px;border-radius:3px;background:rgba(255,255,255,.09);overflow:hidden;margin-top:8px}
|
|
6696
|
+
.slm-ch-meter i{display:block;height:100%;background:var(--slm-accent);
|
|
6697
|
+
transition:width var(--slm-mo-base) var(--slm-mo-out)}
|
|
6698
|
+
.slm-ch-radio{display:flex;gap:9px;align-items:flex-start;padding:11px 12px;border:1px solid var(--slm-line);
|
|
6699
|
+
border-radius:10px;margin-top:8px;font-size:12.5px;cursor:pointer;
|
|
6700
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out)}
|
|
6701
|
+
.slm-ch-radio:hover{border-color:var(--slm-muted)}
|
|
6702
|
+
.slm-ch-radio input{flex:none;margin-top:2px}
|
|
6703
|
+
.slm-ch-radio b{display:block;font-weight:800;margin-bottom:2px}
|
|
6704
|
+
.slm-ch-radio .why{display:block;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
6546
6705
|
.slm-ch-seatlist{max-height:44vh;overflow:auto;border:1px solid var(--slm-line);border-radius:10px;
|
|
6547
6706
|
background:var(--slm-surface);margin-top:10px}
|
|
6548
6707
|
.slm-ch-seatgroup{padding:8px 10px;border-bottom:1px solid var(--slm-line);display:flex;align-items:center;
|
|
@@ -6575,7 +6734,8 @@ var CHANNELS_CSS = `
|
|
|
6575
6734
|
.slm.compact .slm-modes{display:none}
|
|
6576
6735
|
|
|
6577
6736
|
@media (prefers-reduced-motion:reduce){
|
|
6578
|
-
.slm-ch-layer,.slm-ch-banner,.slm-ch-staged,.slm-ch-row,.slm.compact.ch-sheet .slm-rail
|
|
6737
|
+
.slm-ch-layer,.slm-ch-banner,.slm-ch-staged,.slm-ch-row,.slm.compact.ch-sheet .slm-rail,
|
|
6738
|
+
.slm-ch-meter i,.slm-ch-radio{transition:none!important}
|
|
6579
6739
|
.slm-ch-staged.shake,.slm-ch-tick,.slm-ch-bucket,.slm-ch-scrim,.slm-ch-dialog,
|
|
6580
6740
|
.slm-ch-counts b.bump,.slm-ch-selnum.bump{animation:none!important}
|
|
6581
6741
|
.slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
|
|
@@ -6591,9 +6751,37 @@ function bucketRowsHtml(rows) {
|
|
|
6591
6751
|
${row.peek ? `<span class="peek">${esc(row.peek)}</span>` : "<span></span>"}
|
|
6592
6752
|
</div>`).join("");
|
|
6593
6753
|
}
|
|
6754
|
+
var SERVER_INTEGRATION_HTML = `
|
|
6755
|
+
<p class="slm-eyebrow" style="margin-top:18px">Server integration</p>
|
|
6756
|
+
<p class="slm-hint">There is nothing to set up on this screen. Your own server mints a short-lived buyer
|
|
6757
|
+
access session for this channel with the SeatLayer server SDK and hands it to the widget. A channel name
|
|
6758
|
+
on its own never grants access.</p>
|
|
6759
|
+
<p class="slm-note"><a class="slm-linkbtn" href="https://docs.seatlayer.io/server-api/channels"
|
|
6760
|
+
target="_blank" rel="noreferrer noopener">Read the server integration guide \u2192</a></p>`;
|
|
6594
6761
|
function esc(value) {
|
|
6595
6762
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6596
6763
|
}
|
|
6764
|
+
function datetimeLocalValue(ms) {
|
|
6765
|
+
const local = new Date(ms - new Date(ms).getTimezoneOffset() * 6e4);
|
|
6766
|
+
return local.toISOString().slice(0, 16);
|
|
6767
|
+
}
|
|
6768
|
+
function intField(root, selector) {
|
|
6769
|
+
const raw = root.querySelector(selector)?.value.trim() ?? "";
|
|
6770
|
+
const value = Number(raw);
|
|
6771
|
+
return raw !== "" && Number.isInteger(value) ? value : null;
|
|
6772
|
+
}
|
|
6773
|
+
function selectSecret(dialog) {
|
|
6774
|
+
const node = dialog.querySelector("[data-ch-lk-url]");
|
|
6775
|
+
if (!node) return;
|
|
6776
|
+
try {
|
|
6777
|
+
const range = document.createRange();
|
|
6778
|
+
range.selectNodeContents(node);
|
|
6779
|
+
const selection = window.getSelection();
|
|
6780
|
+
selection?.removeAllRanges();
|
|
6781
|
+
selection?.addRange(range);
|
|
6782
|
+
} catch {
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6597
6785
|
var ChannelsMode = class {
|
|
6598
6786
|
constructor(host, capabilities) {
|
|
6599
6787
|
this.active = false;
|
|
@@ -6603,6 +6791,10 @@ var ChannelsMode = class {
|
|
|
6603
6791
|
this.loadError = null;
|
|
6604
6792
|
this.loading = true;
|
|
6605
6793
|
this.view = "inspect";
|
|
6794
|
+
/** Pan is intentionally the initial desktop interaction. Assignment's
|
|
6795
|
+
* marquee is powerful, but must never make an organizer lose map navigation. */
|
|
6796
|
+
this.mapIntent = "pan";
|
|
6797
|
+
this.focusedSectionId = null;
|
|
6606
6798
|
this.showArchived = false;
|
|
6607
6799
|
this.detailChannelId = null;
|
|
6608
6800
|
this.targetChannelId = "";
|
|
@@ -6610,6 +6802,24 @@ var ChannelsMode = class {
|
|
|
6610
6802
|
this.dialog = null;
|
|
6611
6803
|
this.detent = "medium";
|
|
6612
6804
|
this.seatListLimit = SEAT_LIST_PAGE;
|
|
6805
|
+
/**
|
|
6806
|
+
* Hosted-link STATUS for the channel whose detail panel is open. This is the
|
|
6807
|
+
* listing projection — it carries no url and no capability, because no route
|
|
6808
|
+
* returns one. `unsupported` is the honest answer for a worker that predates
|
|
6809
|
+
* M8, exactly like the buyer-preview probe.
|
|
6810
|
+
*/
|
|
6811
|
+
this.links = [];
|
|
6812
|
+
this.linksChannelId = null;
|
|
6813
|
+
this.linksState = "idle";
|
|
6814
|
+
/**
|
|
6815
|
+
* Monotonic read generations — one for the channel list + allocation, one for
|
|
6816
|
+
* the open channel's links. Reads are concurrent (a 10s poll versus a
|
|
6817
|
+
* mutation's own reload), and the network does not promise to answer them in
|
|
6818
|
+
* order. Only the NEWEST read of each kind may write to state; an older
|
|
6819
|
+
* answer that arrives late is dropped, never painted.
|
|
6820
|
+
*/
|
|
6821
|
+
this.listSeq = 0;
|
|
6822
|
+
this.linksSeq = 0;
|
|
6613
6823
|
this.previewAudience = [];
|
|
6614
6824
|
this.previewIncludePublic = false;
|
|
6615
6825
|
this.previewProjection = null;
|
|
@@ -6636,10 +6846,14 @@ var ChannelsMode = class {
|
|
|
6636
6846
|
enter() {
|
|
6637
6847
|
if (this.active) return;
|
|
6638
6848
|
this.active = true;
|
|
6849
|
+
this.mapIntent = "pan";
|
|
6850
|
+
this.focusedSectionId = null;
|
|
6639
6851
|
this.ensureLayer();
|
|
6640
6852
|
this.host.root.classList.add("ch-mode");
|
|
6641
6853
|
this.applySheetClasses();
|
|
6854
|
+
if (this.host.sections().length > 1) this.host.showSectionOverview();
|
|
6642
6855
|
this.paintRail();
|
|
6856
|
+
this.onInteractionChange?.();
|
|
6643
6857
|
void this.refresh();
|
|
6644
6858
|
this.pollTimer = setInterval(() => {
|
|
6645
6859
|
void this.refresh({ quiet: true });
|
|
@@ -6653,6 +6867,9 @@ var ChannelsMode = class {
|
|
|
6653
6867
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
6654
6868
|
this.pollTimer = null;
|
|
6655
6869
|
this.closeDialog({ restoreFocus: false });
|
|
6870
|
+
this.links = [];
|
|
6871
|
+
this.linksChannelId = null;
|
|
6872
|
+
this.linksState = "idle";
|
|
6656
6873
|
this.layer?.classList.remove("on");
|
|
6657
6874
|
this.host.root.classList.remove(
|
|
6658
6875
|
"ch-mode",
|
|
@@ -6688,6 +6905,17 @@ var ChannelsMode = class {
|
|
|
6688
6905
|
canSelect() {
|
|
6689
6906
|
return this.caps.manage && this.view === "inspect";
|
|
6690
6907
|
}
|
|
6908
|
+
/** Bulk seat assignment is explicit. In Pan map, clicks can still inspect a
|
|
6909
|
+
* single seat, while a primary-button drag always moves the camera. */
|
|
6910
|
+
usesMarqueeSelection() {
|
|
6911
|
+
return this.canSelect() && this.mapIntent === "assign";
|
|
6912
|
+
}
|
|
6913
|
+
/** The renderer calls this when the organizer opens a section from overview. */
|
|
6914
|
+
handleSectionFocus(sectionId) {
|
|
6915
|
+
if (!this.active) return;
|
|
6916
|
+
this.focusedSectionId = sectionId;
|
|
6917
|
+
this.paintRail();
|
|
6918
|
+
}
|
|
6691
6919
|
/**
|
|
6692
6920
|
* Organizer realtime integration point. M5 ships a per-scope socket for
|
|
6693
6921
|
* buyers; the organizer channel-count stream is a later milestone. When it
|
|
@@ -6715,21 +6943,28 @@ var ChannelsMode = class {
|
|
|
6715
6943
|
// ---- data -----------------------------------------------------------------
|
|
6716
6944
|
async refresh(opts = {}) {
|
|
6717
6945
|
if (!this.caps.view) return;
|
|
6946
|
+
const seq = ++this.listSeq;
|
|
6947
|
+
const superseded = () => seq !== this.listSeq;
|
|
6718
6948
|
try {
|
|
6719
6949
|
const list = await this.host.api.channels(this.host.eventKey, { includeArchived: this.showArchived });
|
|
6950
|
+
if (superseded()) return;
|
|
6720
6951
|
this.list = list;
|
|
6721
6952
|
this.assignmentVersion = list.assignmentVersion;
|
|
6722
6953
|
this.loadError = null;
|
|
6723
6954
|
if (!this.targetChannelId) {
|
|
6724
6955
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
6725
6956
|
}
|
|
6726
|
-
await this.loadAllocation();
|
|
6957
|
+
await this.loadAllocation(seq);
|
|
6958
|
+
if (superseded()) return;
|
|
6959
|
+
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
6960
|
+
if (superseded()) return;
|
|
6727
6961
|
this.loading = false;
|
|
6728
6962
|
if (this.active) {
|
|
6729
6963
|
this.paintRail();
|
|
6730
6964
|
this.paintOverlay();
|
|
6731
6965
|
}
|
|
6732
6966
|
} catch (err) {
|
|
6967
|
+
if (superseded()) return;
|
|
6733
6968
|
this.loading = false;
|
|
6734
6969
|
if (err instanceof ManageApiError && err.status === 403) {
|
|
6735
6970
|
this.caps = { view: false, manage: false };
|
|
@@ -6741,10 +6976,11 @@ var ChannelsMode = class {
|
|
|
6741
6976
|
}
|
|
6742
6977
|
/** Walk every allocation page. Bounded by the event's seat count, and the
|
|
6743
6978
|
* server caps each page, so an arena is a handful of round trips. */
|
|
6744
|
-
async loadAllocation() {
|
|
6979
|
+
async loadAllocation(seq) {
|
|
6745
6980
|
const next = /* @__PURE__ */ new Map();
|
|
6746
6981
|
let afterLabel;
|
|
6747
6982
|
for (let page = 0; page < 200; page += 1) {
|
|
6983
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6748
6984
|
const res = await this.host.api.channelAllocation(this.host.eventKey, {
|
|
6749
6985
|
afterLabel,
|
|
6750
6986
|
limit: 1e3
|
|
@@ -6756,6 +6992,7 @@ var ChannelsMode = class {
|
|
|
6756
6992
|
if (!res.nextAfterLabel) break;
|
|
6757
6993
|
afterLabel = res.nextAfterLabel;
|
|
6758
6994
|
}
|
|
6995
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6759
6996
|
this.allocation = next;
|
|
6760
6997
|
}
|
|
6761
6998
|
// ---- lookups --------------------------------------------------------------
|
|
@@ -6822,9 +7059,10 @@ var ChannelsMode = class {
|
|
|
6822
7059
|
* Repaint the allocation (or preview) overlay in ONE canvas pass.
|
|
6823
7060
|
*
|
|
6824
7061
|
* Channel identity on the map is a fill in the administrative color PLUS the
|
|
6825
|
-
* letter flags below — never color alone.
|
|
6826
|
-
*
|
|
6827
|
-
*
|
|
7062
|
+
* letter flags below — never color alone. In buyer preview the map instead
|
|
7063
|
+
* uses two explicit, channel-neutral access states. Physical status keeps its
|
|
7064
|
+
* own cue: only FREE units are repainted, so sold/held/blocked seats still
|
|
7065
|
+
* read exactly as they do in every other tool.
|
|
6828
7066
|
*/
|
|
6829
7067
|
paintOverlay() {
|
|
6830
7068
|
const canvas = this.canvas;
|
|
@@ -6849,11 +7087,14 @@ var ChannelsMode = class {
|
|
|
6849
7087
|
if (!ctx) return;
|
|
6850
7088
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
6851
7089
|
ctx.clearRect(0, 0, width, height);
|
|
7090
|
+
const seatDetail = this.host.isSeatDetail();
|
|
6852
7091
|
const size = Math.max(3, this.host.seatPixelSize());
|
|
6853
7092
|
const half = size / 2;
|
|
6854
7093
|
const projection = this.view === "preview" ? this.previewProjection : null;
|
|
6855
7094
|
const eligible = projection ? new Set(projection.available === false ? [] : projection.eligible ?? []) : null;
|
|
6856
7095
|
const clusters = /* @__PURE__ */ new Map();
|
|
7096
|
+
const sectionTargets = !seatDetail && this.host.sections().length > 1 ? /* @__PURE__ */ new Map() : null;
|
|
7097
|
+
const previewSections = this.view === "preview" && seatDetail && size <= 15 ? /* @__PURE__ */ new Map() : null;
|
|
6857
7098
|
for (const seat of this.host.seats()) {
|
|
6858
7099
|
const status = this.host.statusOf(seat.label) ?? "free";
|
|
6859
7100
|
const channelId = this.allocation.get(seat.label) ?? PUBLIC_CHANNEL_ID;
|
|
@@ -6864,24 +7105,150 @@ var ChannelsMode = class {
|
|
|
6864
7105
|
cluster.n += 1;
|
|
6865
7106
|
clusters.set(channelId, cluster);
|
|
6866
7107
|
}
|
|
7108
|
+
if (!seatDetail) {
|
|
7109
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7110
|
+
const point2 = section ? this.host.worldToScreen({ x: seat.x, y: seat.y }) : null;
|
|
7111
|
+
if (sectionTargets && section && point2) {
|
|
7112
|
+
const bounds = sectionTargets.get(section.id) ?? {
|
|
7113
|
+
label: section.label,
|
|
7114
|
+
minX: point2.x,
|
|
7115
|
+
minY: point2.y,
|
|
7116
|
+
maxX: point2.x,
|
|
7117
|
+
maxY: point2.y
|
|
7118
|
+
};
|
|
7119
|
+
bounds.minX = Math.min(bounds.minX, point2.x);
|
|
7120
|
+
bounds.minY = Math.min(bounds.minY, point2.y);
|
|
7121
|
+
bounds.maxX = Math.max(bounds.maxX, point2.x);
|
|
7122
|
+
bounds.maxY = Math.max(bounds.maxY, point2.y);
|
|
7123
|
+
sectionTargets.set(section.id, bounds);
|
|
7124
|
+
}
|
|
7125
|
+
continue;
|
|
7126
|
+
}
|
|
6867
7127
|
if (status !== "free") continue;
|
|
6868
7128
|
let fill = null;
|
|
7129
|
+
let stroke = null;
|
|
6869
7130
|
if (this.view === "preview") {
|
|
6870
|
-
|
|
7131
|
+
if (eligible?.has(seat.label)) {
|
|
7132
|
+
fill = PREVIEW_ELIGIBLE_FILL;
|
|
7133
|
+
stroke = PREVIEW_ELIGIBLE_STROKE;
|
|
7134
|
+
} else {
|
|
7135
|
+
fill = PREVIEW_UNAVAILABLE_FILL;
|
|
7136
|
+
stroke = PREVIEW_UNAVAILABLE_STROKE;
|
|
7137
|
+
}
|
|
6871
7138
|
} else if (channelId !== PUBLIC_CHANNEL_ID) {
|
|
6872
7139
|
fill = this.markerFor(channelId).color;
|
|
7140
|
+
stroke = ALLOCATION_STROKE;
|
|
6873
7141
|
}
|
|
6874
7142
|
if (!fill) continue;
|
|
6875
7143
|
const point = this.host.worldToScreen({ x: seat.x, y: seat.y });
|
|
6876
7144
|
if (!point) continue;
|
|
6877
7145
|
if (point.x < -size || point.y < -size || point.x > width + size || point.y > height + size) continue;
|
|
7146
|
+
if (previewSections) {
|
|
7147
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7148
|
+
if (section) {
|
|
7149
|
+
const bounds = previewSections.get(section.id) ?? {
|
|
7150
|
+
label: section.label,
|
|
7151
|
+
minX: point.x,
|
|
7152
|
+
minY: point.y,
|
|
7153
|
+
maxX: point.x,
|
|
7154
|
+
maxY: point.y
|
|
7155
|
+
};
|
|
7156
|
+
bounds.minX = Math.min(bounds.minX, point.x);
|
|
7157
|
+
bounds.minY = Math.min(bounds.minY, point.y);
|
|
7158
|
+
bounds.maxX = Math.max(bounds.maxX, point.x);
|
|
7159
|
+
bounds.maxY = Math.max(bounds.maxY, point.y);
|
|
7160
|
+
previewSections.set(section.id, bounds);
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
6878
7163
|
ctx.fillStyle = fill;
|
|
6879
|
-
|
|
6880
|
-
|
|
7164
|
+
if (this.view === "preview" || channelId !== PUBLIC_CHANNEL_ID) {
|
|
7165
|
+
const radius = Math.max(2, half + Math.min(1.5, half * 0.06));
|
|
7166
|
+
ctx.globalAlpha = 1;
|
|
7167
|
+
ctx.beginPath();
|
|
7168
|
+
ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
|
|
7169
|
+
ctx.fill();
|
|
7170
|
+
ctx.strokeStyle = stroke ?? fill;
|
|
7171
|
+
ctx.lineWidth = this.view === "preview" ? Math.max(1, Math.min(1.75, size * 0.13)) : Math.max(1, Math.min(1.5, size * 0.1));
|
|
7172
|
+
ctx.stroke();
|
|
7173
|
+
if (this.view === "preview" && eligible?.has(seat.label) && size >= 22) {
|
|
7174
|
+
this.paintPreviewSeatLabel(ctx, seat.label, point.x, point.y, radius);
|
|
7175
|
+
}
|
|
7176
|
+
} else {
|
|
7177
|
+
ctx.globalAlpha = 0.85;
|
|
7178
|
+
ctx.fillRect(point.x - half, point.y - half, size, size);
|
|
7179
|
+
}
|
|
6881
7180
|
}
|
|
6882
7181
|
ctx.globalAlpha = 1;
|
|
7182
|
+
if (previewSections) this.paintPreviewSectionLabels(ctx, previewSections);
|
|
7183
|
+
this.paintSectionTargets(sectionTargets);
|
|
6883
7184
|
this.paintFlags(clusters);
|
|
6884
7185
|
}
|
|
7186
|
+
/** Draw an eligible seat's actual chart label without inventing a new buyer
|
|
7187
|
+
* identifier. Long labels scale down and are omitted rather than overflowing
|
|
7188
|
+
* into an adjacent seat. */
|
|
7189
|
+
paintPreviewSeatLabel(ctx, label, x, y, radius) {
|
|
7190
|
+
const maxWidth = radius * 1.55;
|
|
7191
|
+
let fontSize = Math.min(13, Math.max(7, radius * 0.55));
|
|
7192
|
+
const minFontSize = 6;
|
|
7193
|
+
while (fontSize >= minFontSize) {
|
|
7194
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7195
|
+
if (ctx.measureText(label).width <= maxWidth) break;
|
|
7196
|
+
fontSize -= 0.5;
|
|
7197
|
+
}
|
|
7198
|
+
if (fontSize < minFontSize) return;
|
|
7199
|
+
ctx.fillStyle = "#ffffff";
|
|
7200
|
+
ctx.textAlign = "center";
|
|
7201
|
+
ctx.textBaseline = "middle";
|
|
7202
|
+
ctx.fillText(label, x, y);
|
|
7203
|
+
}
|
|
7204
|
+
/** A section overview is a navigation map. These transparent, keyboardable
|
|
7205
|
+
* hit areas sit over the renderer's section shells so both mouse and keyboard
|
|
7206
|
+
* always take the organizer into the real focused-section camera state. */
|
|
7207
|
+
paintSectionTargets(sections) {
|
|
7208
|
+
const layer = this.layer;
|
|
7209
|
+
if (!layer) return;
|
|
7210
|
+
layer.querySelectorAll(".slm-ch-section-target").forEach((el) => el.remove());
|
|
7211
|
+
if (!sections) return;
|
|
7212
|
+
for (const [id, section] of sections) {
|
|
7213
|
+
const width = section.maxX - section.minX;
|
|
7214
|
+
const height = section.maxY - section.minY;
|
|
7215
|
+
if (width < 20 || height < 20) continue;
|
|
7216
|
+
const target = document.createElement("button");
|
|
7217
|
+
target.type = "button";
|
|
7218
|
+
target.className = "slm-ch-section-target";
|
|
7219
|
+
target.style.left = `${section.minX - 8}px`;
|
|
7220
|
+
target.style.top = `${section.minY - 8}px`;
|
|
7221
|
+
target.style.width = `${width + 16}px`;
|
|
7222
|
+
target.style.height = `${height + 16}px`;
|
|
7223
|
+
target.setAttribute("aria-label", `Open ${section.label} seats`);
|
|
7224
|
+
target.addEventListener("click", () => {
|
|
7225
|
+
this.focusedSectionId = id;
|
|
7226
|
+
this.host.focusSection(id);
|
|
7227
|
+
this.paintRail();
|
|
7228
|
+
});
|
|
7229
|
+
layer.appendChild(target);
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
/** Keep renderer section names legible over a dense, zoomed-out preview. */
|
|
7233
|
+
paintPreviewSectionLabels(ctx, sections) {
|
|
7234
|
+
for (const section of sections.values()) {
|
|
7235
|
+
const width = section.maxX - section.minX;
|
|
7236
|
+
const height = section.maxY - section.minY;
|
|
7237
|
+
if (width < 52 || height < 26) continue;
|
|
7238
|
+
const centerX = (section.minX + section.maxX) / 2;
|
|
7239
|
+
const centerY = (section.minY + section.maxY) / 2;
|
|
7240
|
+
const fontSize = Math.max(11, Math.min(15, height * 0.16));
|
|
7241
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7242
|
+
const labelWidth = Math.min(width - 8, ctx.measureText(section.label).width + 18);
|
|
7243
|
+
const labelHeight = fontSize + 10;
|
|
7244
|
+
ctx.fillStyle = "rgba(11, 16, 28, .88)";
|
|
7245
|
+
ctx.fillRect(centerX - labelWidth / 2, centerY - labelHeight / 2, labelWidth, labelHeight);
|
|
7246
|
+
ctx.fillStyle = "#f8fafc";
|
|
7247
|
+
ctx.textAlign = "center";
|
|
7248
|
+
ctx.textBaseline = "middle";
|
|
7249
|
+
ctx.fillText(section.label, centerX, centerY);
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
6885
7252
|
/** Letter flags at each channel's centroid — the non-color identity cue. */
|
|
6886
7253
|
paintFlags(clusters) {
|
|
6887
7254
|
const layer = this.layer;
|
|
@@ -6947,7 +7314,7 @@ var ChannelsMode = class {
|
|
|
6947
7314
|
});
|
|
6948
7315
|
});
|
|
6949
7316
|
}
|
|
6950
|
-
setBanner(on, name = "") {
|
|
7317
|
+
setBanner(on, name = "", eligibleSeats) {
|
|
6951
7318
|
const banner = this.bannerEl;
|
|
6952
7319
|
if (!banner) return;
|
|
6953
7320
|
this.host.root.classList.toggle("ch-preview", on);
|
|
@@ -6957,8 +7324,9 @@ var ChannelsMode = class {
|
|
|
6957
7324
|
return;
|
|
6958
7325
|
}
|
|
6959
7326
|
const marker = this.previewAudience.length === 1 ? this.markerFor(this.previewAudience[0]) : { color: "var(--slm-accent)", letter: "" };
|
|
7327
|
+
const availability = eligibleSeats == null ? "" : ` \xB7 ${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat" : "seats"} available now`;
|
|
6960
7328
|
banner.innerHTML = `<span class="dot" style="background:${esc(marker.color)}"></span>
|
|
6961
|
-
Previewing buyer access \xB7 ${esc(name)} \xB7 read-only
|
|
7329
|
+
Previewing buyer access \xB7 ${esc(name)}${availability} \xB7 read-only
|
|
6962
7330
|
<button type="button" data-ch-act="exit-preview">Exit preview</button>`;
|
|
6963
7331
|
banner.classList.add("on");
|
|
6964
7332
|
banner.querySelector('[data-ch-act="exit-preview"]')?.addEventListener("click", () => this.setView("inspect"));
|
|
@@ -6993,6 +7361,8 @@ var ChannelsMode = class {
|
|
|
6993
7361
|
includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
|
|
6994
7362
|
});
|
|
6995
7363
|
this.previewSupported = true;
|
|
7364
|
+
const eligibleSeats = this.previewProjection.available === false ? void 0 : this.previewProjection.counts?.eligible ?? this.previewProjection.eligible?.length;
|
|
7365
|
+
this.setBanner(true, names, eligibleSeats);
|
|
6996
7366
|
} catch (err) {
|
|
6997
7367
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
6998
7368
|
this.previewSupported = !(status === 404 || status === 405 || status === 501);
|
|
@@ -7043,6 +7413,22 @@ var ChannelsMode = class {
|
|
|
7043
7413
|
aria-pressed="${this.view === "inspect"}">Inspect allocation</button>
|
|
7044
7414
|
<button type="button" class="${previewOn.trim()}" data-ch-view="preview"
|
|
7045
7415
|
aria-pressed="${this.view === "preview"}">Preview buyer access</button>
|
|
7416
|
+
</div>${this.mapNavigationHtml()}`;
|
|
7417
|
+
}
|
|
7418
|
+
mapNavigationHtml() {
|
|
7419
|
+
if (this.host.sections().length < 2) return "";
|
|
7420
|
+
const focused = this.focusedSectionId ? this.host.sections().find((section) => section.id === this.focusedSectionId)?.label ?? "section" : null;
|
|
7421
|
+
const panOn = this.mapIntent === "pan" ? " on" : "";
|
|
7422
|
+
const assignOn = this.mapIntent === "assign" ? " on" : "";
|
|
7423
|
+
const intent = this.view === "inspect" && this.caps.manage ? `<div class="slm-ch-viewseg" role="group" aria-label="Map interaction">
|
|
7424
|
+
<button type="button" class="${panOn.trim()}" data-ch-map="pan" aria-pressed="${this.mapIntent === "pan"}">Pan map</button>
|
|
7425
|
+
<button type="button" class="${assignOn.trim()}" data-ch-map="assign" aria-pressed="${this.mapIntent === "assign"}">Assign seats</button>
|
|
7426
|
+
</div>
|
|
7427
|
+
<p>${this.mapIntent === "pan" ? "Drag to explore. Click a section to open its seats." : "Drag across seats to select them for allocation."}</p>` : "<p>Drag to explore. Click a section to open its seats.</p>";
|
|
7428
|
+
return `<div class="slm-ch-mapnav">
|
|
7429
|
+
<div class="slm-ch-mapnav-head"><span>${focused ? `Viewing ${esc(focused)}` : "Section overview"}</span>
|
|
7430
|
+
<button type="button" data-ch-act="sections">All sections</button></div>
|
|
7431
|
+
${intent}
|
|
7046
7432
|
</div>`;
|
|
7047
7433
|
}
|
|
7048
7434
|
countsHtml(counts, key) {
|
|
@@ -7161,11 +7547,8 @@ var ChannelsMode = class {
|
|
|
7161
7547
|
<p class="slm-hint">${channel.access?.hasActiveGrants ? "Buyer access is live. Only this audience can buy from the allocation." : "No buyer access is configured yet. The allocation is protected \u2014 it is not available to Public sale."}</p>
|
|
7162
7548
|
${selfServiceGap ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
7163
7549
|
<span>This channel is marked for buyer self-service but no buyer has been let in yet.</span></div>` : ""}
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
<div class="slm-ch-row2"><button type="button" class="slm-btn ghost" data-ch-act="server-access" disabled
|
|
7167
|
-
title="Guided server setup ships in the next milestone">Configure server integration \xB7 Coming soon</button></div>
|
|
7168
|
-
<p class="slm-note">Your own server can already mint buyer access sessions for this channel with the server SDK.</p>` : "";
|
|
7550
|
+
${this.hostedLinksHtml()}
|
|
7551
|
+
${SERVER_INTEGRATION_HTML}` : "";
|
|
7169
7552
|
return `
|
|
7170
7553
|
<p class="slm-eyebrow">
|
|
7171
7554
|
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
@@ -7175,6 +7558,92 @@ var ChannelsMode = class {
|
|
|
7175
7558
|
${access}
|
|
7176
7559
|
${lifecycle}`;
|
|
7177
7560
|
}
|
|
7561
|
+
// ---- hosted access links --------------------------------------------------
|
|
7562
|
+
/**
|
|
7563
|
+
* Read the status projection for the open channel. Never paints — the caller
|
|
7564
|
+
* decides when the rail repaints, so a poll-driven reload does not fight a
|
|
7565
|
+
* user-driven one. A worker without M8 answers 404/405 and gets the honest
|
|
7566
|
+
* "needs a newer server" line rather than an error toast.
|
|
7567
|
+
*/
|
|
7568
|
+
async loadLinks(channelId) {
|
|
7569
|
+
if (!this.caps.view) return;
|
|
7570
|
+
const seq = ++this.linksSeq;
|
|
7571
|
+
const superseded = () => seq !== this.linksSeq || this.linksChannelId !== channelId;
|
|
7572
|
+
if (this.linksChannelId !== channelId) {
|
|
7573
|
+
this.links = [];
|
|
7574
|
+
this.linksChannelId = channelId;
|
|
7575
|
+
this.linksState = "loading";
|
|
7576
|
+
}
|
|
7577
|
+
try {
|
|
7578
|
+
const res = await this.host.api.accessLinks(this.host.eventKey, channelId);
|
|
7579
|
+
if (superseded()) return;
|
|
7580
|
+
this.links = res.links ?? [];
|
|
7581
|
+
this.linksState = "ready";
|
|
7582
|
+
} catch (err) {
|
|
7583
|
+
if (superseded()) return;
|
|
7584
|
+
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7585
|
+
this.links = [];
|
|
7586
|
+
this.linksState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
|
|
7587
|
+
if (this.linksState === "error") this.host.onError(err);
|
|
7588
|
+
}
|
|
7589
|
+
}
|
|
7590
|
+
/**
|
|
7591
|
+
* The hosted-link section of the detail panel.
|
|
7592
|
+
*
|
|
7593
|
+
* STATUS ONLY, by design (comp 06 `hosted`): label, state, expiry,
|
|
7594
|
+
* redemptions, seats per buyer, live sessions. There is no Copy control here
|
|
7595
|
+
* and no field to hang one on — the URL was shown once at creation and cannot
|
|
7596
|
+
* be produced again. Rotation is the recovery path, and it says so.
|
|
7597
|
+
*/
|
|
7598
|
+
hostedLinksHtml() {
|
|
7599
|
+
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Hosted access links</p>`;
|
|
7600
|
+
if (this.linksState === "unsupported") {
|
|
7601
|
+
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
7602
|
+
<span><b>Hosted links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
7603
|
+
}
|
|
7604
|
+
if (this.linksState === "error") {
|
|
7605
|
+
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
7606
|
+
<span><b>Couldn't load this channel's links.</b>
|
|
7607
|
+
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
7608
|
+
}
|
|
7609
|
+
const live = this.links.filter(accessLinkIsLive).length;
|
|
7610
|
+
const cards = this.linksState === "loading" && !this.links.length ? `<div class="slm-empty">Loading links\u2026</div>` : this.links.length ? this.links.map((link) => this.linkCardHtml(link)).join("") : `<p class="slm-hint">No hosted link yet. Create one to send this allocation to a named group \u2014
|
|
7611
|
+
they open the link and buy only these seats.</p>`;
|
|
7612
|
+
const create = this.caps.manage ? `<button type="button" class="slm-btn" style="width:100%" data-ch-act="link-create">
|
|
7613
|
+
${live ? "Create another hosted link" : "Create hosted access link"}</button>` : "";
|
|
7614
|
+
return `${eyebrow}
|
|
7615
|
+
${cards}
|
|
7616
|
+
${create}
|
|
7617
|
+
<p class="slm-note">A link is shown once, when you create it. SeatLayer keeps only a fingerprint of it, so it
|
|
7618
|
+
can never be shown again \u2014 if a link is lost, rotate it and send the fresh one.</p>`;
|
|
7619
|
+
}
|
|
7620
|
+
linkCardHtml(link) {
|
|
7621
|
+
const badge = accessLinkBadge(link);
|
|
7622
|
+
const used = link.maxRedemptions > 0 ? Math.min(100, Math.round(link.redemptions / link.maxRedemptions * 100)) : 0;
|
|
7623
|
+
const rows = accessLinkPolicyLines(link).map((row) => `<div class="slm-ch-lkrow"><span class="k">${esc(row.k)}</span>
|
|
7624
|
+
<span class="v">${esc(row.v)}</span></div>`).join("");
|
|
7625
|
+
const sessions = link.activeSessions ? `<div class="slm-ch-lkrow"><span class="k">Buyers inside now</span>
|
|
7626
|
+
<span class="v">${link.activeSessions.toLocaleString()}</span></div>` : "";
|
|
7627
|
+
const lastUsed = link.lastRedeemedAt ? `<div class="slm-ch-lkrow"><span class="k">Last opened</span>
|
|
7628
|
+
<span class="v">${esc(new Date(link.lastRedeemedAt).toLocaleString())}</span></div>` : "";
|
|
7629
|
+
const actions = this.caps.manage && accessLinkIsLive(link) ? `<div class="slm-ch-row2">
|
|
7630
|
+
<button type="button" class="slm-btn ghost" data-ch-rotate="${esc(link.id)}">Rotate</button>
|
|
7631
|
+
<button type="button" class="slm-btn ghost" data-ch-revoke="${esc(link.id)}">Revoke</button>
|
|
7632
|
+
</div>` : "";
|
|
7633
|
+
return `<div class="slm-ch-link">
|
|
7634
|
+
<span class="lk-head">
|
|
7635
|
+
<span class="lk-name">${esc(link.label || "Hosted link")}</span>
|
|
7636
|
+
<span class="slm-ch-badge ${badge.kind}">${esc(badge.text)}</span>
|
|
7637
|
+
</span>
|
|
7638
|
+
<div class="slm-ch-meter" role="img"
|
|
7639
|
+
aria-label="${link.redemptions.toLocaleString()} of ${link.maxRedemptions.toLocaleString()} redemptions used">
|
|
7640
|
+
<i style="width:${used}%"></i></div>
|
|
7641
|
+
${rows}${sessions}${lastUsed}
|
|
7642
|
+
<div class="slm-ch-lkrow"><span class="k">The URL</span>
|
|
7643
|
+
<span class="v">Revealed once at creation \u2014 not recoverable</span></div>
|
|
7644
|
+
${actions}
|
|
7645
|
+
</div>`;
|
|
7646
|
+
}
|
|
7178
7647
|
previewRailHtml() {
|
|
7179
7648
|
const audienceOptions = [
|
|
7180
7649
|
{ id: PUBLIC_CHANNEL_ID, name: this.list?.publicSale.name ?? PUBLIC_CHANNEL_NAME },
|
|
@@ -7188,9 +7657,9 @@ var ChannelsMode = class {
|
|
|
7188
7657
|
const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
|
|
7189
7658
|
<span><b>This private sale is not available.</b> ${esc((this.previewProjection.unavailable ?? []).map((entry) => `${this.nameOf(entry.channelId) ?? "This channel"} is ${entry.state}`).join("; ") || "The audience cannot buy right now")}.
|
|
7190
7659
|
A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
|
|
7191
|
-
const
|
|
7192
|
-
const summary =
|
|
7193
|
-
|
|
7660
|
+
const eligibleSeats = this.previewProjection?.counts?.eligible ?? this.previewProjection?.eligible?.length;
|
|
7661
|
+
const summary = eligibleSeats != null && this.previewProjection?.available !== false ? `<div class="slm-ch-alert info"><span>\u2713</span><span><b>${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat is" : "seats are"} available now.</b>
|
|
7662
|
+
This is the exact buyer-visible allocation.${this.previewProjection?.includePublic === false ? " Public sale seats are <b>not</b> included in this access." : ""}</span></div>` : "";
|
|
7194
7663
|
const includePublic = isPublicChannelId(current) ? "" : `
|
|
7195
7664
|
<label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:10px 0">
|
|
7196
7665
|
<input type="checkbox" data-ch-includepublic ${this.previewIncludePublic ? "checked" : ""} />
|
|
@@ -7221,12 +7690,35 @@ var ChannelsMode = class {
|
|
|
7221
7690
|
rail.querySelectorAll("[data-ch-view]").forEach((button) => {
|
|
7222
7691
|
button.addEventListener("click", () => this.setView(button.dataset.chView));
|
|
7223
7692
|
});
|
|
7693
|
+
rail.querySelectorAll("[data-ch-map]").forEach((button) => {
|
|
7694
|
+
button.addEventListener("click", () => {
|
|
7695
|
+
this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
|
|
7696
|
+
this.paintRail();
|
|
7697
|
+
this.onInteractionChange?.();
|
|
7698
|
+
});
|
|
7699
|
+
});
|
|
7224
7700
|
rail.querySelectorAll("[data-ch-detail]").forEach((button) => {
|
|
7225
7701
|
button.addEventListener("click", () => {
|
|
7226
|
-
|
|
7702
|
+
const channelId = button.dataset.chDetail;
|
|
7703
|
+
this.detailChannelId = channelId;
|
|
7227
7704
|
this.paintRail();
|
|
7705
|
+
void this.loadLinks(channelId).then(() => this.paintRail());
|
|
7228
7706
|
});
|
|
7229
7707
|
});
|
|
7708
|
+
rail.querySelectorAll("[data-ch-rotate]").forEach((button) => {
|
|
7709
|
+
button.addEventListener("click", () => this.openDialog({
|
|
7710
|
+
kind: "linkRotate",
|
|
7711
|
+
channelId: this.detailChannelId,
|
|
7712
|
+
linkId: button.dataset.chRotate
|
|
7713
|
+
}));
|
|
7714
|
+
});
|
|
7715
|
+
rail.querySelectorAll("[data-ch-revoke]").forEach((button) => {
|
|
7716
|
+
button.addEventListener("click", () => this.openDialog({
|
|
7717
|
+
kind: "linkRevoke",
|
|
7718
|
+
channelId: this.detailChannelId,
|
|
7719
|
+
linkId: button.dataset.chRevoke
|
|
7720
|
+
}));
|
|
7721
|
+
});
|
|
7230
7722
|
const target = rail.querySelector("[data-ch-target]");
|
|
7231
7723
|
target?.addEventListener("change", () => {
|
|
7232
7724
|
this.targetChannelId = target.value;
|
|
@@ -7255,6 +7747,11 @@ var ChannelsMode = class {
|
|
|
7255
7747
|
}
|
|
7256
7748
|
railAction(action) {
|
|
7257
7749
|
switch (action) {
|
|
7750
|
+
case "sections":
|
|
7751
|
+
this.focusedSectionId = null;
|
|
7752
|
+
this.host.showSectionOverview();
|
|
7753
|
+
this.paintRail();
|
|
7754
|
+
break;
|
|
7258
7755
|
case "create":
|
|
7259
7756
|
this.openDialog({ kind: "create" });
|
|
7260
7757
|
break;
|
|
@@ -7278,8 +7775,17 @@ var ChannelsMode = class {
|
|
|
7278
7775
|
break;
|
|
7279
7776
|
case "back":
|
|
7280
7777
|
this.detailChannelId = null;
|
|
7778
|
+
this.linksChannelId = null;
|
|
7779
|
+
this.links = [];
|
|
7780
|
+
this.linksState = "idle";
|
|
7281
7781
|
this.paintRail();
|
|
7282
7782
|
break;
|
|
7783
|
+
case "link-create":
|
|
7784
|
+
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
7785
|
+
break;
|
|
7786
|
+
case "link-reload":
|
|
7787
|
+
if (this.detailChannelId) void this.reloadLinks();
|
|
7788
|
+
break;
|
|
7283
7789
|
case "retry":
|
|
7284
7790
|
void this.refresh();
|
|
7285
7791
|
break;
|
|
@@ -7351,6 +7857,9 @@ var ChannelsMode = class {
|
|
|
7351
7857
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
7352
7858
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
7353
7859
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
7860
|
+
else if (state.kind === "linkCreate") this.renderLinkCreateDialog(state);
|
|
7861
|
+
else if (state.kind === "linkRotate") this.renderLinkRotateDialog(state);
|
|
7862
|
+
else if (state.kind === "linkRevoke") this.renderLinkRevokeDialog(state);
|
|
7354
7863
|
}
|
|
7355
7864
|
/**
|
|
7356
7865
|
* Mount a modal: `aria-modal` dialog, programmatic name, focus moved inside,
|
|
@@ -7417,7 +7926,7 @@ var ChannelsMode = class {
|
|
|
7417
7926
|
<label>Marker</label>
|
|
7418
7927
|
<div style="display:flex;gap:8px;align-items:center">
|
|
7419
7928
|
<span class="slm-ch-mk" data-ch-marker style="background:${esc(suggestion.color)};width:28px;height:28px;font-size:13px">${esc(suggestion.letter)}</span>
|
|
7420
|
-
<span class="slm-note" style="margin:0">Letter
|
|
7929
|
+
<span class="slm-note" style="margin:0">Letter comes from the name; colour is chosen automatically from the next available palette. Buyers never see either.</span>
|
|
7421
7930
|
</div>
|
|
7422
7931
|
</div>
|
|
7423
7932
|
<div class="slm-field">
|
|
@@ -7760,6 +8269,298 @@ var ChannelsMode = class {
|
|
|
7760
8269
|
});
|
|
7761
8270
|
});
|
|
7762
8271
|
}
|
|
8272
|
+
// ---- hosted-link dialogs --------------------------------------------------
|
|
8273
|
+
async reloadLinks() {
|
|
8274
|
+
const channelId = this.detailChannelId;
|
|
8275
|
+
if (!channelId) return;
|
|
8276
|
+
this.linksState = this.links.length ? this.linksState : "loading";
|
|
8277
|
+
await this.loadLinks(channelId);
|
|
8278
|
+
this.paintRail();
|
|
8279
|
+
}
|
|
8280
|
+
/**
|
|
8281
|
+
* The reload EVERY link mutation owes the panel.
|
|
8282
|
+
*
|
|
8283
|
+
* A create/rotate/revoke changes two things the detail panel renders: the
|
|
8284
|
+
* channel's access line (the server sets `access.intent` on create, and clears
|
|
8285
|
+
* it when the last live link goes) and the link status list. Both are re-read
|
|
8286
|
+
* here and the rail repainted, so the panel the organizer is already looking
|
|
8287
|
+
* at is current the moment the mutation lands — no reload, and no dependence
|
|
8288
|
+
* on HOW the one-time reveal was dismissed (the button, Escape, or never).
|
|
8289
|
+
*/
|
|
8290
|
+
async reloadAfterLinkChange(channelId) {
|
|
8291
|
+
if (this.detailChannelId === channelId) {
|
|
8292
|
+
await this.refresh({ quiet: true });
|
|
8293
|
+
return;
|
|
8294
|
+
}
|
|
8295
|
+
await this.loadLinks(channelId);
|
|
8296
|
+
if (this.active) this.paintRail();
|
|
8297
|
+
}
|
|
8298
|
+
linkById(linkId) {
|
|
8299
|
+
return this.links.find((link) => link.id === linkId) ?? null;
|
|
8300
|
+
}
|
|
8301
|
+
/**
|
|
8302
|
+
* Create. The three policy fields carry the owner's defaults and every one of
|
|
8303
|
+
* them is editable; the PLATFORM bounds (60s–180d, 1–10 000, 1–100, 20 live
|
|
8304
|
+
* links) are the server's to enforce and the server's to explain, so this form
|
|
8305
|
+
* checks only that a number is a number and surfaces the server's sentence for
|
|
8306
|
+
* everything else.
|
|
8307
|
+
*/
|
|
8308
|
+
renderLinkCreateDialog(state) {
|
|
8309
|
+
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
8310
|
+
if (!channel || !this.caps.manage) {
|
|
8311
|
+
this.closeDialog();
|
|
8312
|
+
return;
|
|
8313
|
+
}
|
|
8314
|
+
this.renderScrim(`
|
|
8315
|
+
<h3 id="slm-ch-dlg-title">Create a hosted access link for ${esc(channel.name)}</h3>
|
|
8316
|
+
<p class="sub">Anyone who opens the link can buy from this channel's allocation \u2014 and only from it.
|
|
8317
|
+
You'll see the link once, right after you create it.</p>
|
|
8318
|
+
<div class="slm-field">
|
|
8319
|
+
<label for="slm-ch-lk-label">Label <span style="text-transform:none;font-weight:500">(optional)</span></label>
|
|
8320
|
+
<input class="slm-input" id="slm-ch-lk-label" maxlength="80" placeholder="e.g. VIP list Nov 14" />
|
|
8321
|
+
<p class="slm-note">So you can tell your links apart later. Buyers never see it.</p>
|
|
8322
|
+
</div>
|
|
8323
|
+
<div class="slm-field">
|
|
8324
|
+
<label for="slm-ch-lk-expiry">Stops working</label>
|
|
8325
|
+
<select class="slm-select" id="slm-ch-lk-expiry" data-ch-lk-expiry>
|
|
8326
|
+
<option value="event" selected>When the event starts</option>
|
|
8327
|
+
<option value="custom">On a date I choose</option>
|
|
8328
|
+
</select>
|
|
8329
|
+
</div>
|
|
8330
|
+
<div class="slm-field" data-ch-lk-when-field hidden>
|
|
8331
|
+
<label for="slm-ch-lk-when">Date and time</label>
|
|
8332
|
+
<input class="slm-input" type="datetime-local" id="slm-ch-lk-when" />
|
|
8333
|
+
</div>
|
|
8334
|
+
<div class="slm-field">
|
|
8335
|
+
<label for="slm-ch-lk-redemptions">How many people can use it</label>
|
|
8336
|
+
<input class="slm-input" type="number" id="slm-ch-lk-redemptions" inputmode="numeric"
|
|
8337
|
+
value="${ACCESS_LINK_DEFAULTS.maxRedemptions}" />
|
|
8338
|
+
<p class="slm-note">Each buyer who opens the link uses one.</p>
|
|
8339
|
+
</div>
|
|
8340
|
+
<div class="slm-field">
|
|
8341
|
+
<label for="slm-ch-lk-quantity">Seats per buyer</label>
|
|
8342
|
+
<input class="slm-input" type="number" id="slm-ch-lk-quantity" inputmode="numeric"
|
|
8343
|
+
value="${ACCESS_LINK_DEFAULTS.maxQuantity}" />
|
|
8344
|
+
</div>
|
|
8345
|
+
<label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:2px 0 6px">
|
|
8346
|
+
<input type="checkbox" id="slm-ch-lk-public" />
|
|
8347
|
+
Also let this link buy Public sale seats
|
|
8348
|
+
</label>
|
|
8349
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8350
|
+
<div class="foot">
|
|
8351
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8352
|
+
<button type="button" class="slm-btn" data-ch-lk-create>Create link</button>
|
|
8353
|
+
</div>`, (dialog) => {
|
|
8354
|
+
const expiry = dialog.querySelector("[data-ch-lk-expiry]");
|
|
8355
|
+
const whenField = dialog.querySelector("[data-ch-lk-when-field]");
|
|
8356
|
+
const when = dialog.querySelector("#slm-ch-lk-when");
|
|
8357
|
+
expiry.addEventListener("change", () => {
|
|
8358
|
+
const custom = expiry.value === "custom";
|
|
8359
|
+
whenField.hidden = !custom;
|
|
8360
|
+
if (custom && !when.value) when.value = datetimeLocalValue(Date.now() + 7 * 864e5);
|
|
8361
|
+
});
|
|
8362
|
+
dialog.querySelector("[data-ch-lk-create]")?.addEventListener("click", () => {
|
|
8363
|
+
const maxRedemptions = intField(dialog, "#slm-ch-lk-redemptions");
|
|
8364
|
+
const maxQuantity = intField(dialog, "#slm-ch-lk-quantity");
|
|
8365
|
+
if (maxRedemptions == null || maxQuantity == null) {
|
|
8366
|
+
this.showDialogError("Those two settings need to be whole numbers.");
|
|
8367
|
+
return;
|
|
8368
|
+
}
|
|
8369
|
+
let expiresAt;
|
|
8370
|
+
if (expiry.value === "custom") {
|
|
8371
|
+
expiresAt = Date.parse(when.value);
|
|
8372
|
+
if (!Number.isFinite(expiresAt)) {
|
|
8373
|
+
this.showDialogError("Pick the date and time the link should stop working.");
|
|
8374
|
+
return;
|
|
8375
|
+
}
|
|
8376
|
+
}
|
|
8377
|
+
void this.createLink(channel.id, {
|
|
8378
|
+
label: dialog.querySelector("#slm-ch-lk-label")?.value.trim() || null,
|
|
8379
|
+
includePublic: dialog.querySelector("#slm-ch-lk-public")?.checked ?? false,
|
|
8380
|
+
...expiresAt === void 0 ? {} : { expiresAt },
|
|
8381
|
+
maxRedemptions,
|
|
8382
|
+
maxQuantity
|
|
8383
|
+
});
|
|
8384
|
+
});
|
|
8385
|
+
});
|
|
8386
|
+
}
|
|
8387
|
+
async createLink(channelId, input) {
|
|
8388
|
+
try {
|
|
8389
|
+
const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
|
|
8390
|
+
this.revealLink(reveal, { channelId });
|
|
8391
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8392
|
+
} catch (err) {
|
|
8393
|
+
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8394
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
8395
|
+
}
|
|
8396
|
+
}
|
|
8397
|
+
/**
|
|
8398
|
+
* The ONE-TIME reveal.
|
|
8399
|
+
*
|
|
8400
|
+
* Three things make this unrecoverable rather than merely "not shown twice":
|
|
8401
|
+
*
|
|
8402
|
+
* 1. `url` is a local const. It is never assigned to a field on this class,
|
|
8403
|
+
* never handed to the host, never put in a `DialogState`.
|
|
8404
|
+
* 2. `this.dialog` is cleared FIRST, so `renderDialog()` — the only function
|
|
8405
|
+
* that rebuilds a sheet — has nothing to rebuild this one from.
|
|
8406
|
+
* 3. The string exists in exactly one DOM node inside the scrim. Dismissing
|
|
8407
|
+
* the dialog removes the scrim, and the closure goes with it.
|
|
8408
|
+
*
|
|
8409
|
+
* The server holds only a hash, so even a compromised client cannot ask for it
|
|
8410
|
+
* again. Rotation is the recovery path, and the copy says so.
|
|
8411
|
+
*/
|
|
8412
|
+
revealLink(reveal, opts) {
|
|
8413
|
+
const url = reveal.url;
|
|
8414
|
+
this.dialog = null;
|
|
8415
|
+
const rotated = opts.rotated ? `<div class="slm-ch-alert warn" role="status"><span>\u26A0</span><span>
|
|
8416
|
+
<b>The old link has stopped working.</b> ${reveal.endedSessions ? `${reveal.endedSessions.toLocaleString()} buyer${reveal.endedSessions === 1 ? "" : "s"} lost access immediately.` : "Buyers who already came in can finish; every new visit needs this link."}</span></div>` : "";
|
|
8417
|
+
const policy = accessLinkPolicyLines(reveal.link).map((row) => `<div class="slm-ch-lkrow"><span class="k">${esc(row.k)}</span>
|
|
8418
|
+
<span class="v">${esc(row.v)}</span></div>`).join("");
|
|
8419
|
+
this.renderScrim(`
|
|
8420
|
+
<h3 id="slm-ch-dlg-title">Copy this link now</h3>
|
|
8421
|
+
<p class="sub">This is the only time SeatLayer can show it. We keep just a fingerprint, so it cannot be
|
|
8422
|
+
shown again \u2014 if it is lost, rotate the link for a fresh one.</p>
|
|
8423
|
+
${rotated}
|
|
8424
|
+
<div class="slm-ch-secret" data-ch-lk-url>${esc(url)}</div>
|
|
8425
|
+
<div class="slm-ch-row2" style="margin-top:8px">
|
|
8426
|
+
<button type="button" class="slm-btn" data-ch-lk-copy>Copy link</button>
|
|
8427
|
+
</div>
|
|
8428
|
+
<div class="slm-ch-alert warn" style="margin-top:12px"><span>\u26A0</span>
|
|
8429
|
+
<span>Anyone who opens this link can buy from this allocation. Send it only to the people it is meant
|
|
8430
|
+
for \u2014 forwarding it hands on the same access, and SeatLayer cannot tell the difference.</span></div>
|
|
8431
|
+
<p class="slm-eyebrow" style="margin-top:14px">What this link allows</p>
|
|
8432
|
+
${policy}
|
|
8433
|
+
<div class="foot">
|
|
8434
|
+
<button type="button" class="slm-btn" data-ch-close data-ch-lk-done>I've copied it</button>
|
|
8435
|
+
</div>`, (dialog) => {
|
|
8436
|
+
const copy = dialog.querySelector("[data-ch-lk-copy]");
|
|
8437
|
+
copy?.addEventListener("click", () => {
|
|
8438
|
+
const ok = () => {
|
|
8439
|
+
copy.textContent = "Copied";
|
|
8440
|
+
this.announce("Hosted access link copied.");
|
|
8441
|
+
};
|
|
8442
|
+
const clipboard = typeof navigator === "undefined" ? null : navigator.clipboard;
|
|
8443
|
+
if (clipboard?.writeText) {
|
|
8444
|
+
clipboard.writeText(url).then(ok, () => selectSecret(dialog));
|
|
8445
|
+
return;
|
|
8446
|
+
}
|
|
8447
|
+
selectSecret(dialog);
|
|
8448
|
+
});
|
|
8449
|
+
});
|
|
8450
|
+
this.announce("Your hosted access link is ready and is shown once.");
|
|
8451
|
+
}
|
|
8452
|
+
/**
|
|
8453
|
+
* Rotate. The organizer must SAY what happens to the buyers already inside —
|
|
8454
|
+
* the confirm stays disabled until one of the two choices is picked, because
|
|
8455
|
+
* the gentle branch and the destructive branch are both real decisions and the
|
|
8456
|
+
* server refuses (422 `end_active_sessions_required`) to guess either.
|
|
8457
|
+
*/
|
|
8458
|
+
renderLinkRotateDialog(state) {
|
|
8459
|
+
const link = this.linkById(state.linkId);
|
|
8460
|
+
if (!link || !this.caps.manage) {
|
|
8461
|
+
this.closeDialog();
|
|
8462
|
+
return;
|
|
8463
|
+
}
|
|
8464
|
+
const sessions = link.activeSessions ?? 0;
|
|
8465
|
+
const warning = sessions ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
8466
|
+
<span><b>${sessions.toLocaleString()} buyer${sessions === 1 ? "" : "s"}</b> got in with the current link
|
|
8467
|
+
and still ${sessions === 1 ? "has" : "have"} active access.</span></div>` : "";
|
|
8468
|
+
this.renderScrim(`
|
|
8469
|
+
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "hosted")} link?</h3>
|
|
8470
|
+
<p class="sub">The current link stops opening immediately and cannot be restored. You will get a new
|
|
8471
|
+
link to copy \u2014 shown once.</p>
|
|
8472
|
+
${warning}
|
|
8473
|
+
<label class="slm-ch-radio">
|
|
8474
|
+
<input type="radio" name="slm-ch-rot" value="keep" data-ch-rot />
|
|
8475
|
+
<span><b>Let them finish</b><span class="why">Access already handed out expires on its own; seats in
|
|
8476
|
+
checkout are untouched. Every new visit needs the new link.</span></span>
|
|
8477
|
+
</label>
|
|
8478
|
+
<label class="slm-ch-radio">
|
|
8479
|
+
<input type="radio" name="slm-ch-rot" value="end" data-ch-rot />
|
|
8480
|
+
<span><b>End their access now</b><span class="why">All access from the old link ends immediately.
|
|
8481
|
+
Buyers part-way through choosing seats lose access.</span></span>
|
|
8482
|
+
</label>
|
|
8483
|
+
<p class="slm-note">Choose one \u2014 SeatLayer will not decide this for you.</p>
|
|
8484
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8485
|
+
<div class="foot">
|
|
8486
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8487
|
+
<button type="button" class="slm-btn" data-ch-lk-rotate disabled>Rotate and copy new link</button>
|
|
8488
|
+
</div>`, (dialog) => {
|
|
8489
|
+
const confirm = dialog.querySelector("[data-ch-lk-rotate]");
|
|
8490
|
+
dialog.querySelectorAll("[data-ch-rot]").forEach((radio) => {
|
|
8491
|
+
radio.addEventListener("change", () => {
|
|
8492
|
+
confirm.disabled = false;
|
|
8493
|
+
});
|
|
8494
|
+
});
|
|
8495
|
+
confirm.addEventListener("click", () => {
|
|
8496
|
+
const picked = [...dialog.querySelectorAll("[data-ch-rot]")].find((radio) => radio.checked);
|
|
8497
|
+
if (!picked) {
|
|
8498
|
+
this.showDialogError(accessLinkErrorCopy({ code: "end_active_sessions_required" }));
|
|
8499
|
+
return;
|
|
8500
|
+
}
|
|
8501
|
+
void this.rotateLink(state.channelId, link.id, picked.value === "end");
|
|
8502
|
+
});
|
|
8503
|
+
});
|
|
8504
|
+
}
|
|
8505
|
+
async rotateLink(channelId, linkId, endActiveSessions) {
|
|
8506
|
+
try {
|
|
8507
|
+
const reveal = await this.host.api.rotateAccessLink(
|
|
8508
|
+
this.host.eventKey,
|
|
8509
|
+
channelId,
|
|
8510
|
+
linkId,
|
|
8511
|
+
endActiveSessions
|
|
8512
|
+
);
|
|
8513
|
+
this.revealLink(reveal, { channelId, rotated: true });
|
|
8514
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8515
|
+
} catch (err) {
|
|
8516
|
+
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8517
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
8518
|
+
}
|
|
8519
|
+
}
|
|
8520
|
+
renderLinkRevokeDialog(state) {
|
|
8521
|
+
const link = this.linkById(state.linkId);
|
|
8522
|
+
if (!link || !this.caps.manage) {
|
|
8523
|
+
this.closeDialog();
|
|
8524
|
+
return;
|
|
8525
|
+
}
|
|
8526
|
+
const sessions = link.activeSessions ?? 0;
|
|
8527
|
+
this.renderScrim(`
|
|
8528
|
+
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "hosted")} link?</h3>
|
|
8529
|
+
<p class="sub">It stops opening immediately and cannot be restored \u2014 there is no undo, and no way to
|
|
8530
|
+
bring the same URL back. Seats already bought through it keep their sale.</p>
|
|
8531
|
+
${sessions ? `<label class="slm-ch-radio">
|
|
8532
|
+
<input type="checkbox" data-ch-lk-endsessions />
|
|
8533
|
+
<span><b>Also end access for the ${sessions.toLocaleString()}
|
|
8534
|
+
buyer${sessions === 1 ? "" : "s"} already inside</b><span class="why">Leave this off and they can
|
|
8535
|
+
finish what they started; new visits are refused either way.</span></span>
|
|
8536
|
+
</label>` : ""}
|
|
8537
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8538
|
+
<div class="foot">
|
|
8539
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8540
|
+
<button type="button" class="slm-btn danger" data-ch-lk-revoke>Revoke link</button>
|
|
8541
|
+
</div>`, (dialog) => {
|
|
8542
|
+
dialog.querySelector("[data-ch-lk-revoke]")?.addEventListener("click", () => {
|
|
8543
|
+
const end = dialog.querySelector("[data-ch-lk-endsessions]")?.checked ?? false;
|
|
8544
|
+
void this.revokeLink(state.channelId, link.id, end);
|
|
8545
|
+
});
|
|
8546
|
+
});
|
|
8547
|
+
}
|
|
8548
|
+
async revokeLink(channelId, linkId, endActiveSessions) {
|
|
8549
|
+
try {
|
|
8550
|
+
const res = await this.host.api.revokeAccessLink(
|
|
8551
|
+
this.host.eventKey,
|
|
8552
|
+
channelId,
|
|
8553
|
+
linkId,
|
|
8554
|
+
endActiveSessions
|
|
8555
|
+
);
|
|
8556
|
+
this.closeDialog();
|
|
8557
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8558
|
+
this.host.toast(res.endedSessions ? `Link revoked. ${res.endedSessions.toLocaleString()} buyer${res.endedSessions === 1 ? "" : "s"} lost access.` : "Link revoked. It no longer opens for anyone.", "ok");
|
|
8559
|
+
} catch (err) {
|
|
8560
|
+
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8561
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
7763
8564
|
// ---- compact detents ------------------------------------------------------
|
|
7764
8565
|
applySheetClasses() {
|
|
7765
8566
|
const root = this.host.root;
|
|
@@ -7838,9 +8639,17 @@ var LEGEND = [
|
|
|
7838
8639
|
{ key: "booked", label: "Booked", color: "#22a06b" },
|
|
7839
8640
|
{ key: "blocked", label: "Blocked", color: "#8b94ac" }
|
|
7840
8641
|
];
|
|
7841
|
-
var
|
|
8642
|
+
var MANAGER_CSS = `
|
|
7842
8643
|
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
|
|
7843
|
-
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius)
|
|
8644
|
+
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
8645
|
+
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
8646
|
+
borrowed from CHANNELS_CSS. The base cockpit animates whether or not
|
|
8647
|
+
Channels mode is in use, so owning its own tokens is what stops a token
|
|
8648
|
+
edit from silently changing only half the surface. Channels mode declares
|
|
8649
|
+
the identical values so an embed of it stays self-contained. */
|
|
8650
|
+
--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
8651
|
+
--slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
|
|
8652
|
+
--slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
|
|
7844
8653
|
.slm *{box-sizing:border-box;margin:0;padding:0}
|
|
7845
8654
|
.slm button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
|
7846
8655
|
.slm input{font:inherit}
|
|
@@ -7853,7 +8662,8 @@ var CSS2 = `
|
|
|
7853
8662
|
.slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7854
8663
|
.slm-live{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.12em;font-weight:800;color:var(--slm-muted)}
|
|
7855
8664
|
.slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
|
|
7856
|
-
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
8665
|
+
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
8666
|
+
animation:slm-pulse var(--slm-mo-ambient) infinite}
|
|
7857
8667
|
@keyframes slm-pulse{0%{box-shadow:0 0 0 0 rgba(34,160,107,.5)}70%{box-shadow:0 0 0 7px rgba(34,160,107,0)}100%{box-shadow:0 0 0 0 rgba(34,160,107,0)}}
|
|
7858
8668
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
7859
8669
|
border-top:1px solid var(--slm-line)}
|
|
@@ -7862,7 +8672,11 @@ var CSS2 = `
|
|
|
7862
8672
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
7863
8673
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
7864
8674
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
7865
|
-
|
|
8675
|
+
/* The one playful moment this surface is allowed (\xA72 --mo-spring). It ran at
|
|
8676
|
+
.58s against a 200ms catalog, which read as a different design language from
|
|
8677
|
+
the dashboard tile it mirrors; Channels mode's own count bump is the same
|
|
8678
|
+
pattern and must stay in step with it. */
|
|
8679
|
+
.slm-kpi.changed b{animation:slm-kpi-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
7866
8680
|
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
7867
8681
|
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
7868
8682
|
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
@@ -7881,12 +8695,14 @@ var CSS2 = `
|
|
|
7881
8695
|
.slm-hud-chip{padding:6px 11px;border-radius:999px;font-size:12px;font-weight:700;background:var(--slm-surface);
|
|
7882
8696
|
border:1px solid var(--slm-line);color:var(--slm-text)}
|
|
7883
8697
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
7884
|
-
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
8698
|
+
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
8699
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
7885
8700
|
.slm-zoomhint.on{opacity:1}
|
|
7886
8701
|
.slm-liveevent{position:absolute;left:50%;top:14px;z-index:4;display:flex;align-items:center;gap:8px;max-width:min(560px,calc(100% - 32px));
|
|
7887
8702
|
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
7888
8703
|
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
7889
|
-
transition:opacity
|
|
8704
|
+
transition:opacity var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-base) var(--slm-mo-out);
|
|
8705
|
+
backdrop-filter:blur(10px)}
|
|
7890
8706
|
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
7891
8707
|
.slm.block-mode .slm-liveevent{top:52px}
|
|
7892
8708
|
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
@@ -7906,7 +8722,8 @@ var CSS2 = `
|
|
|
7906
8722
|
/* activity feed */
|
|
7907
8723
|
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
7908
8724
|
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
7909
|
-
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
8725
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
8726
|
+
animation:slm-in var(--slm-mo-quick) var(--slm-mo-out)}
|
|
7910
8727
|
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
7911
8728
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
7912
8729
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
@@ -7973,7 +8790,8 @@ var CSS2 = `
|
|
|
7973
8790
|
|
|
7974
8791
|
/* toast */
|
|
7975
8792
|
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
7976
|
-
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
8793
|
+
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
8794
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out);
|
|
7977
8795
|
background:var(--slm-surface);color:var(--slm-text);border:1px solid var(--slm-line);z-index:5}
|
|
7978
8796
|
.slm-toast.on{opacity:1}
|
|
7979
8797
|
.slm-toast.err{background:#c0392b;color:#fff;border-color:#c0392b}
|
|
@@ -7984,7 +8802,9 @@ var CSS2 = `
|
|
|
7984
8802
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
7985
8803
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
7986
8804
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
7987
|
-
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
8805
|
+
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
8806
|
+
background:var(--slm-surface)!important;text-align:left!important;
|
|
8807
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-quick) var(--slm-mo-out)}
|
|
7988
8808
|
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
7989
8809
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
7990
8810
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
@@ -8003,7 +8823,8 @@ var CSS2 = `
|
|
|
8003
8823
|
.slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
|
|
8004
8824
|
/* sections: availability windows */
|
|
8005
8825
|
.slm-availlist{display:flex;flex-direction:column;gap:8px;margin:2px 0 12px}
|
|
8006
|
-
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
8826
|
+
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
8827
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),opacity var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8007
8828
|
.slm-availrow.zone{background:color-mix(in srgb,var(--slm-surface) 82%,#000)}
|
|
8008
8829
|
.slm-availrow.hidden{opacity:.62}.slm-availrow.closed{opacity:.82}
|
|
8009
8830
|
.slm-availhead{display:flex;align-items:center;gap:8px}
|
|
@@ -8043,16 +8864,29 @@ var CSS2 = `
|
|
|
8043
8864
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
8044
8865
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
8045
8866
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
8867
|
+
/* Reduced motion, as a BLANKET over the cockpit subtree rather than a list of
|
|
8868
|
+
selectors. The list this replaces named four animations and two transitions,
|
|
8869
|
+
and had silently fallen behind the stylesheet: the zoom hint, the toast and
|
|
8870
|
+
the availability rows all still animated for a user who had asked the OS for
|
|
8871
|
+
none. An enumerated list has to be edited every time a rule is added, and
|
|
8872
|
+
nothing fails when it isn't \u2014 so it drifts. This cannot.
|
|
8873
|
+
|
|
8874
|
+
Motion is removed, never the information it carried: Channels mode's own
|
|
8875
|
+
block substitutes static outlines for its shake and success states, and it
|
|
8876
|
+
stays authoritative for those. No JS here waits on animationend or
|
|
8877
|
+
transitionend, so cutting them outright strands no state. */
|
|
8046
8878
|
@media (prefers-reduced-motion:reduce){
|
|
8047
|
-
.slm
|
|
8048
|
-
|
|
8879
|
+
.slm,.slm *,.slm *::before,.slm *::after{
|
|
8880
|
+
animation:none!important;
|
|
8881
|
+
transition:none!important;
|
|
8882
|
+
scroll-behavior:auto!important}
|
|
8049
8883
|
}
|
|
8050
8884
|
${CHANNELS_CSS}`;
|
|
8051
8885
|
function injectStyle() {
|
|
8052
8886
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
8053
8887
|
const el = document.createElement("style");
|
|
8054
8888
|
el.id = STYLE_ID2;
|
|
8055
|
-
el.textContent =
|
|
8889
|
+
el.textContent = MANAGER_CSS;
|
|
8056
8890
|
document.head.appendChild(el);
|
|
8057
8891
|
}
|
|
8058
8892
|
function themeVars(theme) {
|
|
@@ -8112,6 +8946,11 @@ var SeatManager = class {
|
|
|
8112
8946
|
this.reconnectTimer = null;
|
|
8113
8947
|
this.attempt = 0;
|
|
8114
8948
|
this.closed = false;
|
|
8949
|
+
/** Mirrors the `live` root class, so the getter never has to read the DOM. */
|
|
8950
|
+
this.connectionStatus = "reconnecting";
|
|
8951
|
+
/** When the server last told us something. Stamped on accepted traffic only —
|
|
8952
|
+
* a socket that opens and says nothing has not refreshed anything. */
|
|
8953
|
+
this.lastMessageAt = null;
|
|
8115
8954
|
this.ready = false;
|
|
8116
8955
|
this.feed = [];
|
|
8117
8956
|
this.feedTimer = null;
|
|
@@ -8321,6 +9160,12 @@ var SeatManager = class {
|
|
|
8321
9160
|
},
|
|
8322
9161
|
worldToScreen: (point) => this.renderer?.worldToScreen(point) ?? null,
|
|
8323
9162
|
seatPixelSize: () => this.seatPixelSize(),
|
|
9163
|
+
isSeatDetail: () => this.renderer?.getRung?.() === "seats",
|
|
9164
|
+
showSectionOverview: () => {
|
|
9165
|
+
this.renderer?.clearSectionFocus();
|
|
9166
|
+
this.renderer?.setRung?.("sections");
|
|
9167
|
+
},
|
|
9168
|
+
focusSection: (sectionId) => this.renderer?.focusSection(sectionId),
|
|
8324
9169
|
isCompact: () => !!this.root?.classList.contains("compact"),
|
|
8325
9170
|
setMapInert: (inert) => {
|
|
8326
9171
|
this.mapHost.toggleAttribute("inert", inert);
|
|
@@ -8330,13 +9175,15 @@ var SeatManager = class {
|
|
|
8330
9175
|
onError: (err) => this.opts.onError?.(err)
|
|
8331
9176
|
};
|
|
8332
9177
|
}
|
|
8333
|
-
/**
|
|
8334
|
-
*
|
|
9178
|
+
/** Actual on-screen seat diameter, for the channel overlay's marks. The
|
|
9179
|
+
* renderer's base seat radius is 9 chart units; retaining the camera scale
|
|
9180
|
+
* (rather than capping it) keeps every preview paint aligned with the real
|
|
9181
|
+
* chart geometry at deep zoom. */
|
|
8335
9182
|
seatPixelSize() {
|
|
8336
9183
|
const rect = this.renderer?.getVisibleWorldRect?.();
|
|
8337
9184
|
const width = this.mapHost?.clientWidth ?? 0;
|
|
8338
9185
|
if (!rect?.width || !width) return 6;
|
|
8339
|
-
return Math.max(3,
|
|
9186
|
+
return Math.max(3, width / rect.width * 18);
|
|
8340
9187
|
}
|
|
8341
9188
|
/** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
|
|
8342
9189
|
setHeatOverlay(enabled) {
|
|
@@ -8512,6 +9359,16 @@ var SeatManager = class {
|
|
|
8512
9359
|
getControlRoomSnapshot(windowMinutes = this.trendWindowMinutes) {
|
|
8513
9360
|
return this.setTrendWindow(windowMinutes);
|
|
8514
9361
|
}
|
|
9362
|
+
/**
|
|
9363
|
+
* The realtime link's current state and the "as of" behind it.
|
|
9364
|
+
*
|
|
9365
|
+
* Pair with `onConnectionChange` for the edges: a host that mounts after a
|
|
9366
|
+
* drop, or re-reads on tab focus, needs to be able to ASK rather than wait
|
|
9367
|
+
* for the next transition that may never come.
|
|
9368
|
+
*/
|
|
9369
|
+
getConnection() {
|
|
9370
|
+
return { status: this.connectionStatus, lastMessageAt: this.lastMessageAt };
|
|
9371
|
+
}
|
|
8515
9372
|
getLog(opts = {}) {
|
|
8516
9373
|
return this.api.log(this.key, opts);
|
|
8517
9374
|
}
|
|
@@ -8576,6 +9433,10 @@ var SeatManager = class {
|
|
|
8576
9433
|
onSelect: (seat) => this.handleSeatSelect(seat),
|
|
8577
9434
|
onDeselect: () => this.syncSelection(),
|
|
8578
9435
|
onMarquee: () => this.syncSelection(),
|
|
9436
|
+
onSectionTap: (sectionId) => {
|
|
9437
|
+
this.renderer?.focusSection(sectionId);
|
|
9438
|
+
this.channels?.handleSectionFocus(sectionId);
|
|
9439
|
+
},
|
|
8579
9440
|
onViewChange: () => {
|
|
8580
9441
|
this.updateZoomHint();
|
|
8581
9442
|
this.channels?.handleViewChange();
|
|
@@ -8586,10 +9447,11 @@ var SeatManager = class {
|
|
|
8586
9447
|
this.applyHeatOverlay();
|
|
8587
9448
|
this.updateZoomHint();
|
|
8588
9449
|
}
|
|
8589
|
-
/** Block
|
|
8590
|
-
*
|
|
9450
|
+
/** Block always uses a marquee. Channels only enables its marquee after the
|
|
9451
|
+
* organizer deliberately chooses Assign seats; Pan map keeps desktop drag
|
|
9452
|
+
* available for large charts. */
|
|
8591
9453
|
isBulkSelectMode() {
|
|
8592
|
-
return this.mode === "block" || this.mode === "channels" && this.channels?.
|
|
9454
|
+
return this.mode === "block" || this.mode === "channels" && this.channels?.usesMarqueeSelection() === true;
|
|
8593
9455
|
}
|
|
8594
9456
|
/**
|
|
8595
9457
|
* Block never touches held or booked inventory, so it cannot select it.
|
|
@@ -8599,7 +9461,7 @@ var SeatManager = class {
|
|
|
8599
9461
|
*/
|
|
8600
9462
|
selectableStatuses() {
|
|
8601
9463
|
if (this.mode === "block") return ["free", "not_for_sale"];
|
|
8602
|
-
if (this.mode === "inspect" || this.isBulkSelectMode()) {
|
|
9464
|
+
if (this.mode === "inspect" || this.isBulkSelectMode() || this.mode === "channels" && this.channels?.canSelect() === true) {
|
|
8603
9465
|
return ["free", "held", "booked", "not_for_sale"];
|
|
8604
9466
|
}
|
|
8605
9467
|
return [];
|
|
@@ -8702,6 +9564,7 @@ var SeatManager = class {
|
|
|
8702
9564
|
return;
|
|
8703
9565
|
}
|
|
8704
9566
|
if (!msg || typeof msg !== "object") return;
|
|
9567
|
+
this.lastMessageAt = Date.now();
|
|
8705
9568
|
const m = msg;
|
|
8706
9569
|
if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
|
|
8707
9570
|
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
@@ -8758,6 +9621,7 @@ var SeatManager = class {
|
|
|
8758
9621
|
const objs = await this.api.objects(this.key);
|
|
8759
9622
|
this.applySnapshot(objs.seats);
|
|
8760
9623
|
this.updateEffectiveAvailability(objs.hidden, objs.closed);
|
|
9624
|
+
this.lastMessageAt = Date.now();
|
|
8761
9625
|
} catch {
|
|
8762
9626
|
}
|
|
8763
9627
|
}
|
|
@@ -9232,6 +10096,14 @@ var SeatManager = class {
|
|
|
9232
10096
|
this.root?.classList.toggle("live", on);
|
|
9233
10097
|
if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
|
|
9234
10098
|
this.paintMonitorInsights();
|
|
10099
|
+
const next = on ? "live" : "reconnecting";
|
|
10100
|
+
if (next === this.connectionStatus) return;
|
|
10101
|
+
this.connectionStatus = next;
|
|
10102
|
+
try {
|
|
10103
|
+
this.opts.onConnectionChange?.(this.getConnection());
|
|
10104
|
+
} catch (err) {
|
|
10105
|
+
this.opts.onError?.(err);
|
|
10106
|
+
}
|
|
9235
10107
|
}
|
|
9236
10108
|
updateZoomHint() {
|
|
9237
10109
|
const hint = this.els.zoomhint;
|
|
@@ -9976,6 +10848,7 @@ var SeatManager = class {
|
|
|
9976
10848
|
}
|
|
9977
10849
|
};
|
|
9978
10850
|
export {
|
|
10851
|
+
ACCESS_LINK_DEFAULTS,
|
|
9979
10852
|
ApiError,
|
|
9980
10853
|
BuyerAccessContext,
|
|
9981
10854
|
BuyerAccessUnavailableError,
|
|
@@ -9991,6 +10864,10 @@ export {
|
|
|
9991
10864
|
SeatingChart,
|
|
9992
10865
|
accessIntentLabel,
|
|
9993
10866
|
accessLine,
|
|
10867
|
+
accessLinkBadge,
|
|
10868
|
+
accessLinkErrorCopy,
|
|
10869
|
+
accessLinkIsLive,
|
|
10870
|
+
accessLinkPolicyLines,
|
|
9994
10871
|
attachPickerFrame,
|
|
9995
10872
|
bucketRows,
|
|
9996
10873
|
bucketRowsHtml,
|