@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.cjs
CHANGED
|
@@ -38,6 +38,7 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
|
|
|
38
38
|
// src/index.ts
|
|
39
39
|
var index_exports = {};
|
|
40
40
|
__export(index_exports, {
|
|
41
|
+
ACCESS_LINK_DEFAULTS: () => ACCESS_LINK_DEFAULTS,
|
|
41
42
|
ApiError: () => ApiError,
|
|
42
43
|
BuyerAccessContext: () => BuyerAccessContext,
|
|
43
44
|
BuyerAccessUnavailableError: () => BuyerAccessUnavailableError,
|
|
@@ -53,6 +54,10 @@ __export(index_exports, {
|
|
|
53
54
|
SeatingChart: () => SeatingChart,
|
|
54
55
|
accessIntentLabel: () => accessIntentLabel,
|
|
55
56
|
accessLine: () => accessLine,
|
|
57
|
+
accessLinkBadge: () => accessLinkBadge,
|
|
58
|
+
accessLinkErrorCopy: () => accessLinkErrorCopy,
|
|
59
|
+
accessLinkIsLive: () => accessLinkIsLive,
|
|
60
|
+
accessLinkPolicyLines: () => accessLinkPolicyLines,
|
|
56
61
|
attachPickerFrame: () => attachPickerFrame,
|
|
57
62
|
bucketRows: () => bucketRows,
|
|
58
63
|
bucketRowsHtml: () => bucketRowsHtml,
|
|
@@ -6206,6 +6211,78 @@ function accessLine(access) {
|
|
|
6206
6211
|
function accessIntentLabel(intent) {
|
|
6207
6212
|
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";
|
|
6208
6213
|
}
|
|
6214
|
+
var ACCESS_LINK_DEFAULTS = {
|
|
6215
|
+
maxRedemptions: 100,
|
|
6216
|
+
maxQuantity: 4
|
|
6217
|
+
};
|
|
6218
|
+
function accessLinkBadge(link) {
|
|
6219
|
+
switch (link.status ?? link.state) {
|
|
6220
|
+
case "active":
|
|
6221
|
+
return { text: "Active", kind: "active" };
|
|
6222
|
+
case "expired":
|
|
6223
|
+
return { text: "Expired", kind: "archived" };
|
|
6224
|
+
case "exhausted":
|
|
6225
|
+
return { text: "All used", kind: "paused" };
|
|
6226
|
+
case "rotated":
|
|
6227
|
+
return { text: "Replaced", kind: "archived" };
|
|
6228
|
+
default:
|
|
6229
|
+
return { text: "Revoked", kind: "archived" };
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
function accessLinkIsLive(link) {
|
|
6233
|
+
return link.state === "active" && link.status === "active";
|
|
6234
|
+
}
|
|
6235
|
+
function formatMoment(ms) {
|
|
6236
|
+
if (!Number.isFinite(ms)) return "\u2014";
|
|
6237
|
+
return new Date(ms).toLocaleString(void 0, {
|
|
6238
|
+
day: "numeric",
|
|
6239
|
+
month: "short",
|
|
6240
|
+
year: "numeric",
|
|
6241
|
+
hour: "numeric",
|
|
6242
|
+
minute: "2-digit"
|
|
6243
|
+
});
|
|
6244
|
+
}
|
|
6245
|
+
function accessLinkPolicyLines(link) {
|
|
6246
|
+
return [
|
|
6247
|
+
{ k: "Expires", v: formatMoment(link.expiresAt) },
|
|
6248
|
+
{
|
|
6249
|
+
k: "Redemptions",
|
|
6250
|
+
v: `${link.redemptions.toLocaleString()} of ${link.maxRedemptions.toLocaleString()} used`
|
|
6251
|
+
},
|
|
6252
|
+
{
|
|
6253
|
+
k: "Seats per buyer",
|
|
6254
|
+
v: `${link.maxQuantity.toLocaleString()} seat${link.maxQuantity === 1 ? "" : "s"} maximum`
|
|
6255
|
+
},
|
|
6256
|
+
{
|
|
6257
|
+
k: "Covers",
|
|
6258
|
+
v: link.includePublic ? "This channel's allocation and Public sale seats" : "This channel's allocation only"
|
|
6259
|
+
}
|
|
6260
|
+
];
|
|
6261
|
+
}
|
|
6262
|
+
function accessLinkErrorCopy(err) {
|
|
6263
|
+
const fromServer = err?.serverMessage?.trim();
|
|
6264
|
+
switch (err?.code) {
|
|
6265
|
+
case "invalid_expiry":
|
|
6266
|
+
case "invalid_max_redemptions":
|
|
6267
|
+
case "invalid_max_quantity":
|
|
6268
|
+
case "invalid_session_ttl":
|
|
6269
|
+
case "invalid_label":
|
|
6270
|
+
return fromServer || "That setting is outside what a hosted link allows. Adjust it and try again.";
|
|
6271
|
+
case "too_many_access_links":
|
|
6272
|
+
return fromServer || "This channel already has as many live links as it can hold. Revoke one before creating another.";
|
|
6273
|
+
case "access_link_not_active":
|
|
6274
|
+
return "That link is no longer active, so it cannot be rotated or revoked.";
|
|
6275
|
+
case "channel_unavailable":
|
|
6276
|
+
return "This channel is paused or archived, so it cannot let new buyers in. Resume it first.";
|
|
6277
|
+
case "end_active_sessions_required":
|
|
6278
|
+
return "Choose what happens to the buyers who already came in through this link.";
|
|
6279
|
+
case "not_found":
|
|
6280
|
+
return "That link is no longer here. Refresh and try again.";
|
|
6281
|
+
default:
|
|
6282
|
+
if (err?.status === 403) return "Hosted access links need channel-management permission.";
|
|
6283
|
+
return fromServer || "That did not go through. Try again.";
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6209
6286
|
function dropReviewRows(details) {
|
|
6210
6287
|
return (details?.channels ?? []).map((channel) => ({
|
|
6211
6288
|
kind: "skip",
|
|
@@ -6222,13 +6299,14 @@ function stateBadge(state) {
|
|
|
6222
6299
|
|
|
6223
6300
|
// src/manageApi.ts
|
|
6224
6301
|
var ManageApiError = class extends Error {
|
|
6225
|
-
constructor(status, message, code, conflicts, details) {
|
|
6302
|
+
constructor(status, message, code, conflicts, details, serverMessage) {
|
|
6226
6303
|
super(message);
|
|
6227
6304
|
this.name = "ManageApiError";
|
|
6228
6305
|
this.status = status;
|
|
6229
6306
|
this.code = code;
|
|
6230
6307
|
this.conflicts = conflicts;
|
|
6231
6308
|
this.details = details;
|
|
6309
|
+
this.serverMessage = serverMessage;
|
|
6232
6310
|
}
|
|
6233
6311
|
};
|
|
6234
6312
|
async function parse(res) {
|
|
@@ -6241,7 +6319,8 @@ async function parse(res) {
|
|
|
6241
6319
|
err?.error ?? `request_failed_${res.status}`,
|
|
6242
6320
|
err?.code,
|
|
6243
6321
|
err?.conflicts,
|
|
6244
|
-
err?.details
|
|
6322
|
+
err?.details,
|
|
6323
|
+
typeof err?.message === "string" ? err.message : void 0
|
|
6245
6324
|
);
|
|
6246
6325
|
}
|
|
6247
6326
|
return data;
|
|
@@ -6437,6 +6516,58 @@ var ManageApi = class {
|
|
|
6437
6516
|
body: { accessIntent }
|
|
6438
6517
|
});
|
|
6439
6518
|
}
|
|
6519
|
+
// ---- hosted access links (M8) ----
|
|
6520
|
+
/**
|
|
6521
|
+
* Mint a hosted access link. The 201 is the ONE and ONLY time `url` and
|
|
6522
|
+
* `capability` exist outside the buyer's browser — SeatLayer keeps a hash, so
|
|
6523
|
+
* there is no route, cache, or support escalation that can produce this string
|
|
6524
|
+
* again. Callers must reveal it immediately and then let it go.
|
|
6525
|
+
*
|
|
6526
|
+
* Every omitted field takes the server's default: expiry = when the event
|
|
6527
|
+
* starts, 100 redemptions, 4 seats per buyer, this channel's allocation only.
|
|
6528
|
+
* Platform bounds are enforced server-side and reported as 422 with the rule
|
|
6529
|
+
* spelled out in `ManageApiError.serverMessage`.
|
|
6530
|
+
*
|
|
6531
|
+
* Side effect by design: this also declares the channel's access intent as
|
|
6532
|
+
* `hosted_link`, so the rail stops saying "no buyer access configured".
|
|
6533
|
+
*/
|
|
6534
|
+
createAccessLink(key, channelId, input = {}) {
|
|
6535
|
+
return this.auth(
|
|
6536
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links`,
|
|
6537
|
+
{ method: "POST", body: input }
|
|
6538
|
+
);
|
|
6539
|
+
}
|
|
6540
|
+
/** Status only — label, expiry, redemptions, per-buyer cap, lineage, and the
|
|
6541
|
+
* live session count. Never the url, never the capability. Needs `:view`. */
|
|
6542
|
+
accessLinks(key, channelId) {
|
|
6543
|
+
return this.auth(
|
|
6544
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links`
|
|
6545
|
+
);
|
|
6546
|
+
}
|
|
6547
|
+
/**
|
|
6548
|
+
* Rotate — the ONLY recovery for a link nobody kept. The old URL stops opening
|
|
6549
|
+
* immediately and the response is a fresh one-time reveal.
|
|
6550
|
+
*
|
|
6551
|
+
* `endActiveSessions` is REQUIRED, not defaulted: the organizer must say
|
|
6552
|
+
* whether buyers already inside finish their checkout or lose access now. The
|
|
6553
|
+
* server answers 422 `end_active_sessions_required` if it is omitted, and that
|
|
6554
|
+
* refusal is correct — a UI must not pick either branch on their behalf.
|
|
6555
|
+
*/
|
|
6556
|
+
rotateAccessLink(key, channelId, linkId, endActiveSessions) {
|
|
6557
|
+
return this.auth(
|
|
6558
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links/${encodeURIComponent(linkId)}/rotate`,
|
|
6559
|
+
{ method: "POST", body: { endActiveSessions } }
|
|
6560
|
+
);
|
|
6561
|
+
}
|
|
6562
|
+
/** Revoke. The link stops opening immediately; `endActiveSessions` decides
|
|
6563
|
+
* whether the buyers already inside keep their sessions. */
|
|
6564
|
+
revokeAccessLink(key, channelId, linkId, endActiveSessions = false) {
|
|
6565
|
+
const qs = endActiveSessions ? "?endActiveSessions=1" : "";
|
|
6566
|
+
return this.auth(
|
|
6567
|
+
`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}/access-links/${encodeURIComponent(linkId)}${qs}`,
|
|
6568
|
+
{ method: "DELETE" }
|
|
6569
|
+
);
|
|
6570
|
+
}
|
|
6440
6571
|
// ---- reports (token) ----
|
|
6441
6572
|
report(key) {
|
|
6442
6573
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
|
|
@@ -6467,6 +6598,11 @@ var ManageApi = class {
|
|
|
6467
6598
|
var POLL_MS = 1e4;
|
|
6468
6599
|
var MAX_FLAGS = 8;
|
|
6469
6600
|
var SEAT_LIST_PAGE = 300;
|
|
6601
|
+
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
6602
|
+
var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
6603
|
+
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
6604
|
+
var PREVIEW_UNAVAILABLE_STROKE = "#4b5669";
|
|
6605
|
+
var ALLOCATION_STROKE = "#101723";
|
|
6470
6606
|
var CHANNELS_CSS = `
|
|
6471
6607
|
.slm{--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
6472
6608
|
--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);
|
|
@@ -6480,6 +6616,8 @@ var CHANNELS_CSS = `
|
|
|
6480
6616
|
background:rgba(14,16,23,.88);border:1px solid var(--slm-line);font-size:10px;font-weight:800;letter-spacing:.04em;
|
|
6481
6617
|
transform:translate(-50%,-50%);white-space:nowrap}
|
|
6482
6618
|
.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}
|
|
6619
|
+
.slm-ch-section-target{position:absolute;pointer-events:auto;padding:0;border:0;border-radius:8px;background:transparent;cursor:zoom-in}
|
|
6620
|
+
.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)}
|
|
6483
6621
|
|
|
6484
6622
|
/* preview banner \u2014 raised with the organizer chrome dim, as one transition */
|
|
6485
6623
|
.slm-ch-banner{position:absolute;left:50%;top:14px;z-index:6;display:flex;align-items:center;gap:9px;padding:8px 14px;
|
|
@@ -6499,7 +6637,7 @@ var CHANNELS_CSS = `
|
|
|
6499
6637
|
transition:transform var(--slm-mo-slow) var(--slm-mo-out),opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
6500
6638
|
.slm-ch-staged.on{transform:none;opacity:1}
|
|
6501
6639
|
.slm-ch-staged.done{background:rgba(31,122,77,.96);border-color:#1f7a4d}
|
|
6502
|
-
.slm-ch-staged.shake{animation:slm-ch-shake
|
|
6640
|
+
.slm-ch-staged.shake{animation:slm-ch-shake var(--slm-mo-slow) var(--slm-mo-in-out) 2}
|
|
6503
6641
|
.slm-ch-staged b{font-variant-numeric:tabular-nums}
|
|
6504
6642
|
.slm-ch-staged .grow{flex:1}
|
|
6505
6643
|
.slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
|
|
@@ -6516,6 +6654,11 @@ var CHANNELS_CSS = `
|
|
|
6516
6654
|
.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)}
|
|
6517
6655
|
.slm-ch-viewseg button.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
6518
6656
|
.slm-ch-viewseg button:disabled{opacity:.5;cursor:not-allowed}
|
|
6657
|
+
.slm-ch-mapnav{margin:-2px 0 12px;padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
6658
|
+
.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)}
|
|
6659
|
+
.slm-ch-mapnav-head button{color:var(--slm-accent);font-size:11px;font-weight:800;letter-spacing:0;text-transform:none;min-height:30px}
|
|
6660
|
+
.slm-ch-mapnav .slm-ch-viewseg{margin:8px 0 5px}
|
|
6661
|
+
.slm-ch-mapnav p{margin:0;font-size:11px;line-height:1.45;color:var(--slm-muted)}
|
|
6519
6662
|
.slm-ch-list{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
|
6520
6663
|
.slm-ch-row{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
6521
6664
|
text-align:left;width:100%;display:block;transition:border-color var(--slm-mo-quick) var(--slm-mo-out)}
|
|
@@ -6536,7 +6679,7 @@ var CHANNELS_CSS = `
|
|
|
6536
6679
|
font-variant-numeric:tabular-nums}
|
|
6537
6680
|
.slm-ch-counts b{color:var(--slm-text);font-weight:800}
|
|
6538
6681
|
.slm-ch-counts .free b{color:#5bd39b}
|
|
6539
|
-
.slm-ch-counts b.bump{animation:slm-ch-bump
|
|
6682
|
+
.slm-ch-counts b.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6540
6683
|
@keyframes slm-ch-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08)}}
|
|
6541
6684
|
.slm-ch-access{margin-top:6px;font-size:10.5px;color:var(--slm-muted)}
|
|
6542
6685
|
.slm-ch-more{flex:none;color:var(--slm-muted);font-weight:800;padding:0 4px;min-height:28px}
|
|
@@ -6546,12 +6689,13 @@ var CHANNELS_CSS = `
|
|
|
6546
6689
|
font-weight:800;color:#0e1017}
|
|
6547
6690
|
.slm-ch-selsrc-row b{min-width:30px;text-align:right;font-weight:800}
|
|
6548
6691
|
.slm-ch-selsrc-row span{color:var(--slm-muted)}
|
|
6549
|
-
.slm-ch-selnum.bump{animation:slm-ch-bump
|
|
6692
|
+
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6550
6693
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
6551
6694
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
6552
6695
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
6553
6696
|
line-height:1.5;margin-bottom:12px}
|
|
6554
6697
|
.slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
|
|
6698
|
+
.slm-ch-alert.info{background:rgba(110,123,255,.12);border:1px solid rgba(110,123,255,.44);color:#c5cbff}
|
|
6555
6699
|
.slm-ch-alert.err{background:rgba(229,72,77,.1);border:1px solid rgba(229,72,77,.45);color:#f1a4a6}
|
|
6556
6700
|
.slm-ch-alert b{color:#fff}
|
|
6557
6701
|
.slm-ch-alert button{display:block;margin-top:6px;color:#fff;font-weight:800;min-height:36px}
|
|
@@ -6589,6 +6733,26 @@ var CHANNELS_CSS = `
|
|
|
6589
6733
|
border-radius:10px;background:rgba(244,183,64,.06);font-family:ui-monospace,Menlo,monospace;font-size:11px;
|
|
6590
6734
|
overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
6591
6735
|
.slm-ch-err{color:#f1a4a6;font-size:11.5px;margin-top:6px}
|
|
6736
|
+
|
|
6737
|
+
/* hosted access links \u2014 STATUS only; there is no Copy control on this card */
|
|
6738
|
+
.slm-ch-link{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
6739
|
+
margin-bottom:8px}
|
|
6740
|
+
.slm-ch-link .lk-head{display:flex;align-items:center;gap:8px}
|
|
6741
|
+
.slm-ch-link .lk-name{flex:1;min-width:0;font-size:12.5px;font-weight:800;overflow:hidden;text-overflow:ellipsis;
|
|
6742
|
+
white-space:nowrap}
|
|
6743
|
+
.slm-ch-lkrow{display:flex;gap:8px;margin-top:5px;font-size:11px;color:var(--slm-muted)}
|
|
6744
|
+
.slm-ch-lkrow .k{flex:none;min-width:104px}
|
|
6745
|
+
.slm-ch-lkrow .v{color:var(--slm-text);font-variant-numeric:tabular-nums}
|
|
6746
|
+
.slm-ch-meter{height:5px;border-radius:3px;background:rgba(255,255,255,.09);overflow:hidden;margin-top:8px}
|
|
6747
|
+
.slm-ch-meter i{display:block;height:100%;background:var(--slm-accent);
|
|
6748
|
+
transition:width var(--slm-mo-base) var(--slm-mo-out)}
|
|
6749
|
+
.slm-ch-radio{display:flex;gap:9px;align-items:flex-start;padding:11px 12px;border:1px solid var(--slm-line);
|
|
6750
|
+
border-radius:10px;margin-top:8px;font-size:12.5px;cursor:pointer;
|
|
6751
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out)}
|
|
6752
|
+
.slm-ch-radio:hover{border-color:var(--slm-muted)}
|
|
6753
|
+
.slm-ch-radio input{flex:none;margin-top:2px}
|
|
6754
|
+
.slm-ch-radio b{display:block;font-weight:800;margin-bottom:2px}
|
|
6755
|
+
.slm-ch-radio .why{display:block;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
6592
6756
|
.slm-ch-seatlist{max-height:44vh;overflow:auto;border:1px solid var(--slm-line);border-radius:10px;
|
|
6593
6757
|
background:var(--slm-surface);margin-top:10px}
|
|
6594
6758
|
.slm-ch-seatgroup{padding:8px 10px;border-bottom:1px solid var(--slm-line);display:flex;align-items:center;
|
|
@@ -6621,7 +6785,8 @@ var CHANNELS_CSS = `
|
|
|
6621
6785
|
.slm.compact .slm-modes{display:none}
|
|
6622
6786
|
|
|
6623
6787
|
@media (prefers-reduced-motion:reduce){
|
|
6624
|
-
.slm-ch-layer,.slm-ch-banner,.slm-ch-staged,.slm-ch-row,.slm.compact.ch-sheet .slm-rail
|
|
6788
|
+
.slm-ch-layer,.slm-ch-banner,.slm-ch-staged,.slm-ch-row,.slm.compact.ch-sheet .slm-rail,
|
|
6789
|
+
.slm-ch-meter i,.slm-ch-radio{transition:none!important}
|
|
6625
6790
|
.slm-ch-staged.shake,.slm-ch-tick,.slm-ch-bucket,.slm-ch-scrim,.slm-ch-dialog,
|
|
6626
6791
|
.slm-ch-counts b.bump,.slm-ch-selnum.bump{animation:none!important}
|
|
6627
6792
|
.slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
|
|
@@ -6637,9 +6802,37 @@ function bucketRowsHtml(rows) {
|
|
|
6637
6802
|
${row.peek ? `<span class="peek">${esc(row.peek)}</span>` : "<span></span>"}
|
|
6638
6803
|
</div>`).join("");
|
|
6639
6804
|
}
|
|
6805
|
+
var SERVER_INTEGRATION_HTML = `
|
|
6806
|
+
<p class="slm-eyebrow" style="margin-top:18px">Server integration</p>
|
|
6807
|
+
<p class="slm-hint">There is nothing to set up on this screen. Your own server mints a short-lived buyer
|
|
6808
|
+
access session for this channel with the SeatLayer server SDK and hands it to the widget. A channel name
|
|
6809
|
+
on its own never grants access.</p>
|
|
6810
|
+
<p class="slm-note"><a class="slm-linkbtn" href="https://docs.seatlayer.io/server-api/channels"
|
|
6811
|
+
target="_blank" rel="noreferrer noopener">Read the server integration guide \u2192</a></p>`;
|
|
6640
6812
|
function esc(value) {
|
|
6641
6813
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
6642
6814
|
}
|
|
6815
|
+
function datetimeLocalValue(ms) {
|
|
6816
|
+
const local = new Date(ms - new Date(ms).getTimezoneOffset() * 6e4);
|
|
6817
|
+
return local.toISOString().slice(0, 16);
|
|
6818
|
+
}
|
|
6819
|
+
function intField(root, selector) {
|
|
6820
|
+
const raw = root.querySelector(selector)?.value.trim() ?? "";
|
|
6821
|
+
const value = Number(raw);
|
|
6822
|
+
return raw !== "" && Number.isInteger(value) ? value : null;
|
|
6823
|
+
}
|
|
6824
|
+
function selectSecret(dialog) {
|
|
6825
|
+
const node = dialog.querySelector("[data-ch-lk-url]");
|
|
6826
|
+
if (!node) return;
|
|
6827
|
+
try {
|
|
6828
|
+
const range = document.createRange();
|
|
6829
|
+
range.selectNodeContents(node);
|
|
6830
|
+
const selection = window.getSelection();
|
|
6831
|
+
selection?.removeAllRanges();
|
|
6832
|
+
selection?.addRange(range);
|
|
6833
|
+
} catch {
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6643
6836
|
var ChannelsMode = class {
|
|
6644
6837
|
constructor(host, capabilities) {
|
|
6645
6838
|
this.active = false;
|
|
@@ -6649,6 +6842,10 @@ var ChannelsMode = class {
|
|
|
6649
6842
|
this.loadError = null;
|
|
6650
6843
|
this.loading = true;
|
|
6651
6844
|
this.view = "inspect";
|
|
6845
|
+
/** Pan is intentionally the initial desktop interaction. Assignment's
|
|
6846
|
+
* marquee is powerful, but must never make an organizer lose map navigation. */
|
|
6847
|
+
this.mapIntent = "pan";
|
|
6848
|
+
this.focusedSectionId = null;
|
|
6652
6849
|
this.showArchived = false;
|
|
6653
6850
|
this.detailChannelId = null;
|
|
6654
6851
|
this.targetChannelId = "";
|
|
@@ -6656,6 +6853,24 @@ var ChannelsMode = class {
|
|
|
6656
6853
|
this.dialog = null;
|
|
6657
6854
|
this.detent = "medium";
|
|
6658
6855
|
this.seatListLimit = SEAT_LIST_PAGE;
|
|
6856
|
+
/**
|
|
6857
|
+
* Hosted-link STATUS for the channel whose detail panel is open. This is the
|
|
6858
|
+
* listing projection — it carries no url and no capability, because no route
|
|
6859
|
+
* returns one. `unsupported` is the honest answer for a worker that predates
|
|
6860
|
+
* M8, exactly like the buyer-preview probe.
|
|
6861
|
+
*/
|
|
6862
|
+
this.links = [];
|
|
6863
|
+
this.linksChannelId = null;
|
|
6864
|
+
this.linksState = "idle";
|
|
6865
|
+
/**
|
|
6866
|
+
* Monotonic read generations — one for the channel list + allocation, one for
|
|
6867
|
+
* the open channel's links. Reads are concurrent (a 10s poll versus a
|
|
6868
|
+
* mutation's own reload), and the network does not promise to answer them in
|
|
6869
|
+
* order. Only the NEWEST read of each kind may write to state; an older
|
|
6870
|
+
* answer that arrives late is dropped, never painted.
|
|
6871
|
+
*/
|
|
6872
|
+
this.listSeq = 0;
|
|
6873
|
+
this.linksSeq = 0;
|
|
6659
6874
|
this.previewAudience = [];
|
|
6660
6875
|
this.previewIncludePublic = false;
|
|
6661
6876
|
this.previewProjection = null;
|
|
@@ -6682,10 +6897,14 @@ var ChannelsMode = class {
|
|
|
6682
6897
|
enter() {
|
|
6683
6898
|
if (this.active) return;
|
|
6684
6899
|
this.active = true;
|
|
6900
|
+
this.mapIntent = "pan";
|
|
6901
|
+
this.focusedSectionId = null;
|
|
6685
6902
|
this.ensureLayer();
|
|
6686
6903
|
this.host.root.classList.add("ch-mode");
|
|
6687
6904
|
this.applySheetClasses();
|
|
6905
|
+
if (this.host.sections().length > 1) this.host.showSectionOverview();
|
|
6688
6906
|
this.paintRail();
|
|
6907
|
+
this.onInteractionChange?.();
|
|
6689
6908
|
void this.refresh();
|
|
6690
6909
|
this.pollTimer = setInterval(() => {
|
|
6691
6910
|
void this.refresh({ quiet: true });
|
|
@@ -6699,6 +6918,9 @@ var ChannelsMode = class {
|
|
|
6699
6918
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
6700
6919
|
this.pollTimer = null;
|
|
6701
6920
|
this.closeDialog({ restoreFocus: false });
|
|
6921
|
+
this.links = [];
|
|
6922
|
+
this.linksChannelId = null;
|
|
6923
|
+
this.linksState = "idle";
|
|
6702
6924
|
this.layer?.classList.remove("on");
|
|
6703
6925
|
this.host.root.classList.remove(
|
|
6704
6926
|
"ch-mode",
|
|
@@ -6734,6 +6956,17 @@ var ChannelsMode = class {
|
|
|
6734
6956
|
canSelect() {
|
|
6735
6957
|
return this.caps.manage && this.view === "inspect";
|
|
6736
6958
|
}
|
|
6959
|
+
/** Bulk seat assignment is explicit. In Pan map, clicks can still inspect a
|
|
6960
|
+
* single seat, while a primary-button drag always moves the camera. */
|
|
6961
|
+
usesMarqueeSelection() {
|
|
6962
|
+
return this.canSelect() && this.mapIntent === "assign";
|
|
6963
|
+
}
|
|
6964
|
+
/** The renderer calls this when the organizer opens a section from overview. */
|
|
6965
|
+
handleSectionFocus(sectionId) {
|
|
6966
|
+
if (!this.active) return;
|
|
6967
|
+
this.focusedSectionId = sectionId;
|
|
6968
|
+
this.paintRail();
|
|
6969
|
+
}
|
|
6737
6970
|
/**
|
|
6738
6971
|
* Organizer realtime integration point. M5 ships a per-scope socket for
|
|
6739
6972
|
* buyers; the organizer channel-count stream is a later milestone. When it
|
|
@@ -6761,21 +6994,28 @@ var ChannelsMode = class {
|
|
|
6761
6994
|
// ---- data -----------------------------------------------------------------
|
|
6762
6995
|
async refresh(opts = {}) {
|
|
6763
6996
|
if (!this.caps.view) return;
|
|
6997
|
+
const seq = ++this.listSeq;
|
|
6998
|
+
const superseded = () => seq !== this.listSeq;
|
|
6764
6999
|
try {
|
|
6765
7000
|
const list = await this.host.api.channels(this.host.eventKey, { includeArchived: this.showArchived });
|
|
7001
|
+
if (superseded()) return;
|
|
6766
7002
|
this.list = list;
|
|
6767
7003
|
this.assignmentVersion = list.assignmentVersion;
|
|
6768
7004
|
this.loadError = null;
|
|
6769
7005
|
if (!this.targetChannelId) {
|
|
6770
7006
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
6771
7007
|
}
|
|
6772
|
-
await this.loadAllocation();
|
|
7008
|
+
await this.loadAllocation(seq);
|
|
7009
|
+
if (superseded()) return;
|
|
7010
|
+
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
7011
|
+
if (superseded()) return;
|
|
6773
7012
|
this.loading = false;
|
|
6774
7013
|
if (this.active) {
|
|
6775
7014
|
this.paintRail();
|
|
6776
7015
|
this.paintOverlay();
|
|
6777
7016
|
}
|
|
6778
7017
|
} catch (err) {
|
|
7018
|
+
if (superseded()) return;
|
|
6779
7019
|
this.loading = false;
|
|
6780
7020
|
if (err instanceof ManageApiError && err.status === 403) {
|
|
6781
7021
|
this.caps = { view: false, manage: false };
|
|
@@ -6787,10 +7027,11 @@ var ChannelsMode = class {
|
|
|
6787
7027
|
}
|
|
6788
7028
|
/** Walk every allocation page. Bounded by the event's seat count, and the
|
|
6789
7029
|
* server caps each page, so an arena is a handful of round trips. */
|
|
6790
|
-
async loadAllocation() {
|
|
7030
|
+
async loadAllocation(seq) {
|
|
6791
7031
|
const next = /* @__PURE__ */ new Map();
|
|
6792
7032
|
let afterLabel;
|
|
6793
7033
|
for (let page = 0; page < 200; page += 1) {
|
|
7034
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6794
7035
|
const res = await this.host.api.channelAllocation(this.host.eventKey, {
|
|
6795
7036
|
afterLabel,
|
|
6796
7037
|
limit: 1e3
|
|
@@ -6802,6 +7043,7 @@ var ChannelsMode = class {
|
|
|
6802
7043
|
if (!res.nextAfterLabel) break;
|
|
6803
7044
|
afterLabel = res.nextAfterLabel;
|
|
6804
7045
|
}
|
|
7046
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6805
7047
|
this.allocation = next;
|
|
6806
7048
|
}
|
|
6807
7049
|
// ---- lookups --------------------------------------------------------------
|
|
@@ -6868,9 +7110,10 @@ var ChannelsMode = class {
|
|
|
6868
7110
|
* Repaint the allocation (or preview) overlay in ONE canvas pass.
|
|
6869
7111
|
*
|
|
6870
7112
|
* Channel identity on the map is a fill in the administrative color PLUS the
|
|
6871
|
-
* letter flags below — never color alone.
|
|
6872
|
-
*
|
|
6873
|
-
*
|
|
7113
|
+
* letter flags below — never color alone. In buyer preview the map instead
|
|
7114
|
+
* uses two explicit, channel-neutral access states. Physical status keeps its
|
|
7115
|
+
* own cue: only FREE units are repainted, so sold/held/blocked seats still
|
|
7116
|
+
* read exactly as they do in every other tool.
|
|
6874
7117
|
*/
|
|
6875
7118
|
paintOverlay() {
|
|
6876
7119
|
const canvas = this.canvas;
|
|
@@ -6895,11 +7138,14 @@ var ChannelsMode = class {
|
|
|
6895
7138
|
if (!ctx) return;
|
|
6896
7139
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
6897
7140
|
ctx.clearRect(0, 0, width, height);
|
|
7141
|
+
const seatDetail = this.host.isSeatDetail();
|
|
6898
7142
|
const size = Math.max(3, this.host.seatPixelSize());
|
|
6899
7143
|
const half = size / 2;
|
|
6900
7144
|
const projection = this.view === "preview" ? this.previewProjection : null;
|
|
6901
7145
|
const eligible = projection ? new Set(projection.available === false ? [] : projection.eligible ?? []) : null;
|
|
6902
7146
|
const clusters = /* @__PURE__ */ new Map();
|
|
7147
|
+
const sectionTargets = !seatDetail && this.host.sections().length > 1 ? /* @__PURE__ */ new Map() : null;
|
|
7148
|
+
const previewSections = this.view === "preview" && seatDetail && size <= 15 ? /* @__PURE__ */ new Map() : null;
|
|
6903
7149
|
for (const seat of this.host.seats()) {
|
|
6904
7150
|
const status = this.host.statusOf(seat.label) ?? "free";
|
|
6905
7151
|
const channelId = this.allocation.get(seat.label) ?? PUBLIC_CHANNEL_ID;
|
|
@@ -6910,24 +7156,150 @@ var ChannelsMode = class {
|
|
|
6910
7156
|
cluster.n += 1;
|
|
6911
7157
|
clusters.set(channelId, cluster);
|
|
6912
7158
|
}
|
|
7159
|
+
if (!seatDetail) {
|
|
7160
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7161
|
+
const point2 = section ? this.host.worldToScreen({ x: seat.x, y: seat.y }) : null;
|
|
7162
|
+
if (sectionTargets && section && point2) {
|
|
7163
|
+
const bounds = sectionTargets.get(section.id) ?? {
|
|
7164
|
+
label: section.label,
|
|
7165
|
+
minX: point2.x,
|
|
7166
|
+
minY: point2.y,
|
|
7167
|
+
maxX: point2.x,
|
|
7168
|
+
maxY: point2.y
|
|
7169
|
+
};
|
|
7170
|
+
bounds.minX = Math.min(bounds.minX, point2.x);
|
|
7171
|
+
bounds.minY = Math.min(bounds.minY, point2.y);
|
|
7172
|
+
bounds.maxX = Math.max(bounds.maxX, point2.x);
|
|
7173
|
+
bounds.maxY = Math.max(bounds.maxY, point2.y);
|
|
7174
|
+
sectionTargets.set(section.id, bounds);
|
|
7175
|
+
}
|
|
7176
|
+
continue;
|
|
7177
|
+
}
|
|
6913
7178
|
if (status !== "free") continue;
|
|
6914
7179
|
let fill = null;
|
|
7180
|
+
let stroke = null;
|
|
6915
7181
|
if (this.view === "preview") {
|
|
6916
|
-
|
|
7182
|
+
if (eligible?.has(seat.label)) {
|
|
7183
|
+
fill = PREVIEW_ELIGIBLE_FILL;
|
|
7184
|
+
stroke = PREVIEW_ELIGIBLE_STROKE;
|
|
7185
|
+
} else {
|
|
7186
|
+
fill = PREVIEW_UNAVAILABLE_FILL;
|
|
7187
|
+
stroke = PREVIEW_UNAVAILABLE_STROKE;
|
|
7188
|
+
}
|
|
6917
7189
|
} else if (channelId !== PUBLIC_CHANNEL_ID) {
|
|
6918
7190
|
fill = this.markerFor(channelId).color;
|
|
7191
|
+
stroke = ALLOCATION_STROKE;
|
|
6919
7192
|
}
|
|
6920
7193
|
if (!fill) continue;
|
|
6921
7194
|
const point = this.host.worldToScreen({ x: seat.x, y: seat.y });
|
|
6922
7195
|
if (!point) continue;
|
|
6923
7196
|
if (point.x < -size || point.y < -size || point.x > width + size || point.y > height + size) continue;
|
|
7197
|
+
if (previewSections) {
|
|
7198
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7199
|
+
if (section) {
|
|
7200
|
+
const bounds = previewSections.get(section.id) ?? {
|
|
7201
|
+
label: section.label,
|
|
7202
|
+
minX: point.x,
|
|
7203
|
+
minY: point.y,
|
|
7204
|
+
maxX: point.x,
|
|
7205
|
+
maxY: point.y
|
|
7206
|
+
};
|
|
7207
|
+
bounds.minX = Math.min(bounds.minX, point.x);
|
|
7208
|
+
bounds.minY = Math.min(bounds.minY, point.y);
|
|
7209
|
+
bounds.maxX = Math.max(bounds.maxX, point.x);
|
|
7210
|
+
bounds.maxY = Math.max(bounds.maxY, point.y);
|
|
7211
|
+
previewSections.set(section.id, bounds);
|
|
7212
|
+
}
|
|
7213
|
+
}
|
|
6924
7214
|
ctx.fillStyle = fill;
|
|
6925
|
-
|
|
6926
|
-
|
|
7215
|
+
if (this.view === "preview" || channelId !== PUBLIC_CHANNEL_ID) {
|
|
7216
|
+
const radius = Math.max(2, half + Math.min(1.5, half * 0.06));
|
|
7217
|
+
ctx.globalAlpha = 1;
|
|
7218
|
+
ctx.beginPath();
|
|
7219
|
+
ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
|
|
7220
|
+
ctx.fill();
|
|
7221
|
+
ctx.strokeStyle = stroke ?? fill;
|
|
7222
|
+
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));
|
|
7223
|
+
ctx.stroke();
|
|
7224
|
+
if (this.view === "preview" && eligible?.has(seat.label) && size >= 22) {
|
|
7225
|
+
this.paintPreviewSeatLabel(ctx, seat.label, point.x, point.y, radius);
|
|
7226
|
+
}
|
|
7227
|
+
} else {
|
|
7228
|
+
ctx.globalAlpha = 0.85;
|
|
7229
|
+
ctx.fillRect(point.x - half, point.y - half, size, size);
|
|
7230
|
+
}
|
|
6927
7231
|
}
|
|
6928
7232
|
ctx.globalAlpha = 1;
|
|
7233
|
+
if (previewSections) this.paintPreviewSectionLabels(ctx, previewSections);
|
|
7234
|
+
this.paintSectionTargets(sectionTargets);
|
|
6929
7235
|
this.paintFlags(clusters);
|
|
6930
7236
|
}
|
|
7237
|
+
/** Draw an eligible seat's actual chart label without inventing a new buyer
|
|
7238
|
+
* identifier. Long labels scale down and are omitted rather than overflowing
|
|
7239
|
+
* into an adjacent seat. */
|
|
7240
|
+
paintPreviewSeatLabel(ctx, label, x, y, radius) {
|
|
7241
|
+
const maxWidth = radius * 1.55;
|
|
7242
|
+
let fontSize = Math.min(13, Math.max(7, radius * 0.55));
|
|
7243
|
+
const minFontSize = 6;
|
|
7244
|
+
while (fontSize >= minFontSize) {
|
|
7245
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7246
|
+
if (ctx.measureText(label).width <= maxWidth) break;
|
|
7247
|
+
fontSize -= 0.5;
|
|
7248
|
+
}
|
|
7249
|
+
if (fontSize < minFontSize) return;
|
|
7250
|
+
ctx.fillStyle = "#ffffff";
|
|
7251
|
+
ctx.textAlign = "center";
|
|
7252
|
+
ctx.textBaseline = "middle";
|
|
7253
|
+
ctx.fillText(label, x, y);
|
|
7254
|
+
}
|
|
7255
|
+
/** A section overview is a navigation map. These transparent, keyboardable
|
|
7256
|
+
* hit areas sit over the renderer's section shells so both mouse and keyboard
|
|
7257
|
+
* always take the organizer into the real focused-section camera state. */
|
|
7258
|
+
paintSectionTargets(sections) {
|
|
7259
|
+
const layer = this.layer;
|
|
7260
|
+
if (!layer) return;
|
|
7261
|
+
layer.querySelectorAll(".slm-ch-section-target").forEach((el) => el.remove());
|
|
7262
|
+
if (!sections) return;
|
|
7263
|
+
for (const [id, section] of sections) {
|
|
7264
|
+
const width = section.maxX - section.minX;
|
|
7265
|
+
const height = section.maxY - section.minY;
|
|
7266
|
+
if (width < 20 || height < 20) continue;
|
|
7267
|
+
const target = document.createElement("button");
|
|
7268
|
+
target.type = "button";
|
|
7269
|
+
target.className = "slm-ch-section-target";
|
|
7270
|
+
target.style.left = `${section.minX - 8}px`;
|
|
7271
|
+
target.style.top = `${section.minY - 8}px`;
|
|
7272
|
+
target.style.width = `${width + 16}px`;
|
|
7273
|
+
target.style.height = `${height + 16}px`;
|
|
7274
|
+
target.setAttribute("aria-label", `Open ${section.label} seats`);
|
|
7275
|
+
target.addEventListener("click", () => {
|
|
7276
|
+
this.focusedSectionId = id;
|
|
7277
|
+
this.host.focusSection(id);
|
|
7278
|
+
this.paintRail();
|
|
7279
|
+
});
|
|
7280
|
+
layer.appendChild(target);
|
|
7281
|
+
}
|
|
7282
|
+
}
|
|
7283
|
+
/** Keep renderer section names legible over a dense, zoomed-out preview. */
|
|
7284
|
+
paintPreviewSectionLabels(ctx, sections) {
|
|
7285
|
+
for (const section of sections.values()) {
|
|
7286
|
+
const width = section.maxX - section.minX;
|
|
7287
|
+
const height = section.maxY - section.minY;
|
|
7288
|
+
if (width < 52 || height < 26) continue;
|
|
7289
|
+
const centerX = (section.minX + section.maxX) / 2;
|
|
7290
|
+
const centerY = (section.minY + section.maxY) / 2;
|
|
7291
|
+
const fontSize = Math.max(11, Math.min(15, height * 0.16));
|
|
7292
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7293
|
+
const labelWidth = Math.min(width - 8, ctx.measureText(section.label).width + 18);
|
|
7294
|
+
const labelHeight = fontSize + 10;
|
|
7295
|
+
ctx.fillStyle = "rgba(11, 16, 28, .88)";
|
|
7296
|
+
ctx.fillRect(centerX - labelWidth / 2, centerY - labelHeight / 2, labelWidth, labelHeight);
|
|
7297
|
+
ctx.fillStyle = "#f8fafc";
|
|
7298
|
+
ctx.textAlign = "center";
|
|
7299
|
+
ctx.textBaseline = "middle";
|
|
7300
|
+
ctx.fillText(section.label, centerX, centerY);
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
6931
7303
|
/** Letter flags at each channel's centroid — the non-color identity cue. */
|
|
6932
7304
|
paintFlags(clusters) {
|
|
6933
7305
|
const layer = this.layer;
|
|
@@ -6993,7 +7365,7 @@ var ChannelsMode = class {
|
|
|
6993
7365
|
});
|
|
6994
7366
|
});
|
|
6995
7367
|
}
|
|
6996
|
-
setBanner(on, name = "") {
|
|
7368
|
+
setBanner(on, name = "", eligibleSeats) {
|
|
6997
7369
|
const banner = this.bannerEl;
|
|
6998
7370
|
if (!banner) return;
|
|
6999
7371
|
this.host.root.classList.toggle("ch-preview", on);
|
|
@@ -7003,8 +7375,9 @@ var ChannelsMode = class {
|
|
|
7003
7375
|
return;
|
|
7004
7376
|
}
|
|
7005
7377
|
const marker = this.previewAudience.length === 1 ? this.markerFor(this.previewAudience[0]) : { color: "var(--slm-accent)", letter: "" };
|
|
7378
|
+
const availability = eligibleSeats == null ? "" : ` \xB7 ${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat" : "seats"} available now`;
|
|
7006
7379
|
banner.innerHTML = `<span class="dot" style="background:${esc(marker.color)}"></span>
|
|
7007
|
-
Previewing buyer access \xB7 ${esc(name)} \xB7 read-only
|
|
7380
|
+
Previewing buyer access \xB7 ${esc(name)}${availability} \xB7 read-only
|
|
7008
7381
|
<button type="button" data-ch-act="exit-preview">Exit preview</button>`;
|
|
7009
7382
|
banner.classList.add("on");
|
|
7010
7383
|
banner.querySelector('[data-ch-act="exit-preview"]')?.addEventListener("click", () => this.setView("inspect"));
|
|
@@ -7039,6 +7412,8 @@ var ChannelsMode = class {
|
|
|
7039
7412
|
includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
|
|
7040
7413
|
});
|
|
7041
7414
|
this.previewSupported = true;
|
|
7415
|
+
const eligibleSeats = this.previewProjection.available === false ? void 0 : this.previewProjection.counts?.eligible ?? this.previewProjection.eligible?.length;
|
|
7416
|
+
this.setBanner(true, names, eligibleSeats);
|
|
7042
7417
|
} catch (err) {
|
|
7043
7418
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7044
7419
|
this.previewSupported = !(status === 404 || status === 405 || status === 501);
|
|
@@ -7089,6 +7464,22 @@ var ChannelsMode = class {
|
|
|
7089
7464
|
aria-pressed="${this.view === "inspect"}">Inspect allocation</button>
|
|
7090
7465
|
<button type="button" class="${previewOn.trim()}" data-ch-view="preview"
|
|
7091
7466
|
aria-pressed="${this.view === "preview"}">Preview buyer access</button>
|
|
7467
|
+
</div>${this.mapNavigationHtml()}`;
|
|
7468
|
+
}
|
|
7469
|
+
mapNavigationHtml() {
|
|
7470
|
+
if (this.host.sections().length < 2) return "";
|
|
7471
|
+
const focused = this.focusedSectionId ? this.host.sections().find((section) => section.id === this.focusedSectionId)?.label ?? "section" : null;
|
|
7472
|
+
const panOn = this.mapIntent === "pan" ? " on" : "";
|
|
7473
|
+
const assignOn = this.mapIntent === "assign" ? " on" : "";
|
|
7474
|
+
const intent = this.view === "inspect" && this.caps.manage ? `<div class="slm-ch-viewseg" role="group" aria-label="Map interaction">
|
|
7475
|
+
<button type="button" class="${panOn.trim()}" data-ch-map="pan" aria-pressed="${this.mapIntent === "pan"}">Pan map</button>
|
|
7476
|
+
<button type="button" class="${assignOn.trim()}" data-ch-map="assign" aria-pressed="${this.mapIntent === "assign"}">Assign seats</button>
|
|
7477
|
+
</div>
|
|
7478
|
+
<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>";
|
|
7479
|
+
return `<div class="slm-ch-mapnav">
|
|
7480
|
+
<div class="slm-ch-mapnav-head"><span>${focused ? `Viewing ${esc(focused)}` : "Section overview"}</span>
|
|
7481
|
+
<button type="button" data-ch-act="sections">All sections</button></div>
|
|
7482
|
+
${intent}
|
|
7092
7483
|
</div>`;
|
|
7093
7484
|
}
|
|
7094
7485
|
countsHtml(counts, key) {
|
|
@@ -7207,11 +7598,8 @@ var ChannelsMode = class {
|
|
|
7207
7598
|
<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>
|
|
7208
7599
|
${selfServiceGap ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
7209
7600
|
<span>This channel is marked for buyer self-service but no buyer has been let in yet.</span></div>` : ""}
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
<div class="slm-ch-row2"><button type="button" class="slm-btn ghost" data-ch-act="server-access" disabled
|
|
7213
|
-
title="Guided server setup ships in the next milestone">Configure server integration \xB7 Coming soon</button></div>
|
|
7214
|
-
<p class="slm-note">Your own server can already mint buyer access sessions for this channel with the server SDK.</p>` : "";
|
|
7601
|
+
${this.hostedLinksHtml()}
|
|
7602
|
+
${SERVER_INTEGRATION_HTML}` : "";
|
|
7215
7603
|
return `
|
|
7216
7604
|
<p class="slm-eyebrow">
|
|
7217
7605
|
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
@@ -7221,6 +7609,92 @@ var ChannelsMode = class {
|
|
|
7221
7609
|
${access}
|
|
7222
7610
|
${lifecycle}`;
|
|
7223
7611
|
}
|
|
7612
|
+
// ---- hosted access links --------------------------------------------------
|
|
7613
|
+
/**
|
|
7614
|
+
* Read the status projection for the open channel. Never paints — the caller
|
|
7615
|
+
* decides when the rail repaints, so a poll-driven reload does not fight a
|
|
7616
|
+
* user-driven one. A worker without M8 answers 404/405 and gets the honest
|
|
7617
|
+
* "needs a newer server" line rather than an error toast.
|
|
7618
|
+
*/
|
|
7619
|
+
async loadLinks(channelId) {
|
|
7620
|
+
if (!this.caps.view) return;
|
|
7621
|
+
const seq = ++this.linksSeq;
|
|
7622
|
+
const superseded = () => seq !== this.linksSeq || this.linksChannelId !== channelId;
|
|
7623
|
+
if (this.linksChannelId !== channelId) {
|
|
7624
|
+
this.links = [];
|
|
7625
|
+
this.linksChannelId = channelId;
|
|
7626
|
+
this.linksState = "loading";
|
|
7627
|
+
}
|
|
7628
|
+
try {
|
|
7629
|
+
const res = await this.host.api.accessLinks(this.host.eventKey, channelId);
|
|
7630
|
+
if (superseded()) return;
|
|
7631
|
+
this.links = res.links ?? [];
|
|
7632
|
+
this.linksState = "ready";
|
|
7633
|
+
} catch (err) {
|
|
7634
|
+
if (superseded()) return;
|
|
7635
|
+
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7636
|
+
this.links = [];
|
|
7637
|
+
this.linksState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
|
|
7638
|
+
if (this.linksState === "error") this.host.onError(err);
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7641
|
+
/**
|
|
7642
|
+
* The hosted-link section of the detail panel.
|
|
7643
|
+
*
|
|
7644
|
+
* STATUS ONLY, by design (comp 06 `hosted`): label, state, expiry,
|
|
7645
|
+
* redemptions, seats per buyer, live sessions. There is no Copy control here
|
|
7646
|
+
* and no field to hang one on — the URL was shown once at creation and cannot
|
|
7647
|
+
* be produced again. Rotation is the recovery path, and it says so.
|
|
7648
|
+
*/
|
|
7649
|
+
hostedLinksHtml() {
|
|
7650
|
+
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Hosted access links</p>`;
|
|
7651
|
+
if (this.linksState === "unsupported") {
|
|
7652
|
+
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
7653
|
+
<span><b>Hosted links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
7654
|
+
}
|
|
7655
|
+
if (this.linksState === "error") {
|
|
7656
|
+
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
7657
|
+
<span><b>Couldn't load this channel's links.</b>
|
|
7658
|
+
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
7659
|
+
}
|
|
7660
|
+
const live = this.links.filter(accessLinkIsLive).length;
|
|
7661
|
+
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
|
|
7662
|
+
they open the link and buy only these seats.</p>`;
|
|
7663
|
+
const create = this.caps.manage ? `<button type="button" class="slm-btn" style="width:100%" data-ch-act="link-create">
|
|
7664
|
+
${live ? "Create another hosted link" : "Create hosted access link"}</button>` : "";
|
|
7665
|
+
return `${eyebrow}
|
|
7666
|
+
${cards}
|
|
7667
|
+
${create}
|
|
7668
|
+
<p class="slm-note">A link is shown once, when you create it. SeatLayer keeps only a fingerprint of it, so it
|
|
7669
|
+
can never be shown again \u2014 if a link is lost, rotate it and send the fresh one.</p>`;
|
|
7670
|
+
}
|
|
7671
|
+
linkCardHtml(link) {
|
|
7672
|
+
const badge = accessLinkBadge(link);
|
|
7673
|
+
const used = link.maxRedemptions > 0 ? Math.min(100, Math.round(link.redemptions / link.maxRedemptions * 100)) : 0;
|
|
7674
|
+
const rows = accessLinkPolicyLines(link).map((row) => `<div class="slm-ch-lkrow"><span class="k">${esc(row.k)}</span>
|
|
7675
|
+
<span class="v">${esc(row.v)}</span></div>`).join("");
|
|
7676
|
+
const sessions = link.activeSessions ? `<div class="slm-ch-lkrow"><span class="k">Buyers inside now</span>
|
|
7677
|
+
<span class="v">${link.activeSessions.toLocaleString()}</span></div>` : "";
|
|
7678
|
+
const lastUsed = link.lastRedeemedAt ? `<div class="slm-ch-lkrow"><span class="k">Last opened</span>
|
|
7679
|
+
<span class="v">${esc(new Date(link.lastRedeemedAt).toLocaleString())}</span></div>` : "";
|
|
7680
|
+
const actions = this.caps.manage && accessLinkIsLive(link) ? `<div class="slm-ch-row2">
|
|
7681
|
+
<button type="button" class="slm-btn ghost" data-ch-rotate="${esc(link.id)}">Rotate</button>
|
|
7682
|
+
<button type="button" class="slm-btn ghost" data-ch-revoke="${esc(link.id)}">Revoke</button>
|
|
7683
|
+
</div>` : "";
|
|
7684
|
+
return `<div class="slm-ch-link">
|
|
7685
|
+
<span class="lk-head">
|
|
7686
|
+
<span class="lk-name">${esc(link.label || "Hosted link")}</span>
|
|
7687
|
+
<span class="slm-ch-badge ${badge.kind}">${esc(badge.text)}</span>
|
|
7688
|
+
</span>
|
|
7689
|
+
<div class="slm-ch-meter" role="img"
|
|
7690
|
+
aria-label="${link.redemptions.toLocaleString()} of ${link.maxRedemptions.toLocaleString()} redemptions used">
|
|
7691
|
+
<i style="width:${used}%"></i></div>
|
|
7692
|
+
${rows}${sessions}${lastUsed}
|
|
7693
|
+
<div class="slm-ch-lkrow"><span class="k">The URL</span>
|
|
7694
|
+
<span class="v">Revealed once at creation \u2014 not recoverable</span></div>
|
|
7695
|
+
${actions}
|
|
7696
|
+
</div>`;
|
|
7697
|
+
}
|
|
7224
7698
|
previewRailHtml() {
|
|
7225
7699
|
const audienceOptions = [
|
|
7226
7700
|
{ id: PUBLIC_CHANNEL_ID, name: this.list?.publicSale.name ?? PUBLIC_CHANNEL_NAME },
|
|
@@ -7234,9 +7708,9 @@ var ChannelsMode = class {
|
|
|
7234
7708
|
const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
|
|
7235
7709
|
<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")}.
|
|
7236
7710
|
A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
|
|
7237
|
-
const
|
|
7238
|
-
const summary =
|
|
7239
|
-
|
|
7711
|
+
const eligibleSeats = this.previewProjection?.counts?.eligible ?? this.previewProjection?.eligible?.length;
|
|
7712
|
+
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>
|
|
7713
|
+
This is the exact buyer-visible allocation.${this.previewProjection?.includePublic === false ? " Public sale seats are <b>not</b> included in this access." : ""}</span></div>` : "";
|
|
7240
7714
|
const includePublic = isPublicChannelId(current) ? "" : `
|
|
7241
7715
|
<label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:10px 0">
|
|
7242
7716
|
<input type="checkbox" data-ch-includepublic ${this.previewIncludePublic ? "checked" : ""} />
|
|
@@ -7267,12 +7741,35 @@ var ChannelsMode = class {
|
|
|
7267
7741
|
rail.querySelectorAll("[data-ch-view]").forEach((button) => {
|
|
7268
7742
|
button.addEventListener("click", () => this.setView(button.dataset.chView));
|
|
7269
7743
|
});
|
|
7744
|
+
rail.querySelectorAll("[data-ch-map]").forEach((button) => {
|
|
7745
|
+
button.addEventListener("click", () => {
|
|
7746
|
+
this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
|
|
7747
|
+
this.paintRail();
|
|
7748
|
+
this.onInteractionChange?.();
|
|
7749
|
+
});
|
|
7750
|
+
});
|
|
7270
7751
|
rail.querySelectorAll("[data-ch-detail]").forEach((button) => {
|
|
7271
7752
|
button.addEventListener("click", () => {
|
|
7272
|
-
|
|
7753
|
+
const channelId = button.dataset.chDetail;
|
|
7754
|
+
this.detailChannelId = channelId;
|
|
7273
7755
|
this.paintRail();
|
|
7756
|
+
void this.loadLinks(channelId).then(() => this.paintRail());
|
|
7274
7757
|
});
|
|
7275
7758
|
});
|
|
7759
|
+
rail.querySelectorAll("[data-ch-rotate]").forEach((button) => {
|
|
7760
|
+
button.addEventListener("click", () => this.openDialog({
|
|
7761
|
+
kind: "linkRotate",
|
|
7762
|
+
channelId: this.detailChannelId,
|
|
7763
|
+
linkId: button.dataset.chRotate
|
|
7764
|
+
}));
|
|
7765
|
+
});
|
|
7766
|
+
rail.querySelectorAll("[data-ch-revoke]").forEach((button) => {
|
|
7767
|
+
button.addEventListener("click", () => this.openDialog({
|
|
7768
|
+
kind: "linkRevoke",
|
|
7769
|
+
channelId: this.detailChannelId,
|
|
7770
|
+
linkId: button.dataset.chRevoke
|
|
7771
|
+
}));
|
|
7772
|
+
});
|
|
7276
7773
|
const target = rail.querySelector("[data-ch-target]");
|
|
7277
7774
|
target?.addEventListener("change", () => {
|
|
7278
7775
|
this.targetChannelId = target.value;
|
|
@@ -7301,6 +7798,11 @@ var ChannelsMode = class {
|
|
|
7301
7798
|
}
|
|
7302
7799
|
railAction(action) {
|
|
7303
7800
|
switch (action) {
|
|
7801
|
+
case "sections":
|
|
7802
|
+
this.focusedSectionId = null;
|
|
7803
|
+
this.host.showSectionOverview();
|
|
7804
|
+
this.paintRail();
|
|
7805
|
+
break;
|
|
7304
7806
|
case "create":
|
|
7305
7807
|
this.openDialog({ kind: "create" });
|
|
7306
7808
|
break;
|
|
@@ -7324,8 +7826,17 @@ var ChannelsMode = class {
|
|
|
7324
7826
|
break;
|
|
7325
7827
|
case "back":
|
|
7326
7828
|
this.detailChannelId = null;
|
|
7829
|
+
this.linksChannelId = null;
|
|
7830
|
+
this.links = [];
|
|
7831
|
+
this.linksState = "idle";
|
|
7327
7832
|
this.paintRail();
|
|
7328
7833
|
break;
|
|
7834
|
+
case "link-create":
|
|
7835
|
+
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
7836
|
+
break;
|
|
7837
|
+
case "link-reload":
|
|
7838
|
+
if (this.detailChannelId) void this.reloadLinks();
|
|
7839
|
+
break;
|
|
7329
7840
|
case "retry":
|
|
7330
7841
|
void this.refresh();
|
|
7331
7842
|
break;
|
|
@@ -7397,6 +7908,9 @@ var ChannelsMode = class {
|
|
|
7397
7908
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
7398
7909
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
7399
7910
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
7911
|
+
else if (state.kind === "linkCreate") this.renderLinkCreateDialog(state);
|
|
7912
|
+
else if (state.kind === "linkRotate") this.renderLinkRotateDialog(state);
|
|
7913
|
+
else if (state.kind === "linkRevoke") this.renderLinkRevokeDialog(state);
|
|
7400
7914
|
}
|
|
7401
7915
|
/**
|
|
7402
7916
|
* Mount a modal: `aria-modal` dialog, programmatic name, focus moved inside,
|
|
@@ -7463,7 +7977,7 @@ var ChannelsMode = class {
|
|
|
7463
7977
|
<label>Marker</label>
|
|
7464
7978
|
<div style="display:flex;gap:8px;align-items:center">
|
|
7465
7979
|
<span class="slm-ch-mk" data-ch-marker style="background:${esc(suggestion.color)};width:28px;height:28px;font-size:13px">${esc(suggestion.letter)}</span>
|
|
7466
|
-
<span class="slm-note" style="margin:0">Letter
|
|
7980
|
+
<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>
|
|
7467
7981
|
</div>
|
|
7468
7982
|
</div>
|
|
7469
7983
|
<div class="slm-field">
|
|
@@ -7806,6 +8320,298 @@ var ChannelsMode = class {
|
|
|
7806
8320
|
});
|
|
7807
8321
|
});
|
|
7808
8322
|
}
|
|
8323
|
+
// ---- hosted-link dialogs --------------------------------------------------
|
|
8324
|
+
async reloadLinks() {
|
|
8325
|
+
const channelId = this.detailChannelId;
|
|
8326
|
+
if (!channelId) return;
|
|
8327
|
+
this.linksState = this.links.length ? this.linksState : "loading";
|
|
8328
|
+
await this.loadLinks(channelId);
|
|
8329
|
+
this.paintRail();
|
|
8330
|
+
}
|
|
8331
|
+
/**
|
|
8332
|
+
* The reload EVERY link mutation owes the panel.
|
|
8333
|
+
*
|
|
8334
|
+
* A create/rotate/revoke changes two things the detail panel renders: the
|
|
8335
|
+
* channel's access line (the server sets `access.intent` on create, and clears
|
|
8336
|
+
* it when the last live link goes) and the link status list. Both are re-read
|
|
8337
|
+
* here and the rail repainted, so the panel the organizer is already looking
|
|
8338
|
+
* at is current the moment the mutation lands — no reload, and no dependence
|
|
8339
|
+
* on HOW the one-time reveal was dismissed (the button, Escape, or never).
|
|
8340
|
+
*/
|
|
8341
|
+
async reloadAfterLinkChange(channelId) {
|
|
8342
|
+
if (this.detailChannelId === channelId) {
|
|
8343
|
+
await this.refresh({ quiet: true });
|
|
8344
|
+
return;
|
|
8345
|
+
}
|
|
8346
|
+
await this.loadLinks(channelId);
|
|
8347
|
+
if (this.active) this.paintRail();
|
|
8348
|
+
}
|
|
8349
|
+
linkById(linkId) {
|
|
8350
|
+
return this.links.find((link) => link.id === linkId) ?? null;
|
|
8351
|
+
}
|
|
8352
|
+
/**
|
|
8353
|
+
* Create. The three policy fields carry the owner's defaults and every one of
|
|
8354
|
+
* them is editable; the PLATFORM bounds (60s–180d, 1–10 000, 1–100, 20 live
|
|
8355
|
+
* links) are the server's to enforce and the server's to explain, so this form
|
|
8356
|
+
* checks only that a number is a number and surfaces the server's sentence for
|
|
8357
|
+
* everything else.
|
|
8358
|
+
*/
|
|
8359
|
+
renderLinkCreateDialog(state) {
|
|
8360
|
+
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
8361
|
+
if (!channel || !this.caps.manage) {
|
|
8362
|
+
this.closeDialog();
|
|
8363
|
+
return;
|
|
8364
|
+
}
|
|
8365
|
+
this.renderScrim(`
|
|
8366
|
+
<h3 id="slm-ch-dlg-title">Create a hosted access link for ${esc(channel.name)}</h3>
|
|
8367
|
+
<p class="sub">Anyone who opens the link can buy from this channel's allocation \u2014 and only from it.
|
|
8368
|
+
You'll see the link once, right after you create it.</p>
|
|
8369
|
+
<div class="slm-field">
|
|
8370
|
+
<label for="slm-ch-lk-label">Label <span style="text-transform:none;font-weight:500">(optional)</span></label>
|
|
8371
|
+
<input class="slm-input" id="slm-ch-lk-label" maxlength="80" placeholder="e.g. VIP list Nov 14" />
|
|
8372
|
+
<p class="slm-note">So you can tell your links apart later. Buyers never see it.</p>
|
|
8373
|
+
</div>
|
|
8374
|
+
<div class="slm-field">
|
|
8375
|
+
<label for="slm-ch-lk-expiry">Stops working</label>
|
|
8376
|
+
<select class="slm-select" id="slm-ch-lk-expiry" data-ch-lk-expiry>
|
|
8377
|
+
<option value="event" selected>When the event starts</option>
|
|
8378
|
+
<option value="custom">On a date I choose</option>
|
|
8379
|
+
</select>
|
|
8380
|
+
</div>
|
|
8381
|
+
<div class="slm-field" data-ch-lk-when-field hidden>
|
|
8382
|
+
<label for="slm-ch-lk-when">Date and time</label>
|
|
8383
|
+
<input class="slm-input" type="datetime-local" id="slm-ch-lk-when" />
|
|
8384
|
+
</div>
|
|
8385
|
+
<div class="slm-field">
|
|
8386
|
+
<label for="slm-ch-lk-redemptions">How many people can use it</label>
|
|
8387
|
+
<input class="slm-input" type="number" id="slm-ch-lk-redemptions" inputmode="numeric"
|
|
8388
|
+
value="${ACCESS_LINK_DEFAULTS.maxRedemptions}" />
|
|
8389
|
+
<p class="slm-note">Each buyer who opens the link uses one.</p>
|
|
8390
|
+
</div>
|
|
8391
|
+
<div class="slm-field">
|
|
8392
|
+
<label for="slm-ch-lk-quantity">Seats per buyer</label>
|
|
8393
|
+
<input class="slm-input" type="number" id="slm-ch-lk-quantity" inputmode="numeric"
|
|
8394
|
+
value="${ACCESS_LINK_DEFAULTS.maxQuantity}" />
|
|
8395
|
+
</div>
|
|
8396
|
+
<label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:2px 0 6px">
|
|
8397
|
+
<input type="checkbox" id="slm-ch-lk-public" />
|
|
8398
|
+
Also let this link buy Public sale seats
|
|
8399
|
+
</label>
|
|
8400
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8401
|
+
<div class="foot">
|
|
8402
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8403
|
+
<button type="button" class="slm-btn" data-ch-lk-create>Create link</button>
|
|
8404
|
+
</div>`, (dialog) => {
|
|
8405
|
+
const expiry = dialog.querySelector("[data-ch-lk-expiry]");
|
|
8406
|
+
const whenField = dialog.querySelector("[data-ch-lk-when-field]");
|
|
8407
|
+
const when = dialog.querySelector("#slm-ch-lk-when");
|
|
8408
|
+
expiry.addEventListener("change", () => {
|
|
8409
|
+
const custom = expiry.value === "custom";
|
|
8410
|
+
whenField.hidden = !custom;
|
|
8411
|
+
if (custom && !when.value) when.value = datetimeLocalValue(Date.now() + 7 * 864e5);
|
|
8412
|
+
});
|
|
8413
|
+
dialog.querySelector("[data-ch-lk-create]")?.addEventListener("click", () => {
|
|
8414
|
+
const maxRedemptions = intField(dialog, "#slm-ch-lk-redemptions");
|
|
8415
|
+
const maxQuantity = intField(dialog, "#slm-ch-lk-quantity");
|
|
8416
|
+
if (maxRedemptions == null || maxQuantity == null) {
|
|
8417
|
+
this.showDialogError("Those two settings need to be whole numbers.");
|
|
8418
|
+
return;
|
|
8419
|
+
}
|
|
8420
|
+
let expiresAt;
|
|
8421
|
+
if (expiry.value === "custom") {
|
|
8422
|
+
expiresAt = Date.parse(when.value);
|
|
8423
|
+
if (!Number.isFinite(expiresAt)) {
|
|
8424
|
+
this.showDialogError("Pick the date and time the link should stop working.");
|
|
8425
|
+
return;
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
void this.createLink(channel.id, {
|
|
8429
|
+
label: dialog.querySelector("#slm-ch-lk-label")?.value.trim() || null,
|
|
8430
|
+
includePublic: dialog.querySelector("#slm-ch-lk-public")?.checked ?? false,
|
|
8431
|
+
...expiresAt === void 0 ? {} : { expiresAt },
|
|
8432
|
+
maxRedemptions,
|
|
8433
|
+
maxQuantity
|
|
8434
|
+
});
|
|
8435
|
+
});
|
|
8436
|
+
});
|
|
8437
|
+
}
|
|
8438
|
+
async createLink(channelId, input) {
|
|
8439
|
+
try {
|
|
8440
|
+
const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
|
|
8441
|
+
this.revealLink(reveal, { channelId });
|
|
8442
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8443
|
+
} catch (err) {
|
|
8444
|
+
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8445
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
/**
|
|
8449
|
+
* The ONE-TIME reveal.
|
|
8450
|
+
*
|
|
8451
|
+
* Three things make this unrecoverable rather than merely "not shown twice":
|
|
8452
|
+
*
|
|
8453
|
+
* 1. `url` is a local const. It is never assigned to a field on this class,
|
|
8454
|
+
* never handed to the host, never put in a `DialogState`.
|
|
8455
|
+
* 2. `this.dialog` is cleared FIRST, so `renderDialog()` — the only function
|
|
8456
|
+
* that rebuilds a sheet — has nothing to rebuild this one from.
|
|
8457
|
+
* 3. The string exists in exactly one DOM node inside the scrim. Dismissing
|
|
8458
|
+
* the dialog removes the scrim, and the closure goes with it.
|
|
8459
|
+
*
|
|
8460
|
+
* The server holds only a hash, so even a compromised client cannot ask for it
|
|
8461
|
+
* again. Rotation is the recovery path, and the copy says so.
|
|
8462
|
+
*/
|
|
8463
|
+
revealLink(reveal, opts) {
|
|
8464
|
+
const url = reveal.url;
|
|
8465
|
+
this.dialog = null;
|
|
8466
|
+
const rotated = opts.rotated ? `<div class="slm-ch-alert warn" role="status"><span>\u26A0</span><span>
|
|
8467
|
+
<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>` : "";
|
|
8468
|
+
const policy = accessLinkPolicyLines(reveal.link).map((row) => `<div class="slm-ch-lkrow"><span class="k">${esc(row.k)}</span>
|
|
8469
|
+
<span class="v">${esc(row.v)}</span></div>`).join("");
|
|
8470
|
+
this.renderScrim(`
|
|
8471
|
+
<h3 id="slm-ch-dlg-title">Copy this link now</h3>
|
|
8472
|
+
<p class="sub">This is the only time SeatLayer can show it. We keep just a fingerprint, so it cannot be
|
|
8473
|
+
shown again \u2014 if it is lost, rotate the link for a fresh one.</p>
|
|
8474
|
+
${rotated}
|
|
8475
|
+
<div class="slm-ch-secret" data-ch-lk-url>${esc(url)}</div>
|
|
8476
|
+
<div class="slm-ch-row2" style="margin-top:8px">
|
|
8477
|
+
<button type="button" class="slm-btn" data-ch-lk-copy>Copy link</button>
|
|
8478
|
+
</div>
|
|
8479
|
+
<div class="slm-ch-alert warn" style="margin-top:12px"><span>\u26A0</span>
|
|
8480
|
+
<span>Anyone who opens this link can buy from this allocation. Send it only to the people it is meant
|
|
8481
|
+
for \u2014 forwarding it hands on the same access, and SeatLayer cannot tell the difference.</span></div>
|
|
8482
|
+
<p class="slm-eyebrow" style="margin-top:14px">What this link allows</p>
|
|
8483
|
+
${policy}
|
|
8484
|
+
<div class="foot">
|
|
8485
|
+
<button type="button" class="slm-btn" data-ch-close data-ch-lk-done>I've copied it</button>
|
|
8486
|
+
</div>`, (dialog) => {
|
|
8487
|
+
const copy = dialog.querySelector("[data-ch-lk-copy]");
|
|
8488
|
+
copy?.addEventListener("click", () => {
|
|
8489
|
+
const ok = () => {
|
|
8490
|
+
copy.textContent = "Copied";
|
|
8491
|
+
this.announce("Hosted access link copied.");
|
|
8492
|
+
};
|
|
8493
|
+
const clipboard = typeof navigator === "undefined" ? null : navigator.clipboard;
|
|
8494
|
+
if (clipboard?.writeText) {
|
|
8495
|
+
clipboard.writeText(url).then(ok, () => selectSecret(dialog));
|
|
8496
|
+
return;
|
|
8497
|
+
}
|
|
8498
|
+
selectSecret(dialog);
|
|
8499
|
+
});
|
|
8500
|
+
});
|
|
8501
|
+
this.announce("Your hosted access link is ready and is shown once.");
|
|
8502
|
+
}
|
|
8503
|
+
/**
|
|
8504
|
+
* Rotate. The organizer must SAY what happens to the buyers already inside —
|
|
8505
|
+
* the confirm stays disabled until one of the two choices is picked, because
|
|
8506
|
+
* the gentle branch and the destructive branch are both real decisions and the
|
|
8507
|
+
* server refuses (422 `end_active_sessions_required`) to guess either.
|
|
8508
|
+
*/
|
|
8509
|
+
renderLinkRotateDialog(state) {
|
|
8510
|
+
const link = this.linkById(state.linkId);
|
|
8511
|
+
if (!link || !this.caps.manage) {
|
|
8512
|
+
this.closeDialog();
|
|
8513
|
+
return;
|
|
8514
|
+
}
|
|
8515
|
+
const sessions = link.activeSessions ?? 0;
|
|
8516
|
+
const warning = sessions ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
8517
|
+
<span><b>${sessions.toLocaleString()} buyer${sessions === 1 ? "" : "s"}</b> got in with the current link
|
|
8518
|
+
and still ${sessions === 1 ? "has" : "have"} active access.</span></div>` : "";
|
|
8519
|
+
this.renderScrim(`
|
|
8520
|
+
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "hosted")} link?</h3>
|
|
8521
|
+
<p class="sub">The current link stops opening immediately and cannot be restored. You will get a new
|
|
8522
|
+
link to copy \u2014 shown once.</p>
|
|
8523
|
+
${warning}
|
|
8524
|
+
<label class="slm-ch-radio">
|
|
8525
|
+
<input type="radio" name="slm-ch-rot" value="keep" data-ch-rot />
|
|
8526
|
+
<span><b>Let them finish</b><span class="why">Access already handed out expires on its own; seats in
|
|
8527
|
+
checkout are untouched. Every new visit needs the new link.</span></span>
|
|
8528
|
+
</label>
|
|
8529
|
+
<label class="slm-ch-radio">
|
|
8530
|
+
<input type="radio" name="slm-ch-rot" value="end" data-ch-rot />
|
|
8531
|
+
<span><b>End their access now</b><span class="why">All access from the old link ends immediately.
|
|
8532
|
+
Buyers part-way through choosing seats lose access.</span></span>
|
|
8533
|
+
</label>
|
|
8534
|
+
<p class="slm-note">Choose one \u2014 SeatLayer will not decide this for you.</p>
|
|
8535
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8536
|
+
<div class="foot">
|
|
8537
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8538
|
+
<button type="button" class="slm-btn" data-ch-lk-rotate disabled>Rotate and copy new link</button>
|
|
8539
|
+
</div>`, (dialog) => {
|
|
8540
|
+
const confirm = dialog.querySelector("[data-ch-lk-rotate]");
|
|
8541
|
+
dialog.querySelectorAll("[data-ch-rot]").forEach((radio) => {
|
|
8542
|
+
radio.addEventListener("change", () => {
|
|
8543
|
+
confirm.disabled = false;
|
|
8544
|
+
});
|
|
8545
|
+
});
|
|
8546
|
+
confirm.addEventListener("click", () => {
|
|
8547
|
+
const picked = [...dialog.querySelectorAll("[data-ch-rot]")].find((radio) => radio.checked);
|
|
8548
|
+
if (!picked) {
|
|
8549
|
+
this.showDialogError(accessLinkErrorCopy({ code: "end_active_sessions_required" }));
|
|
8550
|
+
return;
|
|
8551
|
+
}
|
|
8552
|
+
void this.rotateLink(state.channelId, link.id, picked.value === "end");
|
|
8553
|
+
});
|
|
8554
|
+
});
|
|
8555
|
+
}
|
|
8556
|
+
async rotateLink(channelId, linkId, endActiveSessions) {
|
|
8557
|
+
try {
|
|
8558
|
+
const reveal = await this.host.api.rotateAccessLink(
|
|
8559
|
+
this.host.eventKey,
|
|
8560
|
+
channelId,
|
|
8561
|
+
linkId,
|
|
8562
|
+
endActiveSessions
|
|
8563
|
+
);
|
|
8564
|
+
this.revealLink(reveal, { channelId, rotated: true });
|
|
8565
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8566
|
+
} catch (err) {
|
|
8567
|
+
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8568
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
8569
|
+
}
|
|
8570
|
+
}
|
|
8571
|
+
renderLinkRevokeDialog(state) {
|
|
8572
|
+
const link = this.linkById(state.linkId);
|
|
8573
|
+
if (!link || !this.caps.manage) {
|
|
8574
|
+
this.closeDialog();
|
|
8575
|
+
return;
|
|
8576
|
+
}
|
|
8577
|
+
const sessions = link.activeSessions ?? 0;
|
|
8578
|
+
this.renderScrim(`
|
|
8579
|
+
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "hosted")} link?</h3>
|
|
8580
|
+
<p class="sub">It stops opening immediately and cannot be restored \u2014 there is no undo, and no way to
|
|
8581
|
+
bring the same URL back. Seats already bought through it keep their sale.</p>
|
|
8582
|
+
${sessions ? `<label class="slm-ch-radio">
|
|
8583
|
+
<input type="checkbox" data-ch-lk-endsessions />
|
|
8584
|
+
<span><b>Also end access for the ${sessions.toLocaleString()}
|
|
8585
|
+
buyer${sessions === 1 ? "" : "s"} already inside</b><span class="why">Leave this off and they can
|
|
8586
|
+
finish what they started; new visits are refused either way.</span></span>
|
|
8587
|
+
</label>` : ""}
|
|
8588
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
8589
|
+
<div class="foot">
|
|
8590
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8591
|
+
<button type="button" class="slm-btn danger" data-ch-lk-revoke>Revoke link</button>
|
|
8592
|
+
</div>`, (dialog) => {
|
|
8593
|
+
dialog.querySelector("[data-ch-lk-revoke]")?.addEventListener("click", () => {
|
|
8594
|
+
const end = dialog.querySelector("[data-ch-lk-endsessions]")?.checked ?? false;
|
|
8595
|
+
void this.revokeLink(state.channelId, link.id, end);
|
|
8596
|
+
});
|
|
8597
|
+
});
|
|
8598
|
+
}
|
|
8599
|
+
async revokeLink(channelId, linkId, endActiveSessions) {
|
|
8600
|
+
try {
|
|
8601
|
+
const res = await this.host.api.revokeAccessLink(
|
|
8602
|
+
this.host.eventKey,
|
|
8603
|
+
channelId,
|
|
8604
|
+
linkId,
|
|
8605
|
+
endActiveSessions
|
|
8606
|
+
);
|
|
8607
|
+
this.closeDialog();
|
|
8608
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8609
|
+
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");
|
|
8610
|
+
} catch (err) {
|
|
8611
|
+
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8612
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
7809
8615
|
// ---- compact detents ------------------------------------------------------
|
|
7810
8616
|
applySheetClasses() {
|
|
7811
8617
|
const root = this.host.root;
|
|
@@ -7884,9 +8690,17 @@ var LEGEND = [
|
|
|
7884
8690
|
{ key: "booked", label: "Booked", color: "#22a06b" },
|
|
7885
8691
|
{ key: "blocked", label: "Blocked", color: "#8b94ac" }
|
|
7886
8692
|
];
|
|
7887
|
-
var
|
|
8693
|
+
var MANAGER_CSS = `
|
|
7888
8694
|
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
|
|
7889
|
-
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius)
|
|
8695
|
+
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
8696
|
+
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
8697
|
+
borrowed from CHANNELS_CSS. The base cockpit animates whether or not
|
|
8698
|
+
Channels mode is in use, so owning its own tokens is what stops a token
|
|
8699
|
+
edit from silently changing only half the surface. Channels mode declares
|
|
8700
|
+
the identical values so an embed of it stays self-contained. */
|
|
8701
|
+
--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
8702
|
+
--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);
|
|
8703
|
+
--slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
|
|
7890
8704
|
.slm *{box-sizing:border-box;margin:0;padding:0}
|
|
7891
8705
|
.slm button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
|
7892
8706
|
.slm input{font:inherit}
|
|
@@ -7899,7 +8713,8 @@ var CSS2 = `
|
|
|
7899
8713
|
.slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7900
8714
|
.slm-live{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.12em;font-weight:800;color:var(--slm-muted)}
|
|
7901
8715
|
.slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
|
|
7902
|
-
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
8716
|
+
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
8717
|
+
animation:slm-pulse var(--slm-mo-ambient) infinite}
|
|
7903
8718
|
@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)}}
|
|
7904
8719
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
7905
8720
|
border-top:1px solid var(--slm-line)}
|
|
@@ -7908,7 +8723,11 @@ var CSS2 = `
|
|
|
7908
8723
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
7909
8724
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
7910
8725
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
7911
|
-
|
|
8726
|
+
/* The one playful moment this surface is allowed (\xA72 --mo-spring). It ran at
|
|
8727
|
+
.58s against a 200ms catalog, which read as a different design language from
|
|
8728
|
+
the dashboard tile it mirrors; Channels mode's own count bump is the same
|
|
8729
|
+
pattern and must stay in step with it. */
|
|
8730
|
+
.slm-kpi.changed b{animation:slm-kpi-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
7912
8731
|
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
7913
8732
|
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
7914
8733
|
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
@@ -7927,12 +8746,14 @@ var CSS2 = `
|
|
|
7927
8746
|
.slm-hud-chip{padding:6px 11px;border-radius:999px;font-size:12px;font-weight:700;background:var(--slm-surface);
|
|
7928
8747
|
border:1px solid var(--slm-line);color:var(--slm-text)}
|
|
7929
8748
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
7930
|
-
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
8749
|
+
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
8750
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
7931
8751
|
.slm-zoomhint.on{opacity:1}
|
|
7932
8752
|
.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));
|
|
7933
8753
|
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
7934
8754
|
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
7935
|
-
transition:opacity
|
|
8755
|
+
transition:opacity var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-base) var(--slm-mo-out);
|
|
8756
|
+
backdrop-filter:blur(10px)}
|
|
7936
8757
|
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
7937
8758
|
.slm.block-mode .slm-liveevent{top:52px}
|
|
7938
8759
|
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
@@ -7952,7 +8773,8 @@ var CSS2 = `
|
|
|
7952
8773
|
/* activity feed */
|
|
7953
8774
|
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
7954
8775
|
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
7955
|
-
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
8776
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
8777
|
+
animation:slm-in var(--slm-mo-quick) var(--slm-mo-out)}
|
|
7956
8778
|
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
7957
8779
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
7958
8780
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
@@ -8019,7 +8841,8 @@ var CSS2 = `
|
|
|
8019
8841
|
|
|
8020
8842
|
/* toast */
|
|
8021
8843
|
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
8022
|
-
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
8844
|
+
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
8845
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out);
|
|
8023
8846
|
background:var(--slm-surface);color:var(--slm-text);border:1px solid var(--slm-line);z-index:5}
|
|
8024
8847
|
.slm-toast.on{opacity:1}
|
|
8025
8848
|
.slm-toast.err{background:#c0392b;color:#fff;border-color:#c0392b}
|
|
@@ -8030,7 +8853,9 @@ var CSS2 = `
|
|
|
8030
8853
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
8031
8854
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
8032
8855
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
8033
|
-
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
8856
|
+
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
8857
|
+
background:var(--slm-surface)!important;text-align:left!important;
|
|
8858
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8034
8859
|
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
8035
8860
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
8036
8861
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
@@ -8049,7 +8874,8 @@ var CSS2 = `
|
|
|
8049
8874
|
.slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
|
|
8050
8875
|
/* sections: availability windows */
|
|
8051
8876
|
.slm-availlist{display:flex;flex-direction:column;gap:8px;margin:2px 0 12px}
|
|
8052
|
-
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
8877
|
+
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
8878
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),opacity var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8053
8879
|
.slm-availrow.zone{background:color-mix(in srgb,var(--slm-surface) 82%,#000)}
|
|
8054
8880
|
.slm-availrow.hidden{opacity:.62}.slm-availrow.closed{opacity:.82}
|
|
8055
8881
|
.slm-availhead{display:flex;align-items:center;gap:8px}
|
|
@@ -8089,16 +8915,29 @@ var CSS2 = `
|
|
|
8089
8915
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
8090
8916
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
8091
8917
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
8918
|
+
/* Reduced motion, as a BLANKET over the cockpit subtree rather than a list of
|
|
8919
|
+
selectors. The list this replaces named four animations and two transitions,
|
|
8920
|
+
and had silently fallen behind the stylesheet: the zoom hint, the toast and
|
|
8921
|
+
the availability rows all still animated for a user who had asked the OS for
|
|
8922
|
+
none. An enumerated list has to be edited every time a rule is added, and
|
|
8923
|
+
nothing fails when it isn't \u2014 so it drifts. This cannot.
|
|
8924
|
+
|
|
8925
|
+
Motion is removed, never the information it carried: Channels mode's own
|
|
8926
|
+
block substitutes static outlines for its shake and success states, and it
|
|
8927
|
+
stays authoritative for those. No JS here waits on animationend or
|
|
8928
|
+
transitionend, so cutting them outright strands no state. */
|
|
8092
8929
|
@media (prefers-reduced-motion:reduce){
|
|
8093
|
-
.slm
|
|
8094
|
-
|
|
8930
|
+
.slm,.slm *,.slm *::before,.slm *::after{
|
|
8931
|
+
animation:none!important;
|
|
8932
|
+
transition:none!important;
|
|
8933
|
+
scroll-behavior:auto!important}
|
|
8095
8934
|
}
|
|
8096
8935
|
${CHANNELS_CSS}`;
|
|
8097
8936
|
function injectStyle() {
|
|
8098
8937
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
8099
8938
|
const el = document.createElement("style");
|
|
8100
8939
|
el.id = STYLE_ID2;
|
|
8101
|
-
el.textContent =
|
|
8940
|
+
el.textContent = MANAGER_CSS;
|
|
8102
8941
|
document.head.appendChild(el);
|
|
8103
8942
|
}
|
|
8104
8943
|
function themeVars(theme) {
|
|
@@ -8158,6 +8997,11 @@ var SeatManager = class {
|
|
|
8158
8997
|
this.reconnectTimer = null;
|
|
8159
8998
|
this.attempt = 0;
|
|
8160
8999
|
this.closed = false;
|
|
9000
|
+
/** Mirrors the `live` root class, so the getter never has to read the DOM. */
|
|
9001
|
+
this.connectionStatus = "reconnecting";
|
|
9002
|
+
/** When the server last told us something. Stamped on accepted traffic only —
|
|
9003
|
+
* a socket that opens and says nothing has not refreshed anything. */
|
|
9004
|
+
this.lastMessageAt = null;
|
|
8161
9005
|
this.ready = false;
|
|
8162
9006
|
this.feed = [];
|
|
8163
9007
|
this.feedTimer = null;
|
|
@@ -8367,6 +9211,12 @@ var SeatManager = class {
|
|
|
8367
9211
|
},
|
|
8368
9212
|
worldToScreen: (point) => this.renderer?.worldToScreen(point) ?? null,
|
|
8369
9213
|
seatPixelSize: () => this.seatPixelSize(),
|
|
9214
|
+
isSeatDetail: () => this.renderer?.getRung?.() === "seats",
|
|
9215
|
+
showSectionOverview: () => {
|
|
9216
|
+
this.renderer?.clearSectionFocus();
|
|
9217
|
+
this.renderer?.setRung?.("sections");
|
|
9218
|
+
},
|
|
9219
|
+
focusSection: (sectionId) => this.renderer?.focusSection(sectionId),
|
|
8370
9220
|
isCompact: () => !!this.root?.classList.contains("compact"),
|
|
8371
9221
|
setMapInert: (inert) => {
|
|
8372
9222
|
this.mapHost.toggleAttribute("inert", inert);
|
|
@@ -8376,13 +9226,15 @@ var SeatManager = class {
|
|
|
8376
9226
|
onError: (err) => this.opts.onError?.(err)
|
|
8377
9227
|
};
|
|
8378
9228
|
}
|
|
8379
|
-
/**
|
|
8380
|
-
*
|
|
9229
|
+
/** Actual on-screen seat diameter, for the channel overlay's marks. The
|
|
9230
|
+
* renderer's base seat radius is 9 chart units; retaining the camera scale
|
|
9231
|
+
* (rather than capping it) keeps every preview paint aligned with the real
|
|
9232
|
+
* chart geometry at deep zoom. */
|
|
8381
9233
|
seatPixelSize() {
|
|
8382
9234
|
const rect = this.renderer?.getVisibleWorldRect?.();
|
|
8383
9235
|
const width = this.mapHost?.clientWidth ?? 0;
|
|
8384
9236
|
if (!rect?.width || !width) return 6;
|
|
8385
|
-
return Math.max(3,
|
|
9237
|
+
return Math.max(3, width / rect.width * 18);
|
|
8386
9238
|
}
|
|
8387
9239
|
/** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
|
|
8388
9240
|
setHeatOverlay(enabled) {
|
|
@@ -8558,6 +9410,16 @@ var SeatManager = class {
|
|
|
8558
9410
|
getControlRoomSnapshot(windowMinutes = this.trendWindowMinutes) {
|
|
8559
9411
|
return this.setTrendWindow(windowMinutes);
|
|
8560
9412
|
}
|
|
9413
|
+
/**
|
|
9414
|
+
* The realtime link's current state and the "as of" behind it.
|
|
9415
|
+
*
|
|
9416
|
+
* Pair with `onConnectionChange` for the edges: a host that mounts after a
|
|
9417
|
+
* drop, or re-reads on tab focus, needs to be able to ASK rather than wait
|
|
9418
|
+
* for the next transition that may never come.
|
|
9419
|
+
*/
|
|
9420
|
+
getConnection() {
|
|
9421
|
+
return { status: this.connectionStatus, lastMessageAt: this.lastMessageAt };
|
|
9422
|
+
}
|
|
8561
9423
|
getLog(opts = {}) {
|
|
8562
9424
|
return this.api.log(this.key, opts);
|
|
8563
9425
|
}
|
|
@@ -8622,6 +9484,10 @@ var SeatManager = class {
|
|
|
8622
9484
|
onSelect: (seat) => this.handleSeatSelect(seat),
|
|
8623
9485
|
onDeselect: () => this.syncSelection(),
|
|
8624
9486
|
onMarquee: () => this.syncSelection(),
|
|
9487
|
+
onSectionTap: (sectionId) => {
|
|
9488
|
+
this.renderer?.focusSection(sectionId);
|
|
9489
|
+
this.channels?.handleSectionFocus(sectionId);
|
|
9490
|
+
},
|
|
8625
9491
|
onViewChange: () => {
|
|
8626
9492
|
this.updateZoomHint();
|
|
8627
9493
|
this.channels?.handleViewChange();
|
|
@@ -8632,10 +9498,11 @@ var SeatManager = class {
|
|
|
8632
9498
|
this.applyHeatOverlay();
|
|
8633
9499
|
this.updateZoomHint();
|
|
8634
9500
|
}
|
|
8635
|
-
/** Block
|
|
8636
|
-
*
|
|
9501
|
+
/** Block always uses a marquee. Channels only enables its marquee after the
|
|
9502
|
+
* organizer deliberately chooses Assign seats; Pan map keeps desktop drag
|
|
9503
|
+
* available for large charts. */
|
|
8637
9504
|
isBulkSelectMode() {
|
|
8638
|
-
return this.mode === "block" || this.mode === "channels" && this.channels?.
|
|
9505
|
+
return this.mode === "block" || this.mode === "channels" && this.channels?.usesMarqueeSelection() === true;
|
|
8639
9506
|
}
|
|
8640
9507
|
/**
|
|
8641
9508
|
* Block never touches held or booked inventory, so it cannot select it.
|
|
@@ -8645,7 +9512,7 @@ var SeatManager = class {
|
|
|
8645
9512
|
*/
|
|
8646
9513
|
selectableStatuses() {
|
|
8647
9514
|
if (this.mode === "block") return ["free", "not_for_sale"];
|
|
8648
|
-
if (this.mode === "inspect" || this.isBulkSelectMode()) {
|
|
9515
|
+
if (this.mode === "inspect" || this.isBulkSelectMode() || this.mode === "channels" && this.channels?.canSelect() === true) {
|
|
8649
9516
|
return ["free", "held", "booked", "not_for_sale"];
|
|
8650
9517
|
}
|
|
8651
9518
|
return [];
|
|
@@ -8748,6 +9615,7 @@ var SeatManager = class {
|
|
|
8748
9615
|
return;
|
|
8749
9616
|
}
|
|
8750
9617
|
if (!msg || typeof msg !== "object") return;
|
|
9618
|
+
this.lastMessageAt = Date.now();
|
|
8751
9619
|
const m = msg;
|
|
8752
9620
|
if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
|
|
8753
9621
|
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
@@ -8804,6 +9672,7 @@ var SeatManager = class {
|
|
|
8804
9672
|
const objs = await this.api.objects(this.key);
|
|
8805
9673
|
this.applySnapshot(objs.seats);
|
|
8806
9674
|
this.updateEffectiveAvailability(objs.hidden, objs.closed);
|
|
9675
|
+
this.lastMessageAt = Date.now();
|
|
8807
9676
|
} catch {
|
|
8808
9677
|
}
|
|
8809
9678
|
}
|
|
@@ -9278,6 +10147,14 @@ var SeatManager = class {
|
|
|
9278
10147
|
this.root?.classList.toggle("live", on);
|
|
9279
10148
|
if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
|
|
9280
10149
|
this.paintMonitorInsights();
|
|
10150
|
+
const next = on ? "live" : "reconnecting";
|
|
10151
|
+
if (next === this.connectionStatus) return;
|
|
10152
|
+
this.connectionStatus = next;
|
|
10153
|
+
try {
|
|
10154
|
+
this.opts.onConnectionChange?.(this.getConnection());
|
|
10155
|
+
} catch (err) {
|
|
10156
|
+
this.opts.onError?.(err);
|
|
10157
|
+
}
|
|
9281
10158
|
}
|
|
9282
10159
|
updateZoomHint() {
|
|
9283
10160
|
const hint = this.els.zoomhint;
|
|
@@ -10023,6 +10900,7 @@ var SeatManager = class {
|
|
|
10023
10900
|
};
|
|
10024
10901
|
// Annotate the CommonJS export names for ESM import in node:
|
|
10025
10902
|
0 && (module.exports = {
|
|
10903
|
+
ACCESS_LINK_DEFAULTS,
|
|
10026
10904
|
ApiError,
|
|
10027
10905
|
BuyerAccessContext,
|
|
10028
10906
|
BuyerAccessUnavailableError,
|
|
@@ -10038,6 +10916,10 @@ var SeatManager = class {
|
|
|
10038
10916
|
SeatingChart,
|
|
10039
10917
|
accessIntentLabel,
|
|
10040
10918
|
accessLine,
|
|
10919
|
+
accessLinkBadge,
|
|
10920
|
+
accessLinkErrorCopy,
|
|
10921
|
+
accessLinkIsLive,
|
|
10922
|
+
accessLinkPolicyLines,
|
|
10041
10923
|
attachPickerFrame,
|
|
10042
10924
|
bucketRows,
|
|
10043
10925
|
bucketRowsHtml,
|