@seatlayer/js 0.41.0 → 0.43.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 +525 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -7
- package/dist/index.d.ts +116 -7
- package/dist/index.js +525 -98
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -6901,7 +6901,7 @@ function retryAfterCopy(details) {
|
|
|
6901
6901
|
}
|
|
6902
6902
|
function accessLine(access) {
|
|
6903
6903
|
if (!access || !access.intent) return "\u2014";
|
|
6904
|
-
const base = access.intent === "
|
|
6904
|
+
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : "Not distributed yet";
|
|
6905
6905
|
const grants = access.hasActiveGrants ? "in use now" : access.lastMintAt ? `last used ${new Date(access.lastMintAt).toLocaleDateString()}` : null;
|
|
6906
6906
|
const detail = access.detail ?? grants;
|
|
6907
6907
|
return detail ? `${base} \xB7 ${detail}` : base;
|
|
@@ -7293,9 +7293,10 @@ var ManageApi = class {
|
|
|
7293
7293
|
};
|
|
7294
7294
|
|
|
7295
7295
|
// src/channelsMode.ts
|
|
7296
|
-
var POLL_MS =
|
|
7296
|
+
var POLL_MS = 3e4;
|
|
7297
7297
|
var MAX_FLAGS = 8;
|
|
7298
7298
|
var SEAT_LIST_PAGE = 300;
|
|
7299
|
+
var MAX_ASSIGNMENT_UNITS = 5e3;
|
|
7299
7300
|
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
7300
7301
|
var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
7301
7302
|
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
@@ -7342,6 +7343,7 @@ var CHANNELS_CSS = (
|
|
|
7342
7343
|
.slm-ch-staged .grow{flex:1}
|
|
7343
7344
|
.slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
|
|
7344
7345
|
background:#f4b740;color:#1a1200;font-weight:800;font-size:12.5px}
|
|
7346
|
+
.slm-ch-staged .go:disabled{opacity:.48;cursor:not-allowed}
|
|
7345
7347
|
.slm-ch-staged .drop{color:var(--slm-muted);font-weight:700;font-size:11.5px;min-height:44px;padding-inline:8px}
|
|
7346
7348
|
.slm-ch-tick{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;
|
|
7347
7349
|
background:#fff;color:#1f7a4d;font-weight:900;font-size:11px;animation:slm-ch-tick var(--slm-mo-base) var(--slm-mo-spring)}
|
|
@@ -7392,6 +7394,12 @@ var CHANNELS_CSS = (
|
|
|
7392
7394
|
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
7393
7395
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
7394
7396
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
7397
|
+
/* distribute options \u2014 two actions, each with the one sentence that explains it */
|
|
7398
|
+
.slm-ch-dist{display:flex;flex-direction:column;gap:6px;padding:12px;margin-bottom:8px;border:1px solid var(--slm-line);
|
|
7399
|
+
border-radius:10px;background:var(--slm-surface)}
|
|
7400
|
+
.slm-ch-dist b{font-size:13px;font-weight:800}
|
|
7401
|
+
.slm-ch-dist .why{color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
7402
|
+
.slm-ch-dist .slm-btn{width:100%;margin-top:2px}
|
|
7395
7403
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
7396
7404
|
line-height:1.5;margin-bottom:12px}
|
|
7397
7405
|
.slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
|
|
@@ -7407,7 +7415,7 @@ var CHANNELS_CSS = (
|
|
|
7407
7415
|
/* dialogs */
|
|
7408
7416
|
.slm-ch-scrim{position:absolute;inset:0;z-index:12;background:rgba(4,6,12,.62);display:grid;place-items:center;
|
|
7409
7417
|
padding:18px;animation:slm-ch-fade var(--slm-mo-quick) var(--slm-mo-out)}
|
|
7410
|
-
.slm-ch-dialog{width:min(460px,100%);max-height:100%;overflow:auto;background:#12151f;border:1px solid var(--slm-line);
|
|
7418
|
+
.slm-ch-dialog{width:min(460px,100%);max-height:100%;overflow:auto;overscroll-behavior:contain;background:#12151f;border:1px solid var(--slm-line);
|
|
7411
7419
|
border-radius:14px;padding:20px;box-shadow:0 24px 70px rgba(0,0,0,.6);
|
|
7412
7420
|
animation:slm-ch-rise var(--slm-mo-base) var(--slm-mo-out)}
|
|
7413
7421
|
.slm-ch-dialog h3{margin:0 0 4px;font-size:16px;font-weight:800;letter-spacing:-.01em}
|
|
@@ -7434,7 +7442,7 @@ var CHANNELS_CSS = (
|
|
|
7434
7442
|
overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
7435
7443
|
.slm-ch-err{color:#f1a4a6;font-size:11.5px;margin-top:6px}
|
|
7436
7444
|
|
|
7437
|
-
/*
|
|
7445
|
+
/* buyer links \u2014 STATUS only; there is no Copy control on this card */
|
|
7438
7446
|
.slm-ch-link{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
7439
7447
|
margin-bottom:8px}
|
|
7440
7448
|
.slm-ch-link .lk-head{display:flex;align-items:center;gap:8px}
|
|
@@ -7453,8 +7461,8 @@ var CHANNELS_CSS = (
|
|
|
7453
7461
|
.slm-ch-radio input{flex:none;margin-top:2px}
|
|
7454
7462
|
.slm-ch-radio b{display:block;font-weight:800;margin-bottom:2px}
|
|
7455
7463
|
.slm-ch-radio .why{display:block;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
7456
|
-
.slm-ch-seatlist{max-height:44vh;overflow:auto;border:1px solid var(--slm-line);
|
|
7457
|
-
background:var(--slm-surface);margin-top:10px}
|
|
7464
|
+
.slm-ch-seatlist{max-height:44vh;overflow:auto;overscroll-behavior:contain;border:1px solid var(--slm-line);
|
|
7465
|
+
border-radius:10px;background:var(--slm-surface);margin-top:10px}
|
|
7458
7466
|
.slm-ch-seatgroup{padding:8px 10px;border-bottom:1px solid var(--slm-line);display:flex;align-items:center;
|
|
7459
7467
|
justify-content:space-between;gap:8px;font-size:11px;font-weight:800;color:var(--slm-muted);position:sticky;top:0;
|
|
7460
7468
|
background:var(--slm-surface)}
|
|
@@ -7466,6 +7474,25 @@ var CHANNELS_CSS = (
|
|
|
7466
7474
|
.slm-ch-seatitem[aria-checked="true"] .box{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7467
7475
|
.slm-ch-seatitem .meta{margin-left:auto;color:var(--slm-muted);font-size:10.5px}
|
|
7468
7476
|
|
|
7477
|
+
/* scope chooser: whole sections or many rows in one additive pass */
|
|
7478
|
+
.slm-ch-scopebar{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:10px;margin-top:12px}
|
|
7479
|
+
.slm-ch-scopebar label{display:grid;gap:5px;color:var(--slm-muted);font-size:10.5px;font-weight:800}
|
|
7480
|
+
.slm-ch-scopebar input{width:100%;min-height:40px;padding:8px 10px;border:1px solid var(--slm-line);border-radius:8px;
|
|
7481
|
+
background:var(--slm-surface);color:var(--slm-text);font:inherit}
|
|
7482
|
+
.slm-ch-scopesummary{padding-bottom:10px;color:var(--slm-muted);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
7483
|
+
.slm-ch-scopegroup{position:sticky;top:0;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;
|
|
7484
|
+
gap:6px;padding:6px 8px;border-bottom:1px solid var(--slm-line);background:var(--slm-surface)}
|
|
7485
|
+
.slm-ch-groupcheck{display:flex;min-width:0;align-items:center;gap:8px;padding:5px 2px;text-align:left;font-size:11px;font-weight:800}
|
|
7486
|
+
.slm-ch-groupcheck .box{width:16px;height:16px;flex:none;display:grid;place-items:center;border:1px solid var(--slm-muted);border-radius:4px;
|
|
7487
|
+
color:transparent;font-size:10px}
|
|
7488
|
+
.slm-ch-groupcheck[aria-checked="true"] .box,.slm-ch-groupcheck[aria-checked="mixed"] .box{border-color:var(--slm-accent);
|
|
7489
|
+
background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7490
|
+
.slm-ch-groupcheck .meta{min-width:0;margin-left:auto;color:var(--slm-muted);font-size:10.5px;font-weight:600;white-space:nowrap}
|
|
7491
|
+
.slm-ch-grouptoggle{min-height:32px;padding:5px 8px;color:var(--slm-accent);font-size:11px;font-weight:800}
|
|
7492
|
+
.slm-ch-scopehint{padding:8px 10px;color:var(--slm-muted);font-size:10.5px;border-bottom:1px solid var(--slm-line)}
|
|
7493
|
+
.slm-ch-scope-empty{padding:18px 12px;color:var(--slm-muted);font-size:11.5px;text-align:center}
|
|
7494
|
+
@media(max-width:560px){.slm-ch-scopebar{grid-template-columns:1fr}.slm-ch-scopesummary{padding-bottom:0}}
|
|
7495
|
+
|
|
7469
7496
|
/* compact: bottom sheet with three detents (\xA713) */
|
|
7470
7497
|
.slm.compact.ch-sheet .slm-rail{position:absolute;left:0;right:0;bottom:0;z-index:8;border-top:1px solid var(--slm-line);
|
|
7471
7498
|
border-radius:18px 18px 0 0;background:#12151f;
|
|
@@ -7504,12 +7531,12 @@ function bucketRowsHtml(rows) {
|
|
|
7504
7531
|
</div>`).join("");
|
|
7505
7532
|
}
|
|
7506
7533
|
var SERVER_INTEGRATION_HTML = `
|
|
7507
|
-
<p class="slm-eyebrow" style="margin-top:18px">
|
|
7508
|
-
<p class="slm-hint">There is nothing to set up on this screen. Your own server mints a short-lived buyer
|
|
7534
|
+
<p class="slm-eyebrow" style="margin-top:18px">On your website</p>
|
|
7535
|
+
<p class="slm-hint">There is nothing more to set up on this screen. Your own server mints a short-lived buyer
|
|
7509
7536
|
access session for this channel with the SeatLayer server SDK and hands it to the widget. A channel name
|
|
7510
7537
|
on its own never grants access.</p>
|
|
7511
7538
|
<p class="slm-note"><a class="slm-linkbtn" href="https://docs.seatlayer.io/server-api/channels"
|
|
7512
|
-
target="_blank" rel="noreferrer noopener">Read the
|
|
7539
|
+
target="_blank" rel="noreferrer noopener">Read the website integration guide \u2192</a></p>`;
|
|
7513
7540
|
function esc(value) {
|
|
7514
7541
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7515
7542
|
}
|
|
@@ -7590,6 +7617,21 @@ var ChannelsMode = class {
|
|
|
7590
7617
|
this.stagedDoneTimer = null;
|
|
7591
7618
|
this.lastSelectionCount = 0;
|
|
7592
7619
|
this.lastCounts = /* @__PURE__ */ new Map();
|
|
7620
|
+
/** Rows are structural chart data — they do not move while the organizer is
|
|
7621
|
+
* allocating. Deriving them walks every seat, so the view is cached for the
|
|
7622
|
+
* lifetime of this mode entry and ordinary selection repaints stay O(1). */
|
|
7623
|
+
this.assignmentRowsCache = null;
|
|
7624
|
+
/** The markup currently in the rail. An identical repaint is skipped, which is
|
|
7625
|
+
* what keeps the organizer's scroll position (and open <select>) alive. */
|
|
7626
|
+
this.railHtml = null;
|
|
7627
|
+
/**
|
|
7628
|
+
* The `assignmentVersion` the allocation map was built from. Walking every
|
|
7629
|
+
* allocation page is the expensive half of a refresh and the server already
|
|
7630
|
+
* tells us, in the channels response, whether ANY seat moved. Unchanged
|
|
7631
|
+
* version, unchanged allocation — so the walk is skipped entirely.
|
|
7632
|
+
*/
|
|
7633
|
+
this.allocationVersion = null;
|
|
7634
|
+
this.onVisibility = null;
|
|
7593
7635
|
this.host = host;
|
|
7594
7636
|
this.caps = capabilities;
|
|
7595
7637
|
}
|
|
@@ -7600,6 +7642,8 @@ var ChannelsMode = class {
|
|
|
7600
7642
|
this.active = true;
|
|
7601
7643
|
this.mapIntent = "pan";
|
|
7602
7644
|
this.focusedSectionId = null;
|
|
7645
|
+
this.assignmentRowsCache = null;
|
|
7646
|
+
this.railHtml = null;
|
|
7603
7647
|
this.ensureLayer();
|
|
7604
7648
|
this.host.root.classList.add("ch-mode");
|
|
7605
7649
|
this.applySheetClasses();
|
|
@@ -7608,8 +7652,15 @@ var ChannelsMode = class {
|
|
|
7608
7652
|
this.onInteractionChange?.();
|
|
7609
7653
|
void this.refresh();
|
|
7610
7654
|
this.pollTimer = setInterval(() => {
|
|
7655
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
7611
7656
|
void this.refresh({ quiet: true });
|
|
7612
7657
|
}, POLL_MS);
|
|
7658
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
7659
|
+
this.onVisibility = () => {
|
|
7660
|
+
if (this.active && !document.hidden) void this.refresh({ quiet: true });
|
|
7661
|
+
};
|
|
7662
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
7663
|
+
}
|
|
7613
7664
|
}
|
|
7614
7665
|
/** Called when the cockpit leaves Channels mode. Everything this mode painted
|
|
7615
7666
|
* over the map goes with it — no other tool ever inherits a channel overlay. */
|
|
@@ -7618,6 +7669,11 @@ var ChannelsMode = class {
|
|
|
7618
7669
|
this.active = false;
|
|
7619
7670
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
7620
7671
|
this.pollTimer = null;
|
|
7672
|
+
if (this.onVisibility && typeof document !== "undefined") {
|
|
7673
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
7674
|
+
}
|
|
7675
|
+
this.onVisibility = null;
|
|
7676
|
+
this.railHtml = null;
|
|
7621
7677
|
this.closeDialog({ restoreFocus: false });
|
|
7622
7678
|
this.links = [];
|
|
7623
7679
|
this.linksChannelId = null;
|
|
@@ -7706,8 +7762,11 @@ var ChannelsMode = class {
|
|
|
7706
7762
|
if (!this.targetChannelId) {
|
|
7707
7763
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
7708
7764
|
}
|
|
7709
|
-
|
|
7710
|
-
|
|
7765
|
+
if (this.allocationVersion !== list.assignmentVersion) {
|
|
7766
|
+
await this.loadAllocation(seq);
|
|
7767
|
+
if (superseded()) return;
|
|
7768
|
+
this.allocationVersion = list.assignmentVersion;
|
|
7769
|
+
}
|
|
7711
7770
|
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
7712
7771
|
if (superseded()) return;
|
|
7713
7772
|
this.loading = false;
|
|
@@ -8047,6 +8106,7 @@ var ChannelsMode = class {
|
|
|
8047
8106
|
}
|
|
8048
8107
|
const target = this.nameOf(this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8049
8108
|
const mutations = mutationCount(buckets);
|
|
8109
|
+
const tooLarge = labels.length > MAX_ASSIGNMENT_UNITS;
|
|
8050
8110
|
const skipped = buckets.skippedHeld.count + buckets.skippedBooked.count;
|
|
8051
8111
|
const parts = [
|
|
8052
8112
|
`<b>${labels.length.toLocaleString()}</b> selected`,
|
|
@@ -8058,7 +8118,9 @@ var ChannelsMode = class {
|
|
|
8058
8118
|
<span>${parts.join(" \xB7 ")}</span>
|
|
8059
8119
|
<span class="grow"></span>
|
|
8060
8120
|
<button type="button" class="drop" data-ch-act="discard">Discard</button>
|
|
8061
|
-
<button type="button" class="go" data-ch-act="review">
|
|
8121
|
+
<button type="button" class="go" data-ch-act="review"${tooLarge ? " disabled" : ""}>
|
|
8122
|
+
${tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : "Review changes"}
|
|
8123
|
+
</button>`);
|
|
8062
8124
|
this.stagedEl?.querySelectorAll("[data-ch-act]").forEach((button) => {
|
|
8063
8125
|
button.addEventListener("click", () => {
|
|
8064
8126
|
if (button.dataset.chAct === "discard") this.host.clearSelection();
|
|
@@ -8126,35 +8188,59 @@ var ChannelsMode = class {
|
|
|
8126
8188
|
this.paintOverlay();
|
|
8127
8189
|
}
|
|
8128
8190
|
}
|
|
8191
|
+
/**
|
|
8192
|
+
* Replace the rail's markup — but only when it actually differs, and never at
|
|
8193
|
+
* the cost of where the organizer had scrolled to.
|
|
8194
|
+
*
|
|
8195
|
+
* The rail repaints on a clock, on every selection change and after every
|
|
8196
|
+
* mutation. Rewriting `innerHTML` each time resets `scrollTop`, which is
|
|
8197
|
+
* exactly what "the rail gets stuck" was: scroll down to Create channel, the
|
|
8198
|
+
* poll ticks, and the list snaps back to the top under the cursor. So skip the
|
|
8199
|
+
* write when the markup is byte-identical, and restore the offset when it is
|
|
8200
|
+
* not.
|
|
8201
|
+
*
|
|
8202
|
+
* Returns whether the DOM was rewritten. Callers must only re-wire listeners
|
|
8203
|
+
* when it was — a skipped paint keeps the old nodes AND their listeners, so
|
|
8204
|
+
* re-wiring would double every handler.
|
|
8205
|
+
*/
|
|
8206
|
+
setRailHtml(html) {
|
|
8207
|
+
if (html === this.railHtml) return false;
|
|
8208
|
+
const rail = this.host.rail;
|
|
8209
|
+
const scrollTop = rail.scrollTop;
|
|
8210
|
+
rail.innerHTML = html;
|
|
8211
|
+
this.railHtml = html;
|
|
8212
|
+
if (scrollTop) rail.scrollTop = scrollTop;
|
|
8213
|
+
return true;
|
|
8214
|
+
}
|
|
8129
8215
|
paintRail() {
|
|
8130
8216
|
if (!this.active) return;
|
|
8131
8217
|
const rail = this.host.rail;
|
|
8132
8218
|
if (!this.caps.view) {
|
|
8133
|
-
|
|
8134
|
-
<p class="slm-hint">You need channel-management permission on this event to see allocations.</p
|
|
8219
|
+
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8220
|
+
<p class="slm-hint">You need channel-management permission on this event to see allocations.</p>`);
|
|
8135
8221
|
return;
|
|
8136
8222
|
}
|
|
8137
8223
|
if (this.loading && !this.list) {
|
|
8138
|
-
|
|
8139
|
-
<div class="slm-empty">Loading allocations\u2026</div
|
|
8224
|
+
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8225
|
+
<div class="slm-empty">Loading allocations\u2026</div>`);
|
|
8140
8226
|
return;
|
|
8141
8227
|
}
|
|
8142
8228
|
if (!this.list && this.loadError) {
|
|
8143
|
-
|
|
8229
|
+
if (this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8144
8230
|
<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8145
8231
|
<span><b>Couldn't load sales channels.</b> Everything else on this event still works.
|
|
8146
|
-
<button type="button" data-ch-act="retry">Try again</button></span></div
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
|
|
8232
|
+
<button type="button" data-ch-act="retry">Try again</button></span></div>`)) {
|
|
8233
|
+
rail.querySelector('[data-ch-act="retry"]')?.addEventListener("click", () => {
|
|
8234
|
+
void this.refresh();
|
|
8235
|
+
});
|
|
8236
|
+
}
|
|
8150
8237
|
return;
|
|
8151
8238
|
}
|
|
8152
8239
|
const selection = this.host.selectionLabels();
|
|
8153
8240
|
const grab = this.host.isCompact() ? `<div class="slm-ch-grab"><span class="slm-ch-grabbar"></span></div>` : "";
|
|
8154
8241
|
const segment = this.viewSegmentHtml();
|
|
8155
8242
|
const body = this.view === "preview" ? this.previewRailHtml() : this.detailChannelId ? this.detailRailHtml(this.detailChannelId) : selection.length && this.caps.manage ? this.selectionRailHtml(selection) : this.listRailHtml();
|
|
8156
|
-
|
|
8157
|
-
this.wireRail();
|
|
8243
|
+
if (this.setRailHtml(`${grab}${segment}${body}`)) this.wireRail();
|
|
8158
8244
|
this.paintStagedBar();
|
|
8159
8245
|
}
|
|
8160
8246
|
viewSegmentHtml() {
|
|
@@ -8225,8 +8311,9 @@ var ChannelsMode = class {
|
|
|
8225
8311
|
const create = this.caps.manage ? `<button type="button" class="slm-btn ghost" style="width:100%" data-ch-act="create">+ Create channel</button>` : "";
|
|
8226
8312
|
const readOnly = this.caps.manage ? "" : `<p class="slm-note">You can see how inventory is allocated. Changing it needs channel-management permission.</p>`;
|
|
8227
8313
|
return `
|
|
8314
|
+
${this.caps.manage ? this.assignmentToolsHtml() : ""}
|
|
8228
8315
|
<p class="slm-eyebrow">Sales channels</p>
|
|
8229
|
-
<p class="slm-hint">
|
|
8316
|
+
<p class="slm-hint">Channel colours and names are only visible to organizers, never to buyers.</p>
|
|
8230
8317
|
<div class="slm-ch-list">${rows}</div>
|
|
8231
8318
|
${create}
|
|
8232
8319
|
${readOnly}
|
|
@@ -8235,6 +8322,38 @@ var ChannelsMode = class {
|
|
|
8235
8322
|
style="text-align:left">${this.showArchived ? "Hide" : "Show"} archived${archivedCount ? ` (${archivedCount})` : ""}</button>
|
|
8236
8323
|
</p>`;
|
|
8237
8324
|
}
|
|
8325
|
+
/**
|
|
8326
|
+
* Destination-first assignment controls.
|
|
8327
|
+
*
|
|
8328
|
+
* These used to live only inside the selection rail, which meant every route
|
|
8329
|
+
* into them was gated behind "select a seat on the map first" — the section,
|
|
8330
|
+
* row and category choosers were invisible until the organizer had already
|
|
8331
|
+
* done the work by hand. They now paint above the channel list too, so the
|
|
8332
|
+
* destination is chosen first and whole sections and rows are discoverable
|
|
8333
|
+
* without learning a hidden seat-first workflow.
|
|
8334
|
+
*/
|
|
8335
|
+
assignmentToolsHtml() {
|
|
8336
|
+
const options = this.assignableChannels().map((channel) => `<option value="${esc(channel.id)}"${channel.id === this.targetChannelId ? " selected" : ""}>${esc(channel.name)}</option>`).join("");
|
|
8337
|
+
const sections = this.host.sections().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-sections">Sections</button>' : "";
|
|
8338
|
+
const rows = this.assignmentRows().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-rows">Rows</button>' : "";
|
|
8339
|
+
const dragClass = this.mapIntent === "assign" ? "slm-btn" : "slm-btn ghost";
|
|
8340
|
+
return `
|
|
8341
|
+
<p class="slm-eyebrow">Assign inventory</p>
|
|
8342
|
+
<div class="slm-field">
|
|
8343
|
+
<label for="slm-ch-target">Assign to</label>
|
|
8344
|
+
<select class="slm-select" id="slm-ch-target" data-ch-target>${options}</select>
|
|
8345
|
+
</div>
|
|
8346
|
+
<p class="slm-eyebrow" style="margin-top:12px">Select by</p>
|
|
8347
|
+
<div class="slm-ch-row2" style="margin-top:2px">
|
|
8348
|
+
${sections}${rows}
|
|
8349
|
+
<button type="button" class="${dragClass}" data-ch-act="drag-select">Drag box</button>
|
|
8350
|
+
</div>
|
|
8351
|
+
<div class="slm-ch-row2">
|
|
8352
|
+
<button type="button" class="slm-btn ghost" data-ch-act="pick-category">Category</button>
|
|
8353
|
+
<button type="button" class="slm-btn ghost" data-ch-act="seatlist">Seat list \u2328</button>
|
|
8354
|
+
</div>
|
|
8355
|
+
<p class="slm-note">Choose a destination, then add whole sections, multiple rows, a dragged area, or individual seats.</p>`;
|
|
8356
|
+
}
|
|
8238
8357
|
selectionRailHtml(selection) {
|
|
8239
8358
|
const sources = selectionSources(selection, this.allocation, this.list);
|
|
8240
8359
|
const conflict = this.conflict ? `<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
@@ -8243,34 +8362,27 @@ var ChannelsMode = class {
|
|
|
8243
8362
|
<button type="button" data-ch-act="refresh-review">Refresh and review \u2192</button></span></div>` : "";
|
|
8244
8363
|
const bump = selection.length !== this.lastSelectionCount ? " bump" : "";
|
|
8245
8364
|
this.lastSelectionCount = selection.length;
|
|
8246
|
-
const
|
|
8365
|
+
const tooLarge = selection.length > MAX_ASSIGNMENT_UNITS;
|
|
8247
8366
|
const sourceRows = sources.map((row) => {
|
|
8248
8367
|
const marker = this.markerFor(row.channelId);
|
|
8249
8368
|
return `<div class="slm-ch-selsrc-row">
|
|
8250
8369
|
<span class="mk" style="background:${esc(marker.color)}" aria-hidden="true">${esc(marker.letter)}</span>
|
|
8251
8370
|
<b>${row.count.toLocaleString()}</b><span>${esc(row.name)}</span></div>`;
|
|
8252
8371
|
}).join("");
|
|
8253
|
-
const sectionSelect = this.host.sections().length ? `<button type="button" class="slm-btn ghost" data-ch-act="pick-section">Section</button>` : "";
|
|
8254
8372
|
return `
|
|
8255
8373
|
${conflict}
|
|
8374
|
+
${this.assignmentToolsHtml()}
|
|
8256
8375
|
<div class="slm-selbar"><span class="slm-selnum slm-ch-selnum${bump}">${selection.length.toLocaleString()}</span>
|
|
8257
8376
|
<span class="slm-sellabel">selected</span></div>
|
|
8258
8377
|
<div class="slm-ch-selsrc" aria-live="polite" aria-label="Selection sources">${sourceRows}</div>
|
|
8259
|
-
|
|
8260
|
-
<
|
|
8261
|
-
|
|
8262
|
-
</div>
|
|
8378
|
+
${tooLarge ? `<div class="slm-ch-alert warn" role="alert"><span>\u2139</span><span>
|
|
8379
|
+
<b>This selection is too large to apply at once.</b> Choose at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()}
|
|
8380
|
+
seats, for example by splitting the venue into row groups.</span></div>` : ""}
|
|
8263
8381
|
<p class="slm-note">Changes are staged \u2014 nothing moves until you review and apply.
|
|
8264
8382
|
Seats in checkout or already sold are never moved.</p>
|
|
8265
8383
|
<div class="slm-ch-row2">
|
|
8266
8384
|
<button type="button" class="slm-btn ghost" data-ch-act="discard">Clear selection</button>
|
|
8267
|
-
<button type="button" class="slm-btn" data-ch-act="review">Review changes</button>
|
|
8268
|
-
</div>
|
|
8269
|
-
<p class="slm-eyebrow" style="margin-top:18px">Select by</p>
|
|
8270
|
-
<div class="slm-ch-row2" style="margin-top:2px">
|
|
8271
|
-
${sectionSelect}
|
|
8272
|
-
<button type="button" class="slm-btn ghost" data-ch-act="pick-category">Category</button>
|
|
8273
|
-
<button type="button" class="slm-btn ghost" data-ch-act="seatlist">List \u2328</button>
|
|
8385
|
+
<button type="button" class="slm-btn" data-ch-act="review"${tooLarge ? " disabled" : ""}>Review changes</button>
|
|
8274
8386
|
</div>
|
|
8275
8387
|
<p class="slm-note">The seat list offers the same selection with checkboxes for keyboard and screen-reader use.</p>`;
|
|
8276
8388
|
}
|
|
@@ -8285,22 +8397,7 @@ var ChannelsMode = class {
|
|
|
8285
8397
|
<button type="button" class="slm-btn ghost" data-ch-act="archive">Archive\u2026</button>
|
|
8286
8398
|
</div>
|
|
8287
8399
|
<p class="slm-note">Archive returns the allocation to a destination you choose. Nothing is ever deleted silently.</p>` : "";
|
|
8288
|
-
const
|
|
8289
|
-
const intents = ["none", "internal", "server", "hosted_link"];
|
|
8290
|
-
const selfServiceGap = (intent === "server" || intent === "hosted_link") && !channel.access?.hasActiveGrants;
|
|
8291
|
-
const access = this.caps.manage ? `
|
|
8292
|
-
<p class="slm-eyebrow" style="margin-top:14px">Buyer access</p>
|
|
8293
|
-
<div class="slm-field">
|
|
8294
|
-
<label for="slm-ch-intent">How should buyers reach this channel?</label>
|
|
8295
|
-
<select class="slm-select" id="slm-ch-intent" data-ch-intent>
|
|
8296
|
-
${intents.map((value) => `<option value="${value}"${value === intent ? " selected" : ""}>${esc(accessIntentLabel(value))}</option>`).join("")}
|
|
8297
|
-
</select>
|
|
8298
|
-
</div>
|
|
8299
|
-
<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>
|
|
8300
|
-
${selfServiceGap ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
8301
|
-
<span>This channel is marked for buyer self-service but no buyer has been let in yet.</span></div>` : ""}
|
|
8302
|
-
${this.hostedLinksHtml()}
|
|
8303
|
-
${SERVER_INTEGRATION_HTML}` : "";
|
|
8400
|
+
const access = this.caps.manage ? this.distributeHtml(channel) : "";
|
|
8304
8401
|
return `
|
|
8305
8402
|
<p class="slm-eyebrow">
|
|
8306
8403
|
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
@@ -8310,7 +8407,54 @@ var ChannelsMode = class {
|
|
|
8310
8407
|
${access}
|
|
8311
8408
|
${lifecycle}`;
|
|
8312
8409
|
}
|
|
8313
|
-
|
|
8410
|
+
/**
|
|
8411
|
+
* "Distribute" — how the seats in this channel actually reach a buyer.
|
|
8412
|
+
*
|
|
8413
|
+
* This replaced a four-value "access intent" picker (none / internal /
|
|
8414
|
+
* hosted_link / server). Two of those values did nothing anywhere: no buyer or
|
|
8415
|
+
* inventory path reads `access_intent`, so `none` and `internal` were labels an
|
|
8416
|
+
* organizer could set and then wait forever for something to happen. A third,
|
|
8417
|
+
* `hosted_link`, is not the organizer's to choose at all — the server sets it
|
|
8418
|
+
* when a buyer link is created and clears it when the last live one is revoked.
|
|
8419
|
+
*
|
|
8420
|
+
* So this is two ACTIONS, not a setting: create a buyer link, or point the
|
|
8421
|
+
* channel at a website integration. Both of them do something the moment they
|
|
8422
|
+
* are pressed. A legacy row still carrying `none` or `internal` renders the
|
|
8423
|
+
* neutral "not distributed yet" state with both actions offered — the stored
|
|
8424
|
+
* value is left alone (it is organizer-declared metadata and the API that
|
|
8425
|
+
* writes it is unchanged), it simply no longer has a control of its own.
|
|
8426
|
+
*/
|
|
8427
|
+
distributeHtml(channel) {
|
|
8428
|
+
const intent = channel.access?.intent ?? "none";
|
|
8429
|
+
const live = channel.access?.hasActiveGrants === true;
|
|
8430
|
+
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.";
|
|
8431
|
+
const option = (title, why, action, cta, primary) => `<div class="slm-ch-dist">
|
|
8432
|
+
<b>${esc(title)}</b>
|
|
8433
|
+
<span class="why">${esc(why)}</span>
|
|
8434
|
+
<button type="button" class="slm-btn${primary ? "" : " ghost"}" data-ch-act="${esc(action)}">${esc(cta)}</button>
|
|
8435
|
+
</div>`;
|
|
8436
|
+
const linksSupported = this.linksState !== "unsupported";
|
|
8437
|
+
return `
|
|
8438
|
+
<p class="slm-eyebrow" style="margin-top:14px">Distribute</p>
|
|
8439
|
+
<p class="slm-hint">${esc(state)}</p>
|
|
8440
|
+
${linksSupported ? option(
|
|
8441
|
+
"Sell with a buyer link",
|
|
8442
|
+
"SeatLayer makes a link you send to a named group. They open it and buy only these seats.",
|
|
8443
|
+
"link-create",
|
|
8444
|
+
this.links.some(accessLinkIsLive) ? "Create another buyer link" : "Create buyer link",
|
|
8445
|
+
true
|
|
8446
|
+
) : ""}
|
|
8447
|
+
${option(
|
|
8448
|
+
"Integrate a website or app",
|
|
8449
|
+
"Your website's backend grants each buyer access \u2014 set up on the Embed page.",
|
|
8450
|
+
"embed-code",
|
|
8451
|
+
"Get embed code",
|
|
8452
|
+
false
|
|
8453
|
+
)}
|
|
8454
|
+
${this.hostedLinksHtml()}
|
|
8455
|
+
${intent === "server" ? SERVER_INTEGRATION_HTML : ""}`;
|
|
8456
|
+
}
|
|
8457
|
+
// ---- buyer links ----------------------------------------------------------
|
|
8314
8458
|
/**
|
|
8315
8459
|
* Read the status projection for the open channel. Never paints — the caller
|
|
8316
8460
|
* decides when the rail repaints, so a poll-driven reload does not fight a
|
|
@@ -8340,32 +8484,34 @@ var ChannelsMode = class {
|
|
|
8340
8484
|
}
|
|
8341
8485
|
}
|
|
8342
8486
|
/**
|
|
8343
|
-
* The
|
|
8487
|
+
* The buyer-link status section of the detail panel.
|
|
8344
8488
|
*
|
|
8345
8489
|
* STATUS ONLY, by design (comp 06 `hosted`): label, state, expiry,
|
|
8346
8490
|
* redemptions, seats per buyer, live sessions. There is no Copy control here
|
|
8347
8491
|
* and no field to hang one on — the URL was shown once at creation and cannot
|
|
8348
8492
|
* be produced again. Rotation is the recovery path, and it says so.
|
|
8493
|
+
*
|
|
8494
|
+
* Creating a link is the Distribute card's action, not this section's, so a
|
|
8495
|
+
* channel with no links renders nothing here rather than an empty heading and
|
|
8496
|
+
* a second button saying the same thing.
|
|
8349
8497
|
*/
|
|
8350
8498
|
hostedLinksHtml() {
|
|
8351
|
-
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">
|
|
8499
|
+
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Buyer links</p>`;
|
|
8352
8500
|
if (this.linksState === "unsupported") {
|
|
8353
8501
|
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
8354
|
-
<span><b>
|
|
8502
|
+
<span><b>Buyer links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
8355
8503
|
}
|
|
8356
8504
|
if (this.linksState === "error") {
|
|
8357
8505
|
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8358
8506
|
<span><b>Couldn't load this channel's links.</b>
|
|
8359
8507
|
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
8360
8508
|
}
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
${live ? "Create another hosted link" : "Create hosted access link"}</button>` : "";
|
|
8509
|
+
if (this.linksState === "loading" && !this.links.length) {
|
|
8510
|
+
return `${eyebrow}<div class="slm-empty">Loading links\u2026</div>`;
|
|
8511
|
+
}
|
|
8512
|
+
if (!this.links.length) return "";
|
|
8366
8513
|
return `${eyebrow}
|
|
8367
|
-
${
|
|
8368
|
-
${create}
|
|
8514
|
+
${this.links.map((link) => this.linkCardHtml(link)).join("")}
|
|
8369
8515
|
<p class="slm-note">A link is shown once, when you create it. SeatLayer keeps only a fingerprint of it, so it
|
|
8370
8516
|
can never be shown again \u2014 if a link is lost, rotate it and send the fresh one.</p>`;
|
|
8371
8517
|
}
|
|
@@ -8384,7 +8530,7 @@ var ChannelsMode = class {
|
|
|
8384
8530
|
</div>` : "";
|
|
8385
8531
|
return `<div class="slm-ch-link">
|
|
8386
8532
|
<span class="lk-head">
|
|
8387
|
-
<span class="lk-name">${esc(link.label || "
|
|
8533
|
+
<span class="lk-name">${esc(link.label || "Buyer link")}</span>
|
|
8388
8534
|
<span class="slm-ch-badge ${badge.kind}">${esc(badge.text)}</span>
|
|
8389
8535
|
</span>
|
|
8390
8536
|
<div class="slm-ch-meter" role="img"
|
|
@@ -8487,10 +8633,6 @@ var ChannelsMode = class {
|
|
|
8487
8633
|
this.previewIncludePublic = includePublic.checked;
|
|
8488
8634
|
void this.loadPreview();
|
|
8489
8635
|
});
|
|
8490
|
-
const intent = rail.querySelector("[data-ch-intent]");
|
|
8491
|
-
intent?.addEventListener("change", () => {
|
|
8492
|
-
void this.setAccessIntent(intent.value);
|
|
8493
|
-
});
|
|
8494
8636
|
const grab = rail.querySelector(".slm-ch-grab");
|
|
8495
8637
|
grab?.addEventListener("click", () => this.cycleDetent());
|
|
8496
8638
|
rail.querySelectorAll("[data-ch-act]").forEach((button) => {
|
|
@@ -8535,6 +8677,13 @@ var ChannelsMode = class {
|
|
|
8535
8677
|
case "link-create":
|
|
8536
8678
|
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
8537
8679
|
break;
|
|
8680
|
+
// The one thing this screen can actually do for a website integration is
|
|
8681
|
+
// record that the channel is meant for one — which is exactly the flag the
|
|
8682
|
+
// dashboard's Embed page reads before it offers the snippet. Saying so is
|
|
8683
|
+
// honest; a fake "copy code" button on a screen with no code would not be.
|
|
8684
|
+
case "embed-code":
|
|
8685
|
+
void this.chooseWebsiteIntegration();
|
|
8686
|
+
break;
|
|
8538
8687
|
case "link-reload":
|
|
8539
8688
|
if (this.detailChannelId) void this.reloadLinks();
|
|
8540
8689
|
break;
|
|
@@ -8549,8 +8698,16 @@ var ChannelsMode = class {
|
|
|
8549
8698
|
this.conflict = false;
|
|
8550
8699
|
void this.refresh().then(() => this.openDialog({ kind: "review" }));
|
|
8551
8700
|
break;
|
|
8552
|
-
case "pick-
|
|
8553
|
-
this.
|
|
8701
|
+
case "pick-sections":
|
|
8702
|
+
this.openDialog({ kind: "scope", scope: "sections" });
|
|
8703
|
+
break;
|
|
8704
|
+
case "pick-rows":
|
|
8705
|
+
this.openDialog({ kind: "scope", scope: "rows" });
|
|
8706
|
+
break;
|
|
8707
|
+
case "drag-select":
|
|
8708
|
+
this.mapIntent = "assign";
|
|
8709
|
+
this.paintRail();
|
|
8710
|
+
this.onInteractionChange?.();
|
|
8554
8711
|
break;
|
|
8555
8712
|
case "pick-category":
|
|
8556
8713
|
this.pickCategory();
|
|
@@ -8560,12 +8717,10 @@ var ChannelsMode = class {
|
|
|
8560
8717
|
}
|
|
8561
8718
|
}
|
|
8562
8719
|
// ---- selection helpers ----------------------------------------------------
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
if (!
|
|
8566
|
-
this.
|
|
8567
|
-
this.host.selectSection(value);
|
|
8568
|
-
});
|
|
8720
|
+
/** Derived once per mode entry — see `assignmentRowsCache`. */
|
|
8721
|
+
assignmentRows() {
|
|
8722
|
+
if (!this.assignmentRowsCache) this.assignmentRowsCache = this.host.rows();
|
|
8723
|
+
return this.assignmentRowsCache;
|
|
8569
8724
|
}
|
|
8570
8725
|
pickCategory() {
|
|
8571
8726
|
const categories = this.host.categories();
|
|
@@ -8596,6 +8751,209 @@ var ChannelsMode = class {
|
|
|
8596
8751
|
});
|
|
8597
8752
|
});
|
|
8598
8753
|
}
|
|
8754
|
+
/**
|
|
8755
|
+
* Add several whole sections, or several whole rows, in one operation.
|
|
8756
|
+
*
|
|
8757
|
+
* ADDITIVE by contract: the chooser starts from the labels already selected
|
|
8758
|
+
* and only ever grows that set, so opening it never destroys a hard-won
|
|
8759
|
+
* marquee or seat-list selection. The confirm button always states the net
|
|
8760
|
+
* number of seats it will add, and refuses to exceed `MAX_ASSIGNMENT_UNITS`.
|
|
8761
|
+
*
|
|
8762
|
+
* Rows get the richer variant. A large venue has thousands of them, so they
|
|
8763
|
+
* arrive collapsed under their section, with a section-level tri-state
|
|
8764
|
+
* checkbox, a search across every row, and a render cap — the flat list used
|
|
8765
|
+
* for sections would be an unusable wall of buttons.
|
|
8766
|
+
*/
|
|
8767
|
+
renderScopeDialog(state) {
|
|
8768
|
+
const scope = state.scope === "rows" ? "rows" : "sections";
|
|
8769
|
+
const options = scope === "sections" ? this.host.sections().map((section) => ({
|
|
8770
|
+
id: section.id,
|
|
8771
|
+
label: section.label,
|
|
8772
|
+
group: "Sections",
|
|
8773
|
+
labels: this.host.labelsInSection(section.id)
|
|
8774
|
+
})) : this.assignmentRows().map((row) => ({
|
|
8775
|
+
id: row.id,
|
|
8776
|
+
label: row.label,
|
|
8777
|
+
group: row.sectionLabel,
|
|
8778
|
+
labels: row.labels
|
|
8779
|
+
}));
|
|
8780
|
+
const available = options.filter((option) => option.labels.length > 0);
|
|
8781
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
8782
|
+
for (const option of available) {
|
|
8783
|
+
const group = grouped.get(option.group) ?? [];
|
|
8784
|
+
group.push(option);
|
|
8785
|
+
grouped.set(option.group, group);
|
|
8786
|
+
}
|
|
8787
|
+
if (scope === "rows") {
|
|
8788
|
+
const groups = [...grouped.entries()];
|
|
8789
|
+
this.renderScrim(`
|
|
8790
|
+
<h3 id="slm-ch-dlg-title">Add multiple rows</h3>
|
|
8791
|
+
<p class="sub">Sections stay collapsed for speed. Select a whole section, expand only the rows you need, or search across every row.</p>
|
|
8792
|
+
<div class="slm-ch-scopebar">
|
|
8793
|
+
<label>Find section or row<input type="search" data-ch-scope-search placeholder="e.g. Orchestra or AA"></label>
|
|
8794
|
+
<span class="slm-ch-scopesummary" data-ch-scope-summary aria-live="polite">0 rows selected</span>
|
|
8795
|
+
</div>
|
|
8796
|
+
<p class="slm-ch-scopehint" data-ch-scope-filter>Showing section groups. Expand one or search to see rows.</p>
|
|
8797
|
+
<div class="slm-ch-seatlist" data-ch-scope-list></div>
|
|
8798
|
+
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8799
|
+
<div class="foot">
|
|
8800
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8801
|
+
<button type="button" class="slm-btn" data-ch-add-scope disabled>Add seats</button>
|
|
8802
|
+
</div>`, (dialog) => {
|
|
8803
|
+
const byId = new Map(available.map((option) => [option.id, option]));
|
|
8804
|
+
const picked = /* @__PURE__ */ new Set();
|
|
8805
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
8806
|
+
const current = new Set(this.host.selectionLabels());
|
|
8807
|
+
const confirm = dialog.querySelector("[data-ch-add-scope]");
|
|
8808
|
+
const error = dialog.querySelector("[data-ch-error]");
|
|
8809
|
+
const list = dialog.querySelector("[data-ch-scope-list]");
|
|
8810
|
+
const search = dialog.querySelector("[data-ch-scope-search]");
|
|
8811
|
+
const summary = dialog.querySelector("[data-ch-scope-summary]");
|
|
8812
|
+
const filterHint = dialog.querySelector("[data-ch-scope-filter]");
|
|
8813
|
+
const renderLimit = 200;
|
|
8814
|
+
let query = "";
|
|
8815
|
+
const selectedLabels = () => {
|
|
8816
|
+
const labels = new Set(current);
|
|
8817
|
+
for (const id of picked) for (const label of byId.get(id)?.labels ?? []) labels.add(label);
|
|
8818
|
+
return labels;
|
|
8819
|
+
};
|
|
8820
|
+
const update = () => {
|
|
8821
|
+
const labels = selectedLabels();
|
|
8822
|
+
const added = labels.size - current.size;
|
|
8823
|
+
const tooLarge = labels.size > MAX_ASSIGNMENT_UNITS;
|
|
8824
|
+
confirm.disabled = added === 0 || tooLarge;
|
|
8825
|
+
confirm.textContent = tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : `Add ${added.toLocaleString()} seat${added === 1 ? "" : "s"}`;
|
|
8826
|
+
error.hidden = !tooLarge;
|
|
8827
|
+
error.textContent = tooLarge ? `That would make ${labels.size.toLocaleString()} selected seats. Choose fewer rows or sections.` : "";
|
|
8828
|
+
summary.textContent = `${picked.size.toLocaleString()} row${picked.size === 1 ? "" : "s"} \xB7 ${added.toLocaleString()} seat${added === 1 ? "" : "s"} added`;
|
|
8829
|
+
};
|
|
8830
|
+
const renderList = () => {
|
|
8831
|
+
const blocks = [];
|
|
8832
|
+
let totalMatches = 0;
|
|
8833
|
+
let rendered = 0;
|
|
8834
|
+
groups.forEach(([group, items], groupIndex) => {
|
|
8835
|
+
const normalizedGroup = group.toLocaleLowerCase();
|
|
8836
|
+
const matches = query ? items.filter((item) => `${normalizedGroup} ${item.label.toLocaleLowerCase()}`.includes(query)) : items;
|
|
8837
|
+
if (query && !matches.length) return;
|
|
8838
|
+
totalMatches += matches.length;
|
|
8839
|
+
const allPicked = items.every((item) => picked.has(item.id));
|
|
8840
|
+
const somePicked = !allPicked && items.some((item) => picked.has(item.id));
|
|
8841
|
+
const open = Boolean(query) || expanded.has(groupIndex);
|
|
8842
|
+
const room = Math.max(0, renderLimit - rendered);
|
|
8843
|
+
const visible = open ? matches.slice(0, room) : [];
|
|
8844
|
+
rendered += visible.length;
|
|
8845
|
+
const seatCount = items.reduce((sum, item) => sum + item.labels.length, 0);
|
|
8846
|
+
blocks.push(`<div class="slm-ch-scopegroup">
|
|
8847
|
+
<button type="button" class="slm-ch-groupcheck" role="checkbox" aria-checked="${allPicked ? "true" : somePicked ? "mixed" : "false"}"
|
|
8848
|
+
aria-label="Select all ${items.length.toLocaleString()} rows in ${esc(group)}" data-ch-scope-group="${groupIndex}">
|
|
8849
|
+
<span class="box" aria-hidden="true">\u2713</span><span>${esc(group)}</span>
|
|
8850
|
+
<span class="meta">${items.length.toLocaleString()} rows \xB7 ${seatCount.toLocaleString()} seats</span>
|
|
8851
|
+
</button>
|
|
8852
|
+
<button type="button" class="slm-ch-grouptoggle" aria-expanded="${open}" aria-label="${open ? "Hide" : "Show"} rows in ${esc(group)}"
|
|
8853
|
+
data-ch-scope-toggle="${groupIndex}">${open ? "Hide" : "Show"}</button>
|
|
8854
|
+
</div>`);
|
|
8855
|
+
blocks.push(...visible.map((option) => `<button type="button" class="slm-ch-seatitem" role="checkbox"
|
|
8856
|
+
aria-checked="${picked.has(option.id)}" data-ch-scope-id="${esc(option.id)}">
|
|
8857
|
+
<span class="box" aria-hidden="true">\u2713</span><span>${esc(option.label)}</span>
|
|
8858
|
+
<span class="meta">${option.labels.length.toLocaleString()} seat${option.labels.length === 1 ? "" : "s"}</span>
|
|
8859
|
+
</button>`));
|
|
8860
|
+
if (open && matches.length > visible.length) {
|
|
8861
|
+
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>`);
|
|
8862
|
+
}
|
|
8863
|
+
});
|
|
8864
|
+
list.innerHTML = blocks.join("") || `<div class="slm-ch-scope-empty">No sections or rows match \u201C${esc(query)}\u201D.</div>`;
|
|
8865
|
+
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.";
|
|
8866
|
+
list.querySelectorAll("[data-ch-scope-group]").forEach((button) => button.addEventListener("click", () => {
|
|
8867
|
+
const items = groups[Number(button.dataset.chScopeGroup)]?.[1] ?? [];
|
|
8868
|
+
const remove = items.every((item) => picked.has(item.id));
|
|
8869
|
+
for (const item of items) remove ? picked.delete(item.id) : picked.add(item.id);
|
|
8870
|
+
renderList();
|
|
8871
|
+
update();
|
|
8872
|
+
}));
|
|
8873
|
+
list.querySelectorAll("[data-ch-scope-toggle]").forEach((button) => button.addEventListener("click", () => {
|
|
8874
|
+
const index = Number(button.dataset.chScopeToggle);
|
|
8875
|
+
if (expanded.has(index)) expanded.delete(index);
|
|
8876
|
+
else expanded.add(index);
|
|
8877
|
+
renderList();
|
|
8878
|
+
}));
|
|
8879
|
+
list.querySelectorAll("[data-ch-scope-id]").forEach((button) => button.addEventListener("click", () => {
|
|
8880
|
+
const id = button.dataset.chScopeId;
|
|
8881
|
+
if (picked.has(id)) picked.delete(id);
|
|
8882
|
+
else picked.add(id);
|
|
8883
|
+
renderList();
|
|
8884
|
+
update();
|
|
8885
|
+
}));
|
|
8886
|
+
};
|
|
8887
|
+
search.addEventListener("input", () => {
|
|
8888
|
+
query = search.value.trim().toLocaleLowerCase();
|
|
8889
|
+
renderList();
|
|
8890
|
+
update();
|
|
8891
|
+
});
|
|
8892
|
+
confirm.addEventListener("click", () => {
|
|
8893
|
+
const labels = [...selectedLabels()];
|
|
8894
|
+
if (!picked.size || labels.length > MAX_ASSIGNMENT_UNITS) return;
|
|
8895
|
+
this.closeDialog();
|
|
8896
|
+
this.host.selectByLabels(labels);
|
|
8897
|
+
});
|
|
8898
|
+
renderList();
|
|
8899
|
+
update();
|
|
8900
|
+
});
|
|
8901
|
+
return;
|
|
8902
|
+
}
|
|
8903
|
+
const body = [...grouped.entries()].map(([group, items]) => `
|
|
8904
|
+
<div class="slm-ch-seatgroup"><span>${esc(group)}</span></div>
|
|
8905
|
+
${items.map((option) => `<button type="button" class="slm-ch-seatitem" role="checkbox"
|
|
8906
|
+
aria-checked="false" data-ch-scope-id="${esc(option.id)}">
|
|
8907
|
+
<span class="box" aria-hidden="true">\u2713</span>
|
|
8908
|
+
<span>${esc(option.label)}</span>
|
|
8909
|
+
<span class="meta">${option.labels.length.toLocaleString()} seat${option.labels.length === 1 ? "" : "s"}</span>
|
|
8910
|
+
</button>`).join("")}`).join("");
|
|
8911
|
+
this.renderScrim(`
|
|
8912
|
+
<h3 id="slm-ch-dlg-title">Add whole sections</h3>
|
|
8913
|
+
<p class="sub">Choose one or more. They are added to the seats already selected on the map.</p>
|
|
8914
|
+
<div class="slm-ch-seatlist">${body || `<div class="slm-empty">No sections are available.</div>`}</div>
|
|
8915
|
+
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8916
|
+
<div class="foot">
|
|
8917
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8918
|
+
<button type="button" class="slm-btn" data-ch-add-scope disabled>Add seats</button>
|
|
8919
|
+
</div>`, (dialog) => {
|
|
8920
|
+
const byId = new Map(available.map((option) => [option.id, option]));
|
|
8921
|
+
const picked = /* @__PURE__ */ new Set();
|
|
8922
|
+
const current = new Set(this.host.selectionLabels());
|
|
8923
|
+
const confirm = dialog.querySelector("[data-ch-add-scope]");
|
|
8924
|
+
const error = dialog.querySelector("[data-ch-error]");
|
|
8925
|
+
const selectedLabels = () => {
|
|
8926
|
+
const labels = new Set(current);
|
|
8927
|
+
for (const id of picked) for (const label of byId.get(id)?.labels ?? []) labels.add(label);
|
|
8928
|
+
return labels;
|
|
8929
|
+
};
|
|
8930
|
+
const update = () => {
|
|
8931
|
+
const labels = selectedLabels();
|
|
8932
|
+
const added = labels.size - current.size;
|
|
8933
|
+
const tooLarge = labels.size > MAX_ASSIGNMENT_UNITS;
|
|
8934
|
+
confirm.disabled = added === 0 || tooLarge;
|
|
8935
|
+
confirm.textContent = tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : `Add ${added.toLocaleString()} seat${added === 1 ? "" : "s"}`;
|
|
8936
|
+
error.hidden = !tooLarge;
|
|
8937
|
+
error.textContent = tooLarge ? `That would make ${labels.size.toLocaleString()} selected seats. Choose fewer rows or sections.` : "";
|
|
8938
|
+
};
|
|
8939
|
+
dialog.querySelectorAll("[data-ch-scope-id]").forEach((button) => {
|
|
8940
|
+
button.addEventListener("click", () => {
|
|
8941
|
+
const id = button.dataset.chScopeId;
|
|
8942
|
+
if (picked.has(id)) picked.delete(id);
|
|
8943
|
+
else picked.add(id);
|
|
8944
|
+
button.setAttribute("aria-checked", String(picked.has(id)));
|
|
8945
|
+
update();
|
|
8946
|
+
});
|
|
8947
|
+
});
|
|
8948
|
+
confirm.addEventListener("click", () => {
|
|
8949
|
+
const labels = [...selectedLabels()];
|
|
8950
|
+
if (!picked.size || labels.length > MAX_ASSIGNMENT_UNITS) return;
|
|
8951
|
+
this.closeDialog();
|
|
8952
|
+
this.host.selectByLabels(labels);
|
|
8953
|
+
});
|
|
8954
|
+
update();
|
|
8955
|
+
});
|
|
8956
|
+
}
|
|
8599
8957
|
// ---- dialogs --------------------------------------------------------------
|
|
8600
8958
|
openDialog(state) {
|
|
8601
8959
|
this.dialog = state;
|
|
@@ -8606,6 +8964,7 @@ var ChannelsMode = class {
|
|
|
8606
8964
|
if (!state) return;
|
|
8607
8965
|
if (state.kind === "create") this.renderCreateDialog(state);
|
|
8608
8966
|
else if (state.kind === "review") this.renderReviewDialog(state);
|
|
8967
|
+
else if (state.kind === "scope") this.renderScopeDialog(state);
|
|
8609
8968
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
8610
8969
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
8611
8970
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
@@ -8750,24 +9109,28 @@ var ChannelsMode = class {
|
|
|
8750
9109
|
const { labels, buckets } = this.currentPlan();
|
|
8751
9110
|
const authoritative = state.applied;
|
|
8752
9111
|
const shown = authoritative ? authoritative.buckets : buckets;
|
|
8753
|
-
const targetName = this.nameOf(this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
9112
|
+
const targetName = this.nameOf(authoritative?.targetChannelId ?? this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8754
9113
|
const rows = bucketRows(shown, targetName);
|
|
8755
9114
|
const mutations = authoritative ? authoritative.applied : mutationCount(buckets);
|
|
8756
9115
|
const allSkipped = !authoritative && labels.length > 0 && mutations === 0;
|
|
9116
|
+
const tooLarge = !authoritative && labels.length > MAX_ASSIGNMENT_UNITS;
|
|
8757
9117
|
const rowsHtml = bucketRowsHtml(rows);
|
|
8758
9118
|
const foot = authoritative ? `<div class="foot"><button type="button" class="slm-btn" data-ch-close>Done</button></div>` : `<div class="foot">
|
|
8759
9119
|
<button type="button" class="quiet" data-ch-close>Back</button>
|
|
8760
|
-
<button type="button" class="slm-btn" data-ch-apply ${allSkipped || state.busy ? "disabled" : ""}>
|
|
9120
|
+
<button type="button" class="slm-btn" data-ch-apply ${allSkipped || tooLarge || state.busy ? "disabled" : ""}>
|
|
8761
9121
|
${state.busy ? "Applying\u2026" : `Apply ${mutations.toLocaleString()} change${mutations === 1 ? "" : "s"}`}
|
|
8762
9122
|
</button>
|
|
8763
9123
|
</div>`;
|
|
8764
9124
|
const confirmNote = !authoritative && needsMoveConfirmation(buckets) ? `<p class="slm-note">Applying moves inventory out of another private channel. That is the line marked above.</p>` : "";
|
|
8765
9125
|
const skippedNote = allSkipped ? `<div class="slm-ch-alert warn"><span>\u2139</span><span>Nothing in this selection can move right now \u2014
|
|
8766
9126
|
every seat is in a buyer's checkout, already sold, or already in ${esc(targetName)}.</span></div>` : "";
|
|
9127
|
+
const limitNote = tooLarge ? `<div class="slm-ch-alert warn" role="alert"><span>\u2139</span><span><b>Choose fewer seats.</b>
|
|
9128
|
+
One Apply can cover at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats.</span></div>` : "";
|
|
8767
9129
|
this.renderScrim(`
|
|
8768
9130
|
<h3 id="slm-ch-dlg-title">${authoritative ? `Moved ${authoritative.applied.toLocaleString()} seat${authoritative.applied === 1 ? "" : "s"} to ${esc(targetName)}` : `Move ${labels.length.toLocaleString()} selected seat${labels.length === 1 ? "" : "s"} to ${esc(targetName)}`}</h3>
|
|
8769
9131
|
<p class="sub">${authoritative ? "These are the exact counts the server applied." : "Every selected seat is in exactly one line below."}</p>
|
|
8770
9132
|
${skippedNote}
|
|
9133
|
+
${limitNote}
|
|
8771
9134
|
${rowsHtml || '<div class="slm-empty">Nothing selected.</div>'}
|
|
8772
9135
|
${confirmNote}
|
|
8773
9136
|
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
@@ -8779,15 +9142,21 @@ var ChannelsMode = class {
|
|
|
8779
9142
|
}
|
|
8780
9143
|
}
|
|
8781
9144
|
/**
|
|
8782
|
-
* Apply. On success the
|
|
8783
|
-
*
|
|
8784
|
-
*
|
|
8785
|
-
*
|
|
9145
|
+
* Apply. On success the sheet CLOSES and the staged bar confirms what moved,
|
|
9146
|
+
* naming the destination and any skipped seats, with the AUTHORITATIVE server
|
|
9147
|
+
* buckets still one Details press away. Leaving the modal up on success made
|
|
9148
|
+
* the organizer dismiss a sheet to get back to a map they had just changed.
|
|
9149
|
+
* On a stale version the server mutated nothing: keep the selection, shake
|
|
9150
|
+
* the bar once, and offer exactly one action — Refresh and review.
|
|
8786
9151
|
*/
|
|
8787
9152
|
async apply() {
|
|
8788
9153
|
if (!this.dialog || !this.caps.manage) return;
|
|
8789
9154
|
const { labels } = this.currentPlan();
|
|
8790
9155
|
if (!labels.length) return;
|
|
9156
|
+
if (labels.length > MAX_ASSIGNMENT_UNITS) {
|
|
9157
|
+
this.showDialogError(`Choose at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats for one Apply.`);
|
|
9158
|
+
return;
|
|
9159
|
+
}
|
|
8791
9160
|
this.dialog = { ...this.dialog, busy: true, error: null };
|
|
8792
9161
|
this.renderDialog();
|
|
8793
9162
|
try {
|
|
@@ -8800,11 +9169,10 @@ var ChannelsMode = class {
|
|
|
8800
9169
|
});
|
|
8801
9170
|
this.assignmentVersion = result.assignmentVersion;
|
|
8802
9171
|
this.conflict = false;
|
|
8803
|
-
this.dialog = { kind: "review", applied: result };
|
|
8804
9172
|
await this.refresh({ quiet: true });
|
|
8805
|
-
this.
|
|
8806
|
-
this.showApplied(result);
|
|
9173
|
+
this.closeDialog({ restoreFocus: false });
|
|
8807
9174
|
this.host.clearSelection();
|
|
9175
|
+
this.showApplied(result);
|
|
8808
9176
|
} catch (err) {
|
|
8809
9177
|
const conflict = err instanceof ManageApiError && err.status === 409 && err.code === "channel_assignment_conflict";
|
|
8810
9178
|
if (conflict) {
|
|
@@ -8827,12 +9195,26 @@ var ChannelsMode = class {
|
|
|
8827
9195
|
this.host.onError(err);
|
|
8828
9196
|
}
|
|
8829
9197
|
}
|
|
9198
|
+
/**
|
|
9199
|
+
* The success receipt, now that the review sheet closes on Apply. It names the
|
|
9200
|
+
* destination and the seats that could not move, keeps the authoritative
|
|
9201
|
+
* buckets reachable through Details, and stays up long enough to be read —
|
|
9202
|
+
* 1.2s was tuned for a confirmation the organizer was already looking at.
|
|
9203
|
+
*/
|
|
8830
9204
|
showApplied(result) {
|
|
9205
|
+
const targetName = this.nameOf(result.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
9206
|
+
const skipped = result.buckets.skippedHeld.count + result.buckets.skippedBooked.count + result.buckets.notFound.count;
|
|
8831
9207
|
this.setStaged(`<span class="slm-ch-tick" aria-hidden="true">\u2713</span>
|
|
8832
|
-
<span>
|
|
8833
|
-
|
|
9208
|
+
<span>Assigned <b>${result.applied.toLocaleString()}</b> seat${result.applied === 1 ? "" : "s"} to ${esc(targetName)}
|
|
9209
|
+
${skipped ? ` \xB7 ${skipped.toLocaleString()} skipped` : ""}</span>
|
|
9210
|
+
<span class="grow"></span>
|
|
9211
|
+
<button type="button" class="drop" data-ch-applied-details>Details</button>`, "done");
|
|
9212
|
+
this.stagedEl?.querySelector("[data-ch-applied-details]")?.addEventListener("click", () => {
|
|
9213
|
+
this.openDialog({ kind: "review", applied: result });
|
|
9214
|
+
});
|
|
9215
|
+
this.announce(`Assigned ${result.applied} seat${result.applied === 1 ? "" : "s"} to ${targetName}${skipped ? `; ${skipped} skipped` : ""}.`);
|
|
8834
9216
|
if (this.stagedDoneTimer) clearTimeout(this.stagedDoneTimer);
|
|
8835
|
-
this.stagedDoneTimer = setTimeout(() => this.setStaged(null),
|
|
9217
|
+
this.stagedDoneTimer = setTimeout(() => this.setStaged(null), 5e3);
|
|
8836
9218
|
}
|
|
8837
9219
|
shakeStaged() {
|
|
8838
9220
|
const bar = this.stagedEl;
|
|
@@ -8875,12 +9257,27 @@ var ChannelsMode = class {
|
|
|
8875
9257
|
});
|
|
8876
9258
|
});
|
|
8877
9259
|
}
|
|
9260
|
+
/**
|
|
9261
|
+
* "Get embed code" — mark the channel as reached through the organizer's own
|
|
9262
|
+
* backend. The write itself is `setChannelAccessIntent(…, 'server')`, the same
|
|
9263
|
+
* API the old picker called; the difference is that it is now a deliberate
|
|
9264
|
+
* action with a consequence the organizer is told about, rather than one of
|
|
9265
|
+
* four dropdown values with no observable effect.
|
|
9266
|
+
*/
|
|
9267
|
+
async chooseWebsiteIntegration() {
|
|
9268
|
+
const channelId = this.detailChannelId;
|
|
9269
|
+
if (!channelId) return;
|
|
9270
|
+
await this.setAccessIntent("server");
|
|
9271
|
+
}
|
|
8878
9272
|
async setAccessIntent(accessIntent) {
|
|
8879
9273
|
const channelId = this.detailChannelId;
|
|
8880
9274
|
if (!channelId || !this.caps.manage) return;
|
|
8881
9275
|
try {
|
|
8882
9276
|
await this.host.api.setChannelAccessIntent(this.host.eventKey, channelId, accessIntent);
|
|
8883
9277
|
await this.refresh();
|
|
9278
|
+
if (accessIntent === "server") {
|
|
9279
|
+
this.host.toast("Marked for your website. The embed code is on the Embed page.", "ok");
|
|
9280
|
+
}
|
|
8884
9281
|
} catch (err) {
|
|
8885
9282
|
this.host.toast("Couldn't save how buyers reach this channel.", "err");
|
|
8886
9283
|
this.host.onError(err);
|
|
@@ -9021,7 +9418,7 @@ var ChannelsMode = class {
|
|
|
9021
9418
|
});
|
|
9022
9419
|
});
|
|
9023
9420
|
}
|
|
9024
|
-
// ----
|
|
9421
|
+
// ---- buyer-link dialogs ---------------------------------------------------
|
|
9025
9422
|
async reloadLinks() {
|
|
9026
9423
|
const channelId = this.detailChannelId;
|
|
9027
9424
|
if (!channelId) return;
|
|
@@ -9064,7 +9461,7 @@ var ChannelsMode = class {
|
|
|
9064
9461
|
return;
|
|
9065
9462
|
}
|
|
9066
9463
|
this.renderScrim(`
|
|
9067
|
-
<h3 id="slm-ch-dlg-title">Create a
|
|
9464
|
+
<h3 id="slm-ch-dlg-title">Create a buyer link for ${esc(channel.name)}</h3>
|
|
9068
9465
|
<p class="sub">Anyone who opens the link can buy from this channel's allocation \u2014 and only from it.
|
|
9069
9466
|
You'll see the link once, right after you create it.</p>
|
|
9070
9467
|
<div class="slm-field">
|
|
@@ -9189,7 +9586,7 @@ var ChannelsMode = class {
|
|
|
9189
9586
|
copy?.addEventListener("click", () => {
|
|
9190
9587
|
const ok = () => {
|
|
9191
9588
|
copy.textContent = "Copied";
|
|
9192
|
-
this.announce("
|
|
9589
|
+
this.announce("Buyer link copied.");
|
|
9193
9590
|
};
|
|
9194
9591
|
const clipboard = typeof navigator === "undefined" ? null : navigator.clipboard;
|
|
9195
9592
|
if (clipboard?.writeText) {
|
|
@@ -9199,7 +9596,7 @@ var ChannelsMode = class {
|
|
|
9199
9596
|
selectSecret(dialog);
|
|
9200
9597
|
});
|
|
9201
9598
|
});
|
|
9202
|
-
this.announce("Your
|
|
9599
|
+
this.announce("Your buyer link is ready and is shown once.");
|
|
9203
9600
|
}
|
|
9204
9601
|
/**
|
|
9205
9602
|
* Rotate. The organizer must SAY what happens to the buyers already inside —
|
|
@@ -9218,7 +9615,7 @@ var ChannelsMode = class {
|
|
|
9218
9615
|
<span><b>${sessions.toLocaleString()} buyer${sessions === 1 ? "" : "s"}</b> got in with the current link
|
|
9219
9616
|
and still ${sessions === 1 ? "has" : "have"} active access.</span></div>` : "";
|
|
9220
9617
|
this.renderScrim(`
|
|
9221
|
-
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "
|
|
9618
|
+
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "buyer")} link?</h3>
|
|
9222
9619
|
<p class="sub">The current link stops opening immediately and cannot be restored. You will get a new
|
|
9223
9620
|
link to copy \u2014 shown once.</p>
|
|
9224
9621
|
${warning}
|
|
@@ -9277,7 +9674,7 @@ var ChannelsMode = class {
|
|
|
9277
9674
|
}
|
|
9278
9675
|
const sessions = link.activeSessions ?? 0;
|
|
9279
9676
|
this.renderScrim(`
|
|
9280
|
-
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "
|
|
9677
|
+
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "buyer")} link?</h3>
|
|
9281
9678
|
<p class="sub">It stops opening immediately and cannot be restored \u2014 there is no undo, and no way to
|
|
9282
9679
|
bring the same URL back. Seats already bought through it keep their sale.</p>
|
|
9283
9680
|
${sessions ? `<label class="slm-ch-radio">
|
|
@@ -9394,7 +9791,12 @@ var LEGEND = [
|
|
|
9394
9791
|
var MANAGER_CSS = (
|
|
9395
9792
|
/* @sl-css */
|
|
9396
9793
|
`
|
|
9397
|
-
|
|
9794
|
+
/* The floor was 480px, which trapped the cockpit in any host shorter than that:
|
|
9795
|
+
every ancestor here is overflow:hidden, so the bottom of the rail (Create
|
|
9796
|
+
channel, Show archived) was rendered but clipped away by the host and could
|
|
9797
|
+
not be reached by scrolling anything. 320px still keeps an unsized host from
|
|
9798
|
+
collapsing to the top bar, and the rail scrolls to its full content below. */
|
|
9799
|
+
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:320px;overflow:hidden;
|
|
9398
9800
|
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
9399
9801
|
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
9400
9802
|
borrowed from CHANNELS_CSS. The base cockpit animates whether or not
|
|
@@ -9462,7 +9864,7 @@ var MANAGER_CSS = (
|
|
|
9462
9864
|
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
9463
9865
|
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
9464
9866
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
9465
|
-
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
9867
|
+
.slm-railscroll{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:16px}
|
|
9466
9868
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
9467
9869
|
.slm-hint{font-size:12.5px;color:var(--slm-muted);line-height:1.5;margin-bottom:14px}
|
|
9468
9870
|
|
|
@@ -9527,7 +9929,8 @@ var MANAGER_CSS = (
|
|
|
9527
9929
|
color:var(--slm-muted);font-size:10.5px}
|
|
9528
9930
|
.slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
|
|
9529
9931
|
.slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
|
|
9530
|
-
.slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);
|
|
9932
|
+
.slm-blockedlist{max-height:246px;overflow:auto;overscroll-behavior:contain;border:1px solid var(--slm-line);
|
|
9933
|
+
border-radius:10px;background:var(--slm-surface)}
|
|
9531
9934
|
.slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
|
|
9532
9935
|
border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
|
|
9533
9936
|
.slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
|
|
@@ -9922,6 +10325,30 @@ var SeatManager = class {
|
|
|
9922
10325
|
this.selectSection(sectionId);
|
|
9923
10326
|
},
|
|
9924
10327
|
sections: () => this.sectionOptions,
|
|
10328
|
+
labelsInSection: (sectionId) => this.renderer?.getSelectableInSection(sectionId).map((seat) => seat.label) ?? [],
|
|
10329
|
+
// Logical rows, not physical row objects: a segmented row is authored as
|
|
10330
|
+
// several objects sharing one `logicalRowId`, and the organizer thinks of
|
|
10331
|
+
// it as one row. Tables carry seats too, so they are rows here as well.
|
|
10332
|
+
rows: () => {
|
|
10333
|
+
const objects = new Map((this.doc?.objects ?? []).map((object) => [object.id, object]));
|
|
10334
|
+
const rows = /* @__PURE__ */ new Map();
|
|
10335
|
+
for (const seat of this.labelToSeat.values()) {
|
|
10336
|
+
const object = objects.get(seat.rowId);
|
|
10337
|
+
if (!object || object.type !== "row" && object.type !== "table") continue;
|
|
10338
|
+
const id = seat.logicalRowId ?? seat.rowId;
|
|
10339
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
|
|
10340
|
+
const row = rows.get(id) ?? {
|
|
10341
|
+
id,
|
|
10342
|
+
label: object.type === "row" ? object.segmentedRow?.displayLabel ?? object.displayLabel ?? object.label : object.displayLabel ?? object.label,
|
|
10343
|
+
sectionId,
|
|
10344
|
+
sectionLabel: this.sectionLabelById.get(sectionId) ?? "Other seats",
|
|
10345
|
+
labels: []
|
|
10346
|
+
};
|
|
10347
|
+
row.labels.push(seat.label);
|
|
10348
|
+
rows.set(id, row);
|
|
10349
|
+
}
|
|
10350
|
+
return [...rows.values()].sort((left, right) => left.sectionLabel.localeCompare(right.sectionLabel, void 0, { numeric: true, sensitivity: "base" }) || left.label.localeCompare(right.label, void 0, { numeric: true, sensitivity: "base" }));
|
|
10351
|
+
},
|
|
9925
10352
|
categories: () => (this.doc?.categories ?? []).map((category) => ({
|
|
9926
10353
|
key: category.key,
|
|
9927
10354
|
label: category.label ?? category.key,
|