@seatlayer/js 0.43.2 → 0.45.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 +536 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +240 -31
- package/dist/index.d.ts +240 -31
- package/dist/index.js +533 -97
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -408,6 +408,7 @@ __export(index_exports, {
|
|
|
408
408
|
SeatManager: () => SeatManager,
|
|
409
409
|
SeatPicker: () => SeatPicker,
|
|
410
410
|
SeatingChart: () => SeatingChart,
|
|
411
|
+
accessIntentDescription: () => accessIntentDescription,
|
|
411
412
|
accessIntentLabel: () => accessIntentLabel,
|
|
412
413
|
accessLine: () => accessLine,
|
|
413
414
|
accessLinkBadge: () => accessLinkBadge,
|
|
@@ -420,6 +421,8 @@ __export(index_exports, {
|
|
|
420
421
|
createBuyerAccessContext: () => createBuyerAccessContext,
|
|
421
422
|
createControllerSink: () => createControllerSink,
|
|
422
423
|
dropReviewRows: () => dropReviewRows,
|
|
424
|
+
intentForbidsCopy: () => intentForbidsCopy,
|
|
425
|
+
intentSwitchBlockedCopy: () => intentSwitchBlockedCopy,
|
|
423
426
|
isPublicChannelId: () => isPublicChannelId,
|
|
424
427
|
markerLetter: () => markerLetter,
|
|
425
428
|
markerOf: () => markerOf,
|
|
@@ -6901,13 +6904,59 @@ function retryAfterCopy(details) {
|
|
|
6901
6904
|
}
|
|
6902
6905
|
function accessLine(access) {
|
|
6903
6906
|
if (!access || !access.intent) return "\u2014";
|
|
6904
|
-
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : "
|
|
6907
|
+
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : access.intent === "internal" ? "Your staff sell these" : "Protected reserve";
|
|
6905
6908
|
const grants = access.hasActiveGrants ? "in use now" : access.lastMintAt ? `last used ${new Date(access.lastMintAt).toLocaleDateString()}` : null;
|
|
6906
6909
|
const detail = access.detail ?? grants;
|
|
6907
6910
|
return detail ? `${base} \xB7 ${detail}` : base;
|
|
6908
6911
|
}
|
|
6909
6912
|
function accessIntentLabel(intent) {
|
|
6910
|
-
return intent === "internal" ? "
|
|
6913
|
+
return intent === "internal" ? "Sell through your own staff" : intent === "server" ? "Integrate a website or app" : intent === "hosted_link" ? "Sell with a buyer link" : "Keep as protected reserve";
|
|
6914
|
+
}
|
|
6915
|
+
function accessIntentDescription(intent) {
|
|
6916
|
+
switch (intent) {
|
|
6917
|
+
case "internal":
|
|
6918
|
+
return "Only your own box office can sell these seats, through your secret key. Buyer links and website integrations are refused.";
|
|
6919
|
+
case "server":
|
|
6920
|
+
return "Your website's backend mints each buyer a short-lived session for these seats. Buyer links are refused; the code lives on the Embed page.";
|
|
6921
|
+
case "hosted_link":
|
|
6922
|
+
return "SeatLayer makes a link you send to a named group. They open it and buy only these seats. No other route can sell them.";
|
|
6923
|
+
default:
|
|
6924
|
+
return "Nobody can buy these seats. Every way of letting a buyer in \u2014 a buyer link, your website, even your own staff \u2014 is refused while this is the route. The seats stay out of public sale.";
|
|
6925
|
+
}
|
|
6926
|
+
}
|
|
6927
|
+
function intentForRoute(route) {
|
|
6928
|
+
return route === "hosted_link" ? "hosted_link" : route === "server" ? "server" : route === "staff" ? "internal" : null;
|
|
6929
|
+
}
|
|
6930
|
+
function intentForbidsCopy(details) {
|
|
6931
|
+
const current = parseIntent(details?.accessIntent);
|
|
6932
|
+
const wanted = intentForRoute(details?.route);
|
|
6933
|
+
const head = `This channel is set to "${accessIntentLabel(current)}"`;
|
|
6934
|
+
return wanted ? `${head}, so it cannot do that. Switch it to "${accessIntentLabel(wanted)}" first.` : `${head}, so it cannot do that. Choose a different route for this channel first.`;
|
|
6935
|
+
}
|
|
6936
|
+
function parseIntent(value) {
|
|
6937
|
+
return value === "internal" || value === "server" || value === "hosted_link" || value === "none" ? value : "none";
|
|
6938
|
+
}
|
|
6939
|
+
function plural(count, one, many) {
|
|
6940
|
+
return `${count.toLocaleString()} ${count === 1 ? one : many}`;
|
|
6941
|
+
}
|
|
6942
|
+
function intentSwitchBlockedCopy(details) {
|
|
6943
|
+
const links = Math.max(0, details?.liveAccessLinks ?? 0);
|
|
6944
|
+
const sessions = Math.max(0, details?.activeSessions ?? 0);
|
|
6945
|
+
const from = parseIntent(details?.from);
|
|
6946
|
+
const to = parseIntent(details?.to);
|
|
6947
|
+
const live = [
|
|
6948
|
+
links ? plural(links, "buyer link is live", "buyer links are live") : null,
|
|
6949
|
+
sessions ? plural(sessions, "buyer is in a checkout", "buyers are in a checkout") : null
|
|
6950
|
+
].filter(Boolean).join(", and ");
|
|
6951
|
+
const headline = `${live || "Buyers are inside this channel"} on "${accessIntentLabel(from)}". Moving it to "${accessIntentLabel(to)}" changes what happens to them.`;
|
|
6952
|
+
const consequences = [];
|
|
6953
|
+
if (links) {
|
|
6954
|
+
consequences.push(`${plural(links, "buyer link closes", "buyer links close")} immediately. Anyone who has not opened it yet never will \u2014 send a new link if you still need one.`);
|
|
6955
|
+
}
|
|
6956
|
+
if (sessions) {
|
|
6957
|
+
consequences.push(`${plural(sessions, "buyer who is already in a checkout keeps", "buyers who are already in a checkout keep")} their seats and can finish paying. Nobody is thrown out. No new buyers come in this way, so the old route empties on its own within 12 hours.`);
|
|
6958
|
+
}
|
|
6959
|
+
return { headline, consequences };
|
|
6911
6960
|
}
|
|
6912
6961
|
var ACCESS_LINK_DEFAULTS = {
|
|
6913
6962
|
maxRedemptions: 100,
|
|
@@ -6972,6 +7021,11 @@ function accessLinkErrorCopy(err) {
|
|
|
6972
7021
|
return "That link is no longer active, so it cannot be rotated or revoked.";
|
|
6973
7022
|
case "channel_unavailable":
|
|
6974
7023
|
return "This channel is paused or archived, so it cannot let new buyers in. Resume it first.";
|
|
7024
|
+
// The channel declares a different sale route. The UI declares `hosted_link`
|
|
7025
|
+
// before it creates, so reaching this means the declaration itself was
|
|
7026
|
+
// refused or raced — say which route is in the way, not the code.
|
|
7027
|
+
case "channel_access_intent_forbids":
|
|
7028
|
+
return intentForbidsCopy(err?.details);
|
|
6975
7029
|
case "end_active_sessions_required":
|
|
6976
7030
|
return "Choose what happens to the buyers who already came in through this link.";
|
|
6977
7031
|
case "not_found":
|
|
@@ -7205,13 +7259,31 @@ var ManageApi = class {
|
|
|
7205
7259
|
const qs = params.toString();
|
|
7206
7260
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/preview${qs ? `?${qs}` : ""}`);
|
|
7207
7261
|
}
|
|
7208
|
-
/**
|
|
7209
|
-
*
|
|
7210
|
-
*
|
|
7211
|
-
|
|
7262
|
+
/**
|
|
7263
|
+
* Choose which sale route this channel opens.
|
|
7264
|
+
*
|
|
7265
|
+
* Since the server's 2026-08-06 change this is AUTHORIZATION, not a label:
|
|
7266
|
+
* exactly one of the four routes may mint buyer access for the channel and the
|
|
7267
|
+
* other three refuse with 409 `channel_access_intent_forbids`. The default is
|
|
7268
|
+
* `none`, which refuses all four — so a route has to be declared before any
|
|
7269
|
+
* buyer-facing action on the channel can succeed.
|
|
7270
|
+
*
|
|
7271
|
+
* Switching the route while buyers are already inside the current one is
|
|
7272
|
+
* refused with 409 `channel_intent_switch_blocked`, whose `details` name what
|
|
7273
|
+
* is live (`liveAccessLinks`, `activeSessions`). Retry with
|
|
7274
|
+
* `acknowledgeLiveAccess: true`: hosted links on the channel are revoked,
|
|
7275
|
+
* while sessions already minted keep their holds and drain on their own.
|
|
7276
|
+
* `intentSwitch` is present on the response ONLY when the switch disturbed
|
|
7277
|
+
* something, so the ordinary case stays the two-key body it has always been.
|
|
7278
|
+
*/
|
|
7279
|
+
setChannelAccessIntent(key, channelId, accessIntent, opts = {}) {
|
|
7212
7280
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/channels/${encodeURIComponent(channelId)}`, {
|
|
7213
7281
|
method: "PATCH",
|
|
7214
|
-
body: {
|
|
7282
|
+
body: {
|
|
7283
|
+
accessIntent,
|
|
7284
|
+
...opts.acknowledgeLiveAccess ? { acknowledgeLiveAccess: true } : {},
|
|
7285
|
+
...opts.reason ? { reason: opts.reason } : {}
|
|
7286
|
+
}
|
|
7215
7287
|
});
|
|
7216
7288
|
}
|
|
7217
7289
|
// ---- hosted access links (M8) ----
|
|
@@ -7226,8 +7298,11 @@ var ManageApi = class {
|
|
|
7226
7298
|
* Platform bounds are enforced server-side and reported as 422 with the rule
|
|
7227
7299
|
* spelled out in `ManageApiError.serverMessage`.
|
|
7228
7300
|
*
|
|
7229
|
-
*
|
|
7230
|
-
* `hosted_link
|
|
7301
|
+
* NOT a side effect any more. This used to SET the channel's access intent to
|
|
7302
|
+
* `hosted_link`; since 2026-08-06 it REQUIRES it, and a channel declaring any
|
|
7303
|
+
* other route refuses with 409 `channel_access_intent_forbids`. Callers must
|
|
7304
|
+
* declare the route first — `ChannelsMode` does exactly that before it
|
|
7305
|
+
* creates, so a first buyer link on a fresh channel is still one gesture.
|
|
7231
7306
|
*/
|
|
7232
7307
|
createAccessLink(key, channelId, input = {}) {
|
|
7233
7308
|
return this.auth(
|
|
@@ -7384,7 +7459,23 @@ var CHANNELS_CSS = (
|
|
|
7384
7459
|
.slm-ch-counts b.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
7385
7460
|
@keyframes slm-ch-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08)}}
|
|
7386
7461
|
.slm-ch-access{margin-top:6px;font-size:10.5px;color:var(--slm-muted)}
|
|
7387
|
-
|
|
7462
|
+
/* A row that opens its channel must READ as pressable, and be one for a keyboard
|
|
7463
|
+
too. It is a role="button" div rather than a <button> because the \u22EF control
|
|
7464
|
+
lives inside it, and a button inside a button is not valid HTML. */
|
|
7465
|
+
.slm-ch-row.open{cursor:pointer}
|
|
7466
|
+
.slm-ch-row.open:hover{border-color:var(--slm-accent);background:color-mix(in srgb,var(--slm-accent) 6%,var(--slm-surface))}
|
|
7467
|
+
.slm-ch-row.open:focus-visible{outline:2px solid var(--slm-accent);outline-offset:2px}
|
|
7468
|
+
.slm-ch-row.open:active{border-color:var(--slm-accent)}
|
|
7469
|
+
.slm-ch-more{flex:none;color:var(--slm-muted);font-weight:800;padding:0 4px;min-height:28px;border-radius:6px}
|
|
7470
|
+
.slm-ch-more:hover{color:var(--slm-text)}
|
|
7471
|
+
.slm-ch-more:focus-visible{outline:2px solid var(--slm-accent);outline-offset:1px;color:var(--slm-text)}
|
|
7472
|
+
.slm-ch-menu{display:flex;flex-direction:column;gap:8px}
|
|
7473
|
+
.slm-ch-menu .slm-btn{width:100%}
|
|
7474
|
+
/* loading is a state, never a flash of empty: same slot, visibly working */
|
|
7475
|
+
.slm-ch-busy{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
7476
|
+
.slm-ch-busy::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--slm-accent);flex:none;
|
|
7477
|
+
animation:slm-ch-pulse var(--slm-mo-ambient) var(--slm-mo-in-out) infinite}
|
|
7478
|
+
@keyframes slm-ch-pulse{0%,100%{opacity:.25;transform:scale(.7)}50%{opacity:1;transform:scale(1)}}
|
|
7388
7479
|
.slm-ch-selsrc{display:flex;flex-direction:column;gap:5px;margin:8px 0 12px}
|
|
7389
7480
|
.slm-ch-selsrc-row{display:flex;align-items:center;gap:8px;font-size:12px;font-variant-numeric:tabular-nums}
|
|
7390
7481
|
.slm-ch-selsrc-row .mk{width:15px;height:15px;border-radius:4px;display:grid;place-items:center;font-size:8px;
|
|
@@ -7394,10 +7485,14 @@ var CHANNELS_CSS = (
|
|
|
7394
7485
|
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
7395
7486
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
7396
7487
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
7397
|
-
/* distribute
|
|
7488
|
+
/* distribute routes \u2014 four choices, each with the one sentence that explains it.
|
|
7489
|
+
The current one is NAMED in its own card, never signalled by colour alone. */
|
|
7398
7490
|
.slm-ch-dist{display:flex;flex-direction:column;gap:6px;padding:12px;margin-bottom:8px;border:1px solid var(--slm-line);
|
|
7399
7491
|
border-radius:10px;background:var(--slm-surface)}
|
|
7400
|
-
.slm-ch-dist
|
|
7492
|
+
.slm-ch-dist.on{border-color:var(--slm-accent,#8b7cf6)}
|
|
7493
|
+
.slm-ch-dist b{font-size:13px;font-weight:800;display:flex;align-items:center;gap:8px;justify-content:space-between}
|
|
7494
|
+
.slm-ch-dist b .cur{font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
|
|
7495
|
+
color:var(--slm-accent,#8b7cf6);white-space:nowrap}
|
|
7401
7496
|
.slm-ch-dist .why{color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
7402
7497
|
.slm-ch-dist .slm-btn{width:100%;margin-top:2px}
|
|
7403
7498
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
@@ -7516,6 +7611,7 @@ var CHANNELS_CSS = (
|
|
|
7516
7611
|
.slm-ch-meter i,.slm-ch-radio{transition:none!important}
|
|
7517
7612
|
.slm-ch-staged.shake,.slm-ch-tick,.slm-ch-bucket,.slm-ch-scrim,.slm-ch-dialog,
|
|
7518
7613
|
.slm-ch-counts b.bump,.slm-ch-selnum.bump{animation:none!important}
|
|
7614
|
+
.slm-ch-busy::before{animation:none!important;opacity:1}
|
|
7519
7615
|
.slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
|
|
7520
7616
|
.slm-ch-staged.done{outline:2px solid #5bd39b;outline-offset:2px}
|
|
7521
7617
|
}
|
|
@@ -7576,6 +7672,18 @@ var ChannelsMode = class {
|
|
|
7576
7672
|
this.focusedSectionId = null;
|
|
7577
7673
|
this.showArchived = false;
|
|
7578
7674
|
this.detailChannelId = null;
|
|
7675
|
+
/**
|
|
7676
|
+
* Whether the organizer has asked to assign seats.
|
|
7677
|
+
*
|
|
7678
|
+
* The list rail leads with the CHANNELS. The assignment tooling — destination
|
|
7679
|
+
* picker plus five select-by routes — used to paint unconditionally above that
|
|
7680
|
+
* list, so a first-time organizer met a workbench for a job they had not asked
|
|
7681
|
+
* to do, with the thing they came for pushed below the fold. It is now
|
|
7682
|
+
* disclosed on intent: the "Assign seats to a channel" action under the list,
|
|
7683
|
+
* the map's own "Assign seats" segment, or simply having a selection. The
|
|
7684
|
+
* selection rail always shows the tools, because there the intent is proven.
|
|
7685
|
+
*/
|
|
7686
|
+
this.assignOpen = false;
|
|
7579
7687
|
this.targetChannelId = "";
|
|
7580
7688
|
this.conflict = false;
|
|
7581
7689
|
this.dialog = null;
|
|
@@ -7602,8 +7710,16 @@ var ChannelsMode = class {
|
|
|
7602
7710
|
this.previewAudience = [];
|
|
7603
7711
|
this.previewIncludePublic = false;
|
|
7604
7712
|
this.previewProjection = null;
|
|
7605
|
-
|
|
7606
|
-
|
|
7713
|
+
/**
|
|
7714
|
+
* The buyer-preview read, as one honest state rather than a boolean.
|
|
7715
|
+
*
|
|
7716
|
+
* `previewSupported: boolean | null` could say "this worker has no projection
|
|
7717
|
+
* route", but it could not say "the read is in flight" or "the read failed" —
|
|
7718
|
+
* both of those painted an audience picker with no result underneath, which
|
|
7719
|
+
* reads as "this audience can buy nothing". Loading and error are now states
|
|
7720
|
+
* of their own, and the error one offers a retry.
|
|
7721
|
+
*/
|
|
7722
|
+
this.previewState = "idle";
|
|
7607
7723
|
this.pollTimer = null;
|
|
7608
7724
|
this.layer = null;
|
|
7609
7725
|
this.canvas = null;
|
|
@@ -7641,6 +7757,7 @@ var ChannelsMode = class {
|
|
|
7641
7757
|
if (this.active) return;
|
|
7642
7758
|
this.active = true;
|
|
7643
7759
|
this.mapIntent = "pan";
|
|
7760
|
+
this.assignOpen = false;
|
|
7644
7761
|
this.focusedSectionId = null;
|
|
7645
7762
|
this.assignmentRowsCache = null;
|
|
7646
7763
|
this.railHtml = null;
|
|
@@ -8150,6 +8267,7 @@ var ChannelsMode = class {
|
|
|
8150
8267
|
this.view = view;
|
|
8151
8268
|
if (view === "inspect") {
|
|
8152
8269
|
this.previewProjection = null;
|
|
8270
|
+
this.previewState = "idle";
|
|
8153
8271
|
this.setBanner(false);
|
|
8154
8272
|
} else {
|
|
8155
8273
|
if (!this.previewAudience.length) {
|
|
@@ -8167,6 +8285,9 @@ var ChannelsMode = class {
|
|
|
8167
8285
|
const audience = [...this.previewAudience];
|
|
8168
8286
|
const names = audience.map((id) => this.nameOf(id) ?? PUBLIC_CHANNEL_NAME).join(" + ");
|
|
8169
8287
|
this.setBanner(true, names);
|
|
8288
|
+
this.previewProjection = null;
|
|
8289
|
+
this.previewState = "loading";
|
|
8290
|
+
if (this.active) this.paintRail();
|
|
8170
8291
|
try {
|
|
8171
8292
|
this.previewProjection = await this.host.api.channelPreview(this.host.eventKey, audience, {
|
|
8172
8293
|
// Naming Public sale as the audience IS asking for public inventory; the
|
|
@@ -8174,14 +8295,14 @@ var ChannelsMode = class {
|
|
|
8174
8295
|
// this the request would resolve to an empty scope and 422.
|
|
8175
8296
|
includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
|
|
8176
8297
|
});
|
|
8177
|
-
this.
|
|
8298
|
+
this.previewState = "ready";
|
|
8178
8299
|
const eligibleSeats = this.previewProjection.available === false ? void 0 : this.previewProjection.counts?.eligible ?? this.previewProjection.eligible?.length;
|
|
8179
8300
|
this.setBanner(true, names, eligibleSeats);
|
|
8180
8301
|
} catch (err) {
|
|
8181
8302
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
8182
|
-
this.
|
|
8303
|
+
this.previewState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
|
|
8183
8304
|
this.previewProjection = null;
|
|
8184
|
-
if (this.
|
|
8305
|
+
if (this.previewState === "error") this.host.onError(err);
|
|
8185
8306
|
}
|
|
8186
8307
|
if (this.active) {
|
|
8187
8308
|
this.paintRail();
|
|
@@ -8222,13 +8343,14 @@ var ChannelsMode = class {
|
|
|
8222
8343
|
}
|
|
8223
8344
|
if (this.loading && !this.list) {
|
|
8224
8345
|
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8225
|
-
<div class="slm-
|
|
8346
|
+
<div class="slm-ch-busy" role="status" data-ch-state="list-loading">Loading channels and allocations\u2026</div>`);
|
|
8226
8347
|
return;
|
|
8227
8348
|
}
|
|
8228
8349
|
if (!this.list && this.loadError) {
|
|
8229
8350
|
if (this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8230
|
-
<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8231
|
-
<span><b>Couldn't load sales channels.</b>
|
|
8351
|
+
<div class="slm-ch-alert err" role="alert" data-ch-state="list-error"><span>\u26A0</span>
|
|
8352
|
+
<span><b>Couldn't load sales channels.</b> This event may well have channels \u2014 we could not read them.
|
|
8353
|
+
Everything else on this event still works.
|
|
8232
8354
|
<button type="button" data-ch-act="retry">Try again</button></span></div>`)) {
|
|
8233
8355
|
rail.querySelector('[data-ch-act="retry"]')?.addEventListener("click", () => {
|
|
8234
8356
|
void this.refresh();
|
|
@@ -8287,10 +8409,13 @@ var ChannelsMode = class {
|
|
|
8287
8409
|
const marker = this.markerFor(channel.id);
|
|
8288
8410
|
const badgeKind = opts.builtin ? "builtin" : channel.state;
|
|
8289
8411
|
const dim = channel.state === "paused" || channel.state === "archived" ? " dim" : "";
|
|
8290
|
-
const
|
|
8291
|
-
const
|
|
8292
|
-
|
|
8293
|
-
|
|
8412
|
+
const opens = !opts.builtin && this.caps.manage && this.detailChannelId !== channel.id;
|
|
8413
|
+
const cls = `slm-ch-row${opts.builtin ? " public" : ""}${channel.state === "archived" ? " archived" : ""}${this.detailChannelId === channel.id ? " on" : ""}${opens ? " open" : ""}`;
|
|
8414
|
+
const more = !opts.builtin && this.caps.manage ? `<button type="button" class="slm-ch-more" data-ch-menu="${esc(channel.id)}"
|
|
8415
|
+
aria-label="More actions for ${esc(channel.name)}" aria-haspopup="dialog">\u22EF</button>` : "";
|
|
8416
|
+
const rowAttrs = opens ? ` role="button" tabindex="0" data-ch-open="${esc(channel.id)}"
|
|
8417
|
+
aria-label="Open ${esc(channel.name)}"` : "";
|
|
8418
|
+
return `<div class="${cls}"${rowAttrs}>
|
|
8294
8419
|
<span class="slm-ch-head">
|
|
8295
8420
|
<span class="slm-ch-mk${dim}" style="background:${esc(marker.color)}" aria-hidden="true">${esc(marker.letter)}</span>
|
|
8296
8421
|
<span class="slm-ch-name">${esc(channel.name)}</span>
|
|
@@ -8304,41 +8429,68 @@ var ChannelsMode = class {
|
|
|
8304
8429
|
listRailHtml() {
|
|
8305
8430
|
const list = this.list;
|
|
8306
8431
|
const archivedCount = list.channels.filter((channel) => channel.state === "archived").length;
|
|
8432
|
+
const visible = list.channels.filter((channel) => this.showArchived || channel.state !== "archived");
|
|
8307
8433
|
const rows = [
|
|
8308
8434
|
this.channelRowHtml(list.publicSale, { builtin: true }),
|
|
8309
|
-
...
|
|
8435
|
+
...visible.map((channel, index) => this.channelRowHtml(channel, { index }))
|
|
8310
8436
|
].join("");
|
|
8437
|
+
const empty = visible.length ? "" : `<p class="slm-note" data-ch-state="list-empty">${archivedCount && !this.showArchived ? `No open channels \u2014 every seat is on public sale. ${archivedCount.toLocaleString()} archived channel${archivedCount === 1 ? " is" : "s are"} hidden below.` : "No private channels yet \u2014 every seat is on public sale."}</p>`;
|
|
8311
8438
|
const create = this.caps.manage ? `<button type="button" class="slm-btn ghost" style="width:100%" data-ch-act="create">+ Create channel</button>` : "";
|
|
8312
8439
|
const readOnly = this.caps.manage ? "" : `<p class="slm-note">You can see how inventory is allocated. Changing it needs channel-management permission.</p>`;
|
|
8313
8440
|
return `
|
|
8314
|
-
${this.caps.manage ? this.assignmentToolsHtml() : ""}
|
|
8315
8441
|
<p class="slm-eyebrow">Sales channels</p>
|
|
8316
8442
|
<p class="slm-hint">Channel colours and names are only visible to organizers, never to buyers.</p>
|
|
8317
8443
|
<div class="slm-ch-list">${rows}</div>
|
|
8444
|
+
${empty}
|
|
8318
8445
|
${create}
|
|
8319
8446
|
${readOnly}
|
|
8447
|
+
${this.assignEntryHtml()}
|
|
8320
8448
|
<p class="slm-note" style="margin-top:10px">
|
|
8321
8449
|
<button type="button" class="slm-linkbtn" data-ch-act="toggle-archived" aria-pressed="${this.showArchived}"
|
|
8322
8450
|
style="text-align:left">${this.showArchived ? "Hide" : "Show"} archived${archivedCount ? ` (${archivedCount})` : ""}</button>
|
|
8323
8451
|
</p>`;
|
|
8324
8452
|
}
|
|
8453
|
+
/**
|
|
8454
|
+
* The assignment entry point on the list rail.
|
|
8455
|
+
*
|
|
8456
|
+
* Collapsed it is ONE plain-language action, so the channels the organizer came
|
|
8457
|
+
* for stay at the top of the panel. Opened it is the same tool set that has
|
|
8458
|
+
* always worked, in the same order, plus the way back out — and opening it is
|
|
8459
|
+
* also what the map's "Assign seats" segment does, so the two routes into the
|
|
8460
|
+
* job cannot disagree about whether it is running.
|
|
8461
|
+
*/
|
|
8462
|
+
assignEntryHtml() {
|
|
8463
|
+
if (!this.caps.manage) return "";
|
|
8464
|
+
if (!this.assignOpen) {
|
|
8465
|
+
return `
|
|
8466
|
+
<button type="button" class="slm-btn ghost" style="width:100%;margin-top:8px"
|
|
8467
|
+
data-ch-act="assign-open" aria-expanded="false">Assign seats to a channel</button>
|
|
8468
|
+
<p class="slm-note">Move whole sections, rows, a dragged area or single seats out of public sale
|
|
8469
|
+
and into a channel. Nothing moves until you review and apply.</p>`;
|
|
8470
|
+
}
|
|
8471
|
+
return `<div style="margin-top:8px">${this.assignmentToolsHtml({ collapsible: true })}</div>`;
|
|
8472
|
+
}
|
|
8325
8473
|
/**
|
|
8326
8474
|
* Destination-first assignment controls.
|
|
8327
8475
|
*
|
|
8328
8476
|
* These used to live only inside the selection rail, which meant every route
|
|
8329
8477
|
* into them was gated behind "select a seat on the map first" — the section,
|
|
8330
8478
|
* row and category choosers were invisible until the organizer had already
|
|
8331
|
-
* done the work by hand. They
|
|
8332
|
-
*
|
|
8333
|
-
*
|
|
8479
|
+
* done the work by hand. They are still reachable before a seat is selected,
|
|
8480
|
+
* but they are no longer the first thing in the panel: on the list rail they
|
|
8481
|
+
* are disclosed by `assignEntryHtml`, and there they carry a way back out
|
|
8482
|
+
* (`collapsible`). In the selection rail there is nothing to disclose — a
|
|
8483
|
+
* selection IS the intent — so they paint unconditionally and without Done.
|
|
8334
8484
|
*/
|
|
8335
|
-
assignmentToolsHtml() {
|
|
8485
|
+
assignmentToolsHtml(opts = {}) {
|
|
8336
8486
|
const options = this.assignableChannels().map((channel) => `<option value="${esc(channel.id)}"${channel.id === this.targetChannelId ? " selected" : ""}>${esc(channel.name)}</option>`).join("");
|
|
8337
8487
|
const sections = this.host.sections().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-sections">Sections</button>' : "";
|
|
8338
8488
|
const rows = this.assignmentRows().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-rows">Rows</button>' : "";
|
|
8339
8489
|
const dragClass = this.mapIntent === "assign" ? "slm-btn" : "slm-btn ghost";
|
|
8490
|
+
const done = opts.collapsible ? `<button type="button" class="slm-linkbtn" data-ch-act="assign-close"
|
|
8491
|
+
aria-expanded="true" style="float:right;font-weight:800">Done</button>` : "";
|
|
8340
8492
|
return `
|
|
8341
|
-
<p class="slm-eyebrow"
|
|
8493
|
+
<p class="slm-eyebrow">${done}Assign inventory</p>
|
|
8342
8494
|
<div class="slm-field">
|
|
8343
8495
|
<label for="slm-ch-target">Assign to</label>
|
|
8344
8496
|
<select class="slm-select" id="slm-ch-target" data-ch-target>${options}</select>
|
|
@@ -8388,8 +8540,16 @@ var ChannelsMode = class {
|
|
|
8388
8540
|
<p class="slm-note">The seat list offers the same selection with checkboxes for keyboard and screen-reader use.</p>`;
|
|
8389
8541
|
}
|
|
8390
8542
|
detailRailHtml(channelId) {
|
|
8543
|
+
const back = `<p class="slm-eyebrow">
|
|
8544
|
+
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
8545
|
+
</p>`;
|
|
8391
8546
|
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
8392
|
-
if (!channel)
|
|
8547
|
+
if (!channel) {
|
|
8548
|
+
return `${back}
|
|
8549
|
+
<div class="slm-ch-alert warn" role="status" data-ch-state="detail-gone"><span>\u2139</span>
|
|
8550
|
+
<span><b>This channel is no longer on this event.</b> It was archived or removed while you had it open.
|
|
8551
|
+
${this.showArchived ? "" : "Archived channels are hidden \u2014 use \u201CShow archived\u201D on the list to see it."}</span></div>`;
|
|
8552
|
+
}
|
|
8393
8553
|
const lifecycle = this.caps.manage ? `
|
|
8394
8554
|
<p class="slm-eyebrow" style="margin-top:18px">Lifecycle</p>
|
|
8395
8555
|
<div class="slm-ch-row2" style="margin-top:2px">
|
|
@@ -8400,58 +8560,59 @@ var ChannelsMode = class {
|
|
|
8400
8560
|
<p class="slm-note">Archive returns the allocation to a destination you choose. Nothing is ever deleted silently.</p>` : "";
|
|
8401
8561
|
const access = this.caps.manage ? this.distributeHtml(channel) : "";
|
|
8402
8562
|
return `
|
|
8403
|
-
|
|
8404
|
-
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
8405
|
-
</p>
|
|
8563
|
+
${back}
|
|
8406
8564
|
<p class="slm-eyebrow">Channel \xB7 ${esc(channel.name)}</p>
|
|
8407
8565
|
<div class="slm-ch-list">${this.channelRowHtml(channel)}</div>
|
|
8408
8566
|
${access}
|
|
8409
8567
|
${lifecycle}`;
|
|
8410
8568
|
}
|
|
8411
8569
|
/**
|
|
8412
|
-
* "Distribute" —
|
|
8570
|
+
* "Distribute" — the ONE way this channel's seats reach a buyer.
|
|
8571
|
+
*
|
|
8572
|
+
* All four routes are here, and this is a real chooser again. It was cut down
|
|
8573
|
+
* to two actions in 0.42.0 for a good reason: `access_intent` was stored,
|
|
8574
|
+
* audited, and read by nothing, so "Keep as protected reserve" and "Sell
|
|
8575
|
+
* through your own staff" were labels an organizer could set and then wait
|
|
8576
|
+
* forever for something to happen. The server closed that hole on 2026-08-06 —
|
|
8577
|
+
* each declaration now opens exactly one route and REFUSES the other three —
|
|
8578
|
+
* so all four are honest choices and belong on the surface.
|
|
8413
8579
|
*
|
|
8414
|
-
*
|
|
8415
|
-
*
|
|
8416
|
-
*
|
|
8417
|
-
* organizer could set and then wait forever for something to happen. A third,
|
|
8418
|
-
* `hosted_link`, is not the organizer's to choose at all — the server sets it
|
|
8419
|
-
* when a buyer link is created and clears it when the last live one is revoked.
|
|
8580
|
+
* None of the old copy came back with them. These sentences are written
|
|
8581
|
+
* against the enforcement matrix (`accessIntentDescription`), which is why
|
|
8582
|
+
* each one says what the route refuses as well as what it allows.
|
|
8420
8583
|
*
|
|
8421
|
-
*
|
|
8422
|
-
*
|
|
8423
|
-
*
|
|
8424
|
-
*
|
|
8425
|
-
* value is left alone (it is organizer-declared metadata and the API that
|
|
8426
|
-
* writes it is unchanged), it simply no longer has a control of its own.
|
|
8584
|
+
* The current route is stated, not merely styled: a chooser whose selection
|
|
8585
|
+
* you have to infer from a border is not a chooser. Its card carries a
|
|
8586
|
+
* "Current route" marker and drops its own select button, because pressing it
|
|
8587
|
+
* would do nothing.
|
|
8427
8588
|
*/
|
|
8428
8589
|
distributeHtml(channel) {
|
|
8429
8590
|
const intent = channel.access?.intent ?? "none";
|
|
8430
8591
|
const live = channel.access?.hasActiveGrants === true;
|
|
8431
|
-
const state = live ? intent === "server" ? "Your website is letting buyers in. Only they can buy these seats." : "A buyer link is live. Only people with that link can buy these seats." : intent === "server" ? "Set up for your website \u2014 no buyer has come through yet. Seats stay reserved." : "Not distributed yet \u2014 seats stay reserved.";
|
|
8432
|
-
const option = (title, why, action, cta, primary) => `<div class="slm-ch-dist">
|
|
8433
|
-
<b>${esc(title)}</b>
|
|
8434
|
-
<span class="why">${esc(why)}</span>
|
|
8435
|
-
<button type="button" class="slm-btn${primary ? "" : " ghost"}" data-ch-act="${esc(action)}">${esc(cta)}</button>
|
|
8436
|
-
</div>`;
|
|
8437
8592
|
const linksSupported = this.linksState !== "unsupported";
|
|
8593
|
+
const hasLiveLink = this.links.some(accessLinkIsLive);
|
|
8594
|
+
const state = intent === "none" ? "Protected reserve \u2014 no route can sell these seats. Every buyer path is refused." : intent === "internal" ? "Your staff sell these seats. No buyer-facing route is open." : live ? intent === "server" ? "Your website is letting buyers in. Only they can buy these seats." : "A buyer link is live. Only people with that link can buy these seats." : intent === "server" ? "Set up for your website \u2014 no buyer has come through yet. Seats stay reserved." : "Set up for buyer links \u2014 no buyer has come through yet. Seats stay reserved.";
|
|
8595
|
+
const option = (route, action, cta, primary) => {
|
|
8596
|
+
const current = route === intent;
|
|
8597
|
+
return `<div class="slm-ch-dist${current ? " on" : ""}" data-ch-route="${esc(route)}">
|
|
8598
|
+
<b>${esc(accessIntentLabel(route))}${current ? '<span class="cur">Current route</span>' : ""}</b>
|
|
8599
|
+
<span class="why">${esc(accessIntentDescription(route))}</span>
|
|
8600
|
+
${current && !cta ? "" : `<button type="button" class="slm-btn${primary && !current ? "" : " ghost"}"
|
|
8601
|
+
data-ch-act="${esc(action)}">${esc(cta)}</button>`}
|
|
8602
|
+
</div>`;
|
|
8603
|
+
};
|
|
8438
8604
|
return `
|
|
8439
8605
|
<p class="slm-eyebrow" style="margin-top:14px">Distribute</p>
|
|
8440
8606
|
<p class="slm-hint">${esc(state)}</p>
|
|
8441
8607
|
${linksSupported ? option(
|
|
8442
|
-
"
|
|
8443
|
-
"SeatLayer makes a link you send to a named group. They open it and buy only these seats.",
|
|
8608
|
+
"hosted_link",
|
|
8444
8609
|
"link-create",
|
|
8445
|
-
|
|
8610
|
+
hasLiveLink ? "Create another buyer link" : "Create buyer link",
|
|
8446
8611
|
true
|
|
8447
8612
|
) : ""}
|
|
8448
|
-
${option(
|
|
8449
|
-
"
|
|
8450
|
-
"
|
|
8451
|
-
"embed-code",
|
|
8452
|
-
"Get embed code",
|
|
8453
|
-
false
|
|
8454
|
-
)}
|
|
8613
|
+
${option("server", "embed-code", intent === "server" ? "Get embed code" : "Use a website or app", false)}
|
|
8614
|
+
${option("internal", "route-internal", intent === "internal" ? "" : "Hand to your staff", false)}
|
|
8615
|
+
${option("none", "route-none", intent === "none" ? "" : "Keep as reserve", false)}
|
|
8455
8616
|
${this.hostedLinksHtml()}
|
|
8456
8617
|
${intent === "server" ? SERVER_INTEGRATION_HTML : ""}`;
|
|
8457
8618
|
}
|
|
@@ -8499,16 +8660,17 @@ var ChannelsMode = class {
|
|
|
8499
8660
|
hostedLinksHtml() {
|
|
8500
8661
|
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Buyer links</p>`;
|
|
8501
8662
|
if (this.linksState === "unsupported") {
|
|
8502
|
-
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
8663
|
+
return `${eyebrow}<div class="slm-ch-alert warn" data-ch-state="links-unsupported"><span>\u2139</span>
|
|
8503
8664
|
<span><b>Buyer links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
8504
8665
|
}
|
|
8505
8666
|
if (this.linksState === "error") {
|
|
8506
|
-
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8507
|
-
<span><b>Couldn't load this channel's links.</b>
|
|
8667
|
+
return `${eyebrow}<div class="slm-ch-alert err" role="alert" data-ch-state="links-error"><span>\u26A0</span>
|
|
8668
|
+
<span><b>Couldn't load this channel's links.</b> This is a failed read, not an empty list \u2014
|
|
8669
|
+
any live link is still working.
|
|
8508
8670
|
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
8509
8671
|
}
|
|
8510
|
-
if (this.linksState === "loading" && !this.links.length) {
|
|
8511
|
-
return `${eyebrow}<div class="slm-
|
|
8672
|
+
if (this.linksState === "idle" || this.linksState === "loading" && !this.links.length) {
|
|
8673
|
+
return `${eyebrow}<div class="slm-ch-busy" role="status" data-ch-state="links-loading">Loading buyer links\u2026</div>`;
|
|
8512
8674
|
}
|
|
8513
8675
|
if (!this.links.length) return "";
|
|
8514
8676
|
return `${eyebrow}
|
|
@@ -8550,9 +8712,14 @@ var ChannelsMode = class {
|
|
|
8550
8712
|
];
|
|
8551
8713
|
const current = this.previewAudience[0] ?? PUBLIC_CHANNEL_ID;
|
|
8552
8714
|
const options = audienceOptions.map((entry) => `<option value="${esc(entry.id)}"${entry.id === current ? " selected" : ""}>${esc(entry.name)}</option>`).join("");
|
|
8553
|
-
const unsupported = this.
|
|
8715
|
+
const unsupported = this.previewState === "unsupported" ? `<div class="slm-ch-alert warn" data-ch-state="preview-unsupported"><span>\u2139</span>
|
|
8554
8716
|
<span><b>Preview needs a newer server.</b> Allocation management works normally;
|
|
8555
8717
|
the buyer-view simulation will appear once this event's API is updated.</span></div>` : "";
|
|
8718
|
+
const busy = this.previewState === "loading" ? `<div class="slm-ch-busy" role="status" data-ch-state="preview-loading">Asking the server what this audience sees\u2026</div>` : "";
|
|
8719
|
+
const failed = this.previewState === "error" ? `<div class="slm-ch-alert err" role="alert" data-ch-state="preview-error"><span>\u26A0</span>
|
|
8720
|
+
<span><b>Couldn't load the buyer preview.</b> Nothing is wrong with this audience's seats \u2014
|
|
8721
|
+
the simulation itself failed to load.
|
|
8722
|
+
<button type="button" data-ch-act="preview-retry">Try again</button></span></div>` : "";
|
|
8556
8723
|
const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
|
|
8557
8724
|
<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")}.
|
|
8558
8725
|
A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
|
|
@@ -8573,7 +8740,7 @@ var ChannelsMode = class {
|
|
|
8573
8740
|
${includePublic}
|
|
8574
8741
|
<p class="slm-hint">This is the same projection the buyer SDK receives for this audience \u2014 not a local
|
|
8575
8742
|
approximation. It is read-only: clicks open seat details, and no holds are created.</p>
|
|
8576
|
-
${unsupported}${unavailable}
|
|
8743
|
+
${busy}${failed}${unsupported}${unavailable}
|
|
8577
8744
|
<div class="slm-ch-legend">
|
|
8578
8745
|
<div class="r"><span class="sw" style="background:#6e7bff"></span> Eligible & free \u2014 buyable by this audience</div>
|
|
8579
8746
|
<div class="r"><span class="sw" style="background:#3a4051"></span> Unavailable to this audience (one neutral state)</div>
|
|
@@ -8582,6 +8749,7 @@ var ChannelsMode = class {
|
|
|
8582
8749
|
${summary}`;
|
|
8583
8750
|
}
|
|
8584
8751
|
paintSelection() {
|
|
8752
|
+
if (this.caps.manage && this.host.selectionLabels().length) this.assignOpen = true;
|
|
8585
8753
|
if (this.view === "inspect" && !this.detailChannelId) this.paintRail();
|
|
8586
8754
|
}
|
|
8587
8755
|
wireRail() {
|
|
@@ -8592,16 +8760,27 @@ var ChannelsMode = class {
|
|
|
8592
8760
|
rail.querySelectorAll("[data-ch-map]").forEach((button) => {
|
|
8593
8761
|
button.addEventListener("click", () => {
|
|
8594
8762
|
this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
|
|
8763
|
+
if (this.mapIntent === "assign") this.assignOpen = true;
|
|
8595
8764
|
this.paintRail();
|
|
8596
8765
|
this.onInteractionChange?.();
|
|
8597
8766
|
});
|
|
8598
8767
|
});
|
|
8599
|
-
rail.querySelectorAll("[data-ch-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8768
|
+
rail.querySelectorAll("[data-ch-open]").forEach((row) => {
|
|
8769
|
+
const open = () => this.openChannel(row.dataset.chOpen);
|
|
8770
|
+
row.addEventListener("click", open);
|
|
8771
|
+
row.addEventListener("keydown", (event) => {
|
|
8772
|
+
if (event.key !== "Enter" && event.key !== " " && event.key !== "Spacebar") return;
|
|
8773
|
+
event.preventDefault();
|
|
8774
|
+
open();
|
|
8775
|
+
});
|
|
8776
|
+
});
|
|
8777
|
+
rail.querySelectorAll("[data-ch-menu]").forEach((button) => {
|
|
8778
|
+
button.addEventListener("click", (event) => {
|
|
8779
|
+
event.stopPropagation();
|
|
8780
|
+
this.openDialog({ kind: "menu", channelId: button.dataset.chMenu });
|
|
8781
|
+
});
|
|
8782
|
+
button.addEventListener("keydown", (event) => {
|
|
8783
|
+
event.stopPropagation();
|
|
8605
8784
|
});
|
|
8606
8785
|
});
|
|
8607
8786
|
rail.querySelectorAll("[data-ch-rotate]").forEach((button) => {
|
|
@@ -8640,6 +8819,12 @@ var ChannelsMode = class {
|
|
|
8640
8819
|
button.addEventListener("click", () => this.railAction(button.dataset.chAct));
|
|
8641
8820
|
});
|
|
8642
8821
|
}
|
|
8822
|
+
/** Open a channel's detail panel and start its buyer-link read. */
|
|
8823
|
+
openChannel(channelId) {
|
|
8824
|
+
this.detailChannelId = channelId;
|
|
8825
|
+
this.paintRail();
|
|
8826
|
+
void this.loadLinks(channelId).then(() => this.paintRail());
|
|
8827
|
+
}
|
|
8643
8828
|
railAction(action) {
|
|
8644
8829
|
switch (action) {
|
|
8645
8830
|
case "sections":
|
|
@@ -8675,16 +8860,25 @@ var ChannelsMode = class {
|
|
|
8675
8860
|
this.linksState = "idle";
|
|
8676
8861
|
this.paintRail();
|
|
8677
8862
|
break;
|
|
8863
|
+
// Choosing the buyer-link route IS creating the first link — the dialog
|
|
8864
|
+
// declares the route on submit (see `createLink`), so this stays one
|
|
8865
|
+
// gesture whether the channel is already on `hosted_link` or not.
|
|
8678
8866
|
case "link-create":
|
|
8679
8867
|
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
8680
8868
|
break;
|
|
8681
8869
|
// The one thing this screen can actually do for a website integration is
|
|
8682
|
-
// record that the channel is meant for one —
|
|
8683
|
-
//
|
|
8684
|
-
//
|
|
8870
|
+
// record that the channel is meant for one — and since 2026-08-06 that
|
|
8871
|
+
// record is what AUTHORIZES the integration to mint buyer sessions at all,
|
|
8872
|
+
// so it is the substantive half of the job, not a flag.
|
|
8685
8873
|
case "embed-code":
|
|
8686
8874
|
void this.chooseWebsiteIntegration();
|
|
8687
8875
|
break;
|
|
8876
|
+
case "route-internal":
|
|
8877
|
+
void this.setAccessIntent("internal");
|
|
8878
|
+
break;
|
|
8879
|
+
case "route-none":
|
|
8880
|
+
void this.setAccessIntent("none");
|
|
8881
|
+
break;
|
|
8688
8882
|
case "link-reload":
|
|
8689
8883
|
if (this.detailChannelId) void this.reloadLinks();
|
|
8690
8884
|
break;
|
|
@@ -8713,6 +8907,22 @@ var ChannelsMode = class {
|
|
|
8713
8907
|
case "pick-category":
|
|
8714
8908
|
this.pickCategory();
|
|
8715
8909
|
break;
|
|
8910
|
+
case "assign-open":
|
|
8911
|
+
this.assignOpen = true;
|
|
8912
|
+
this.paintRail();
|
|
8913
|
+
break;
|
|
8914
|
+
// Collapsing the tools must also disarm the marquee. Leaving it armed
|
|
8915
|
+
// would hide a mode the map is still in — the organizer would drag to pan
|
|
8916
|
+
// and select seats instead, with no control on screen saying why.
|
|
8917
|
+
case "assign-close":
|
|
8918
|
+
this.assignOpen = false;
|
|
8919
|
+
this.mapIntent = "pan";
|
|
8920
|
+
this.paintRail();
|
|
8921
|
+
this.onInteractionChange?.();
|
|
8922
|
+
break;
|
|
8923
|
+
case "preview-retry":
|
|
8924
|
+
void this.loadPreview();
|
|
8925
|
+
break;
|
|
8716
8926
|
default:
|
|
8717
8927
|
break;
|
|
8718
8928
|
}
|
|
@@ -8862,7 +9072,8 @@ var ChannelsMode = class {
|
|
|
8862
9072
|
blocks.push(`<p class="slm-ch-scopehint">${(matches.length - visible.length).toLocaleString()} more row${matches.length - visible.length === 1 ? "" : "s"}. Search to narrow the list.</p>`);
|
|
8863
9073
|
}
|
|
8864
9074
|
});
|
|
8865
|
-
list.innerHTML = blocks.join("") || `<div class="slm-ch-scope-empty">No sections or rows match \u201C${esc(query)}\u201D.</div
|
|
9075
|
+
list.innerHTML = blocks.join("") || (query ? `<div class="slm-ch-scope-empty" data-ch-state="scope-nomatch">No sections or rows match \u201C${esc(query)}\u201D.</div>` : `<div class="slm-ch-scope-empty" data-ch-state="scope-empty">This chart has no rows with selectable seats.
|
|
9076
|
+
Use Drag box or the seat list instead.</div>`);
|
|
8866
9077
|
filterHint.textContent = query ? `Showing ${rendered.toLocaleString()} of ${totalMatches.toLocaleString()} matching rows. Section checkboxes still select every row in that section.` : "Showing section groups. Expand one or search to see rows.";
|
|
8867
9078
|
list.querySelectorAll("[data-ch-scope-group]").forEach((button) => button.addEventListener("click", () => {
|
|
8868
9079
|
const items = groups[Number(button.dataset.chScopeGroup)]?.[1] ?? [];
|
|
@@ -8912,7 +9123,8 @@ var ChannelsMode = class {
|
|
|
8912
9123
|
this.renderScrim(`
|
|
8913
9124
|
<h3 id="slm-ch-dlg-title">Add whole sections</h3>
|
|
8914
9125
|
<p class="sub">Choose one or more. They are added to the seats already selected on the map.</p>
|
|
8915
|
-
<div class="slm-ch-seatlist">${body || `<div class="slm-empty">
|
|
9126
|
+
<div class="slm-ch-seatlist">${body || `<div class="slm-ch-scope-empty" data-ch-state="scope-empty">This chart has no sections with selectable seats.
|
|
9127
|
+
Use Drag box or the seat list instead.</div>`}</div>
|
|
8916
9128
|
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8917
9129
|
<div class="foot">
|
|
8918
9130
|
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
@@ -8969,9 +9181,81 @@ var ChannelsMode = class {
|
|
|
8969
9181
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
8970
9182
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
8971
9183
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
9184
|
+
else if (state.kind === "menu") this.renderMenuDialog(state);
|
|
8972
9185
|
else if (state.kind === "linkCreate") this.renderLinkCreateDialog(state);
|
|
8973
9186
|
else if (state.kind === "linkRotate") this.renderLinkRotateDialog(state);
|
|
8974
9187
|
else if (state.kind === "linkRevoke") this.renderLinkRevokeDialog(state);
|
|
9188
|
+
else if (state.kind === "intentSwitch") this.renderIntentSwitchDialog(state);
|
|
9189
|
+
}
|
|
9190
|
+
/**
|
|
9191
|
+
* `channel_intent_switch_blocked` — buyers are inside the route being left.
|
|
9192
|
+
*
|
|
9193
|
+
* The same review-then-acknowledge shape as the archive and chart-drop guards,
|
|
9194
|
+
* because it is the same kind of decision: the server refuses once, names
|
|
9195
|
+
* exactly what is at stake, and only a deliberate second press goes through.
|
|
9196
|
+
* What acknowledging does is spelled out per consequence — links close now,
|
|
9197
|
+
* checkouts already running survive and drain — rather than hidden behind a
|
|
9198
|
+
* word like "force".
|
|
9199
|
+
*/
|
|
9200
|
+
renderIntentSwitchDialog(state) {
|
|
9201
|
+
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
9202
|
+
const to = state.intentTo;
|
|
9203
|
+
if (!channel || !to || !this.caps.manage) {
|
|
9204
|
+
this.closeDialog();
|
|
9205
|
+
return;
|
|
9206
|
+
}
|
|
9207
|
+
const { headline, consequences } = intentSwitchBlockedCopy(state.switchBlocked);
|
|
9208
|
+
this.renderScrim(`
|
|
9209
|
+
<h3 id="slm-ch-dlg-title">Change how ${esc(channel.name)} reaches buyers?</h3>
|
|
9210
|
+
<p class="sub">${esc(headline)}</p>
|
|
9211
|
+
<div class="slm-ch-alert warn" role="alert"><span>\u26A0</span><span>
|
|
9212
|
+
${consequences.map((line) => `<span style="display:block;margin-bottom:6px">${esc(line)}</span>`).join("")}
|
|
9213
|
+
</span></div>
|
|
9214
|
+
${state.pendingLink ? `<p class="slm-note">Your new buyer link is created as soon as
|
|
9215
|
+
the route changes \u2014 you will not have to fill the form in again.</p>` : ""}
|
|
9216
|
+
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
9217
|
+
<div class="foot">
|
|
9218
|
+
<button type="button" class="quiet" data-ch-close>Leave it as it is</button>
|
|
9219
|
+
<button type="button" class="slm-btn" data-ch-intent-ack${state.busy ? " disabled" : ""}>
|
|
9220
|
+
${state.busy ? "Changing\u2026" : `Change to "${esc(accessIntentLabel(to))}"`}</button>
|
|
9221
|
+
</div>`, (dialog) => {
|
|
9222
|
+
dialog.querySelector("[data-ch-intent-ack]")?.addEventListener("click", () => {
|
|
9223
|
+
void this.acknowledgeIntentSwitch(to, state.pendingLink ?? null);
|
|
9224
|
+
});
|
|
9225
|
+
});
|
|
9226
|
+
}
|
|
9227
|
+
/**
|
|
9228
|
+
* The acknowledged retry. The declare and the create stay one gesture across
|
|
9229
|
+
* the sheet: if this switch was the first half of a declare-then-create, the
|
|
9230
|
+
* held form finishes on the far side of the acknowledgement.
|
|
9231
|
+
*/
|
|
9232
|
+
async acknowledgeIntentSwitch(intent, pendingLink) {
|
|
9233
|
+
const channelId = this.detailChannelId;
|
|
9234
|
+
if (!channelId) {
|
|
9235
|
+
this.closeDialog();
|
|
9236
|
+
return;
|
|
9237
|
+
}
|
|
9238
|
+
if (this.dialog) {
|
|
9239
|
+
this.dialog.busy = true;
|
|
9240
|
+
this.renderDialog();
|
|
9241
|
+
}
|
|
9242
|
+
const ok = await this.setAccessIntent(intent, { acknowledgeLiveAccess: true });
|
|
9243
|
+
if (!ok) {
|
|
9244
|
+
if (this.dialog?.kind === "intentSwitch") {
|
|
9245
|
+
this.dialog.busy = false;
|
|
9246
|
+
this.renderDialog();
|
|
9247
|
+
}
|
|
9248
|
+
return;
|
|
9249
|
+
}
|
|
9250
|
+
if (!pendingLink) {
|
|
9251
|
+
this.closeDialog();
|
|
9252
|
+
return;
|
|
9253
|
+
}
|
|
9254
|
+
if (this.dialog?.kind === "intentSwitch") {
|
|
9255
|
+
this.dialog.busy = false;
|
|
9256
|
+
this.renderDialog();
|
|
9257
|
+
}
|
|
9258
|
+
await this.mintLink(channelId, pendingLink);
|
|
8975
9259
|
}
|
|
8976
9260
|
/**
|
|
8977
9261
|
* Mount a modal: `aria-modal` dialog, programmatic name, focus moved inside,
|
|
@@ -9224,6 +9508,59 @@ var ChannelsMode = class {
|
|
|
9224
9508
|
void bar.offsetWidth;
|
|
9225
9509
|
bar.classList.add("shake");
|
|
9226
9510
|
}
|
|
9511
|
+
/**
|
|
9512
|
+
* The ⋯ menu.
|
|
9513
|
+
*
|
|
9514
|
+
* Opening a channel is the ROW's job now, so ⋯ carries what is left: the
|
|
9515
|
+
* secondary and destructive lifecycle actions. It is rendered through the same
|
|
9516
|
+
* scrim primitive as every other sheet, which is what gives it a focus trap,
|
|
9517
|
+
* Escape, and a name — a bare absolutely-positioned popup would have had none
|
|
9518
|
+
* of those. Archive keeps its own confirmation dialog; this menu never
|
|
9519
|
+
* destroys anything by itself.
|
|
9520
|
+
*/
|
|
9521
|
+
renderMenuDialog(state) {
|
|
9522
|
+
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
9523
|
+
if (!channel || !this.caps.manage) {
|
|
9524
|
+
this.closeDialog();
|
|
9525
|
+
return;
|
|
9526
|
+
}
|
|
9527
|
+
const paused = channel.state === "paused";
|
|
9528
|
+
this.renderScrim(`
|
|
9529
|
+
<h3 id="slm-ch-dlg-title">${esc(channel.name)}</h3>
|
|
9530
|
+
<p class="sub">Open the channel to allocate seats and hand out buyer access.
|
|
9531
|
+
These are the rest of its actions.</p>
|
|
9532
|
+
<div class="slm-ch-menu">
|
|
9533
|
+
<button type="button" class="slm-btn" data-ch-menu-act="open">Open channel</button>
|
|
9534
|
+
<button type="button" class="slm-btn ghost" data-ch-menu-act="rename">Rename</button>
|
|
9535
|
+
<button type="button" class="slm-btn ghost" data-ch-menu-act="pause">${paused ? "Resume selling" : "Pause selling"}</button>
|
|
9536
|
+
<button type="button" class="slm-btn ghost" data-ch-menu-act="archive">Archive\u2026</button>
|
|
9537
|
+
</div>
|
|
9538
|
+
<p class="slm-note">Pausing stops new buyer access; checkouts already running can finish.
|
|
9539
|
+
Archiving closes the channel for good and returns its free seats to a destination you choose.
|
|
9540
|
+
Nothing is ever deleted silently.</p>
|
|
9541
|
+
<div class="foot"><button type="button" class="quiet" data-ch-close>Cancel</button></div>`, (dialog) => {
|
|
9542
|
+
dialog.querySelectorAll("[data-ch-menu-act]").forEach((button) => {
|
|
9543
|
+
button.addEventListener("click", () => {
|
|
9544
|
+
const act = button.dataset.chMenuAct;
|
|
9545
|
+
if (act === "open") {
|
|
9546
|
+
this.closeDialog();
|
|
9547
|
+
this.openChannel(channel.id);
|
|
9548
|
+
return;
|
|
9549
|
+
}
|
|
9550
|
+
if (act === "rename") {
|
|
9551
|
+
this.openDialog({ kind: "rename", channelId: channel.id });
|
|
9552
|
+
return;
|
|
9553
|
+
}
|
|
9554
|
+
if (act === "archive") {
|
|
9555
|
+
this.openDialog({ kind: "archive", channelId: channel.id });
|
|
9556
|
+
return;
|
|
9557
|
+
}
|
|
9558
|
+
this.closeDialog();
|
|
9559
|
+
void this.togglePause(channel.id);
|
|
9560
|
+
});
|
|
9561
|
+
});
|
|
9562
|
+
});
|
|
9563
|
+
}
|
|
9227
9564
|
renderRenameDialog(state) {
|
|
9228
9565
|
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
9229
9566
|
if (!channel) {
|
|
@@ -9259,34 +9596,74 @@ var ChannelsMode = class {
|
|
|
9259
9596
|
});
|
|
9260
9597
|
}
|
|
9261
9598
|
/**
|
|
9262
|
-
* "
|
|
9263
|
-
*
|
|
9264
|
-
*
|
|
9265
|
-
*
|
|
9266
|
-
*
|
|
9599
|
+
* "Use a website or app" — declare the channel's route as `server`.
|
|
9600
|
+
*
|
|
9601
|
+
* This is no longer a flag the Embed page happens to read: since 2026-08-06 it
|
|
9602
|
+
* is what AUTHORIZES `POST /v1/events/:key/buyer-access-sessions` to mint for
|
|
9603
|
+
* this channel at all. Without it, an integration that is otherwise perfectly
|
|
9604
|
+
* wired up gets a 409 on every buyer.
|
|
9267
9605
|
*/
|
|
9268
9606
|
async chooseWebsiteIntegration() {
|
|
9269
9607
|
const channelId = this.detailChannelId;
|
|
9270
9608
|
if (!channelId) return;
|
|
9271
9609
|
await this.setAccessIntent("server");
|
|
9272
9610
|
}
|
|
9273
|
-
|
|
9611
|
+
/**
|
|
9612
|
+
* Declare this channel's sale route.
|
|
9613
|
+
*
|
|
9614
|
+
* Reports through the toast lane the rest of the rail's direct actions use.
|
|
9615
|
+
* The two enforcement refusals get real answers rather than a generic failure:
|
|
9616
|
+
* `channel_intent_switch_blocked` opens the review sheet (there is a decision
|
|
9617
|
+
* to make, and a sheet is where decisions live), and
|
|
9618
|
+
* `channel_access_intent_forbids` — which the organizer can hit by racing
|
|
9619
|
+
* their own second tab — says which route is in the way.
|
|
9620
|
+
*/
|
|
9621
|
+
async setAccessIntent(accessIntent, opts = {}) {
|
|
9274
9622
|
const channelId = this.detailChannelId;
|
|
9275
|
-
if (!channelId || !this.caps.manage) return;
|
|
9623
|
+
if (!channelId || !this.caps.manage) return false;
|
|
9276
9624
|
try {
|
|
9277
|
-
await this.host.api.setChannelAccessIntent(
|
|
9625
|
+
const result = await this.host.api.setChannelAccessIntent(
|
|
9626
|
+
this.host.eventKey,
|
|
9627
|
+
channelId,
|
|
9628
|
+
accessIntent,
|
|
9629
|
+
opts
|
|
9630
|
+
);
|
|
9278
9631
|
await this.refresh();
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
}
|
|
9632
|
+
this.host.toast(this.intentSavedCopy(accessIntent, result?.intentSwitch), "ok");
|
|
9633
|
+
return true;
|
|
9282
9634
|
} catch (err) {
|
|
9283
|
-
|
|
9635
|
+
if (err instanceof ManageApiError && err.code === "channel_intent_switch_blocked") {
|
|
9636
|
+
this.openDialog({
|
|
9637
|
+
kind: "intentSwitch",
|
|
9638
|
+
channelId,
|
|
9639
|
+
intentTo: accessIntent,
|
|
9640
|
+
switchBlocked: err.details
|
|
9641
|
+
});
|
|
9642
|
+
return false;
|
|
9643
|
+
}
|
|
9644
|
+
if (err instanceof ManageApiError && err.code === "channel_access_intent_forbids") {
|
|
9645
|
+
this.host.toast(intentForbidsCopy(err.details), "err");
|
|
9646
|
+
return false;
|
|
9647
|
+
}
|
|
9648
|
+
this.host.toast("Couldn't change how this channel reaches buyers.", "err");
|
|
9284
9649
|
this.host.onError(err);
|
|
9650
|
+
return false;
|
|
9285
9651
|
}
|
|
9286
9652
|
}
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9653
|
+
/** What just happened, including anything the switch took down with it — the
|
|
9654
|
+
* server reports `intentSwitch` only when it actually disturbed something. */
|
|
9655
|
+
intentSavedCopy(intent, disturbed) {
|
|
9656
|
+
const head = intent === "server" ? "Set to your website or app. The embed code is on the Embed page." : intent === "internal" ? "Only your own staff can sell this channel now." : intent === "hosted_link" ? "Set to buyer links. Create one to let buyers in." : "Kept as a protected reserve. No route can sell these seats.";
|
|
9657
|
+
if (!disturbed) return head;
|
|
9658
|
+
const closed = disturbed.closedLinks ? ` ${disturbed.closedLinks.toLocaleString()} buyer link${disturbed.closedLinks === 1 ? "" : "s"} closed.` : "";
|
|
9659
|
+
const kept = disturbed.keptSessions ? ` ${disturbed.keptSessions.toLocaleString()} buyer${disturbed.keptSessions === 1 ? "" : "s"} already in a checkout can still finish.` : "";
|
|
9660
|
+
return `${head}${closed}${kept}`;
|
|
9661
|
+
}
|
|
9662
|
+
/** `channelId` is explicit because this is reachable from the ⋯ menu on a row
|
|
9663
|
+
* that is NOT the open channel, as well as from the detail panel itself. */
|
|
9664
|
+
async togglePause(channelId = this.detailChannelId) {
|
|
9665
|
+
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
9666
|
+
if (!channel || !this.caps.manage) return;
|
|
9290
9667
|
const paused = channel.state !== "paused";
|
|
9291
9668
|
try {
|
|
9292
9669
|
await this.host.api.setChannelPaused(this.host.eventKey, channel.id, paused);
|
|
@@ -9534,7 +9911,35 @@ var ChannelsMode = class {
|
|
|
9534
9911
|
});
|
|
9535
9912
|
});
|
|
9536
9913
|
}
|
|
9914
|
+
/**
|
|
9915
|
+
* DECLARE, then create.
|
|
9916
|
+
*
|
|
9917
|
+
* `createAccessLink` used to set the channel's route to `hosted_link` as a
|
|
9918
|
+
* side effect, which is exactly why the picker could never refuse anything.
|
|
9919
|
+
* The server took that side effect away and now REQUIRES the declaration —
|
|
9920
|
+
* and channels default to `none`, so a create that did not declare first
|
|
9921
|
+
* would 409 on the organizer's very first "Create buyer link".
|
|
9922
|
+
*
|
|
9923
|
+
* So the route is declared here, immediately before the create. It stays ONE
|
|
9924
|
+
* gesture: nothing is declared while the organizer is still filling the form
|
|
9925
|
+
* in (cancelling changes nothing), and if the declaration is the part that is
|
|
9926
|
+
* refused, the review sheet holds this form and finishes the job on the far
|
|
9927
|
+
* side of the acknowledgement.
|
|
9928
|
+
*/
|
|
9537
9929
|
async createLink(channelId, input) {
|
|
9930
|
+
if (!await this.ensureHostedLinkRoute(channelId, input)) return;
|
|
9931
|
+
await this.mintLink(channelId, input);
|
|
9932
|
+
}
|
|
9933
|
+
/**
|
|
9934
|
+
* The create half, on its own.
|
|
9935
|
+
*
|
|
9936
|
+
* Separate from `createLink` because the acknowledge path has ALREADY declared
|
|
9937
|
+
* the route — with the very acknowledgement the plain declaration was refused
|
|
9938
|
+
* for. Sending it back through `ensureHostedLinkRoute` would re-derive the
|
|
9939
|
+
* route from a channel list that has not necessarily caught up, and could
|
|
9940
|
+
* refuse the organizer a second time for a decision they just made.
|
|
9941
|
+
*/
|
|
9942
|
+
async mintLink(channelId, input) {
|
|
9538
9943
|
try {
|
|
9539
9944
|
const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
|
|
9540
9945
|
this.revealLink(reveal, { channelId });
|
|
@@ -9544,6 +9949,37 @@ var ChannelsMode = class {
|
|
|
9544
9949
|
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
9545
9950
|
}
|
|
9546
9951
|
}
|
|
9952
|
+
/**
|
|
9953
|
+
* Make sure the channel declares the buyer-link route before a link is minted.
|
|
9954
|
+
*
|
|
9955
|
+
* Returns false when the create must NOT proceed — either it was refused, or
|
|
9956
|
+
* the decision has been handed to the switch-review sheet, which resumes it.
|
|
9957
|
+
* A channel already on `hosted_link` costs no request at all, so creating a
|
|
9958
|
+
* second link is the same single call it has always been.
|
|
9959
|
+
*/
|
|
9960
|
+
async ensureHostedLinkRoute(channelId, pendingLink) {
|
|
9961
|
+
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
9962
|
+
if ((channel?.access?.intent ?? "none") === "hosted_link") return true;
|
|
9963
|
+
try {
|
|
9964
|
+
await this.host.api.setChannelAccessIntent(this.host.eventKey, channelId, "hosted_link");
|
|
9965
|
+
await this.refresh();
|
|
9966
|
+
return true;
|
|
9967
|
+
} catch (err) {
|
|
9968
|
+
if (err instanceof ManageApiError && err.code === "channel_intent_switch_blocked") {
|
|
9969
|
+
this.openDialog({
|
|
9970
|
+
kind: "intentSwitch",
|
|
9971
|
+
channelId,
|
|
9972
|
+
intentTo: "hosted_link",
|
|
9973
|
+
switchBlocked: err.details,
|
|
9974
|
+
pendingLink
|
|
9975
|
+
});
|
|
9976
|
+
return false;
|
|
9977
|
+
}
|
|
9978
|
+
this.showDialogError(err instanceof ManageApiError && err.code === "channel_access_intent_forbids" ? intentForbidsCopy(err.details) : "Couldn't set this channel up for buyer links. Try again.");
|
|
9979
|
+
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
9980
|
+
return false;
|
|
9981
|
+
}
|
|
9982
|
+
}
|
|
9547
9983
|
/**
|
|
9548
9984
|
* The ONE-TIME reveal.
|
|
9549
9985
|
*
|
|
@@ -12210,6 +12646,7 @@ var SeatManager = class {
|
|
|
12210
12646
|
SeatManager,
|
|
12211
12647
|
SeatPicker,
|
|
12212
12648
|
SeatingChart,
|
|
12649
|
+
accessIntentDescription,
|
|
12213
12650
|
accessIntentLabel,
|
|
12214
12651
|
accessLine,
|
|
12215
12652
|
accessLinkBadge,
|
|
@@ -12222,6 +12659,8 @@ var SeatManager = class {
|
|
|
12222
12659
|
createBuyerAccessContext,
|
|
12223
12660
|
createControllerSink,
|
|
12224
12661
|
dropReviewRows,
|
|
12662
|
+
intentForbidsCopy,
|
|
12663
|
+
intentSwitchBlockedCopy,
|
|
12225
12664
|
isPublicChannelId,
|
|
12226
12665
|
markerLetter,
|
|
12227
12666
|
markerOf,
|