@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.js
CHANGED
|
@@ -6493,7 +6493,7 @@ function retryAfterCopy(details) {
|
|
|
6493
6493
|
}
|
|
6494
6494
|
function accessLine(access) {
|
|
6495
6495
|
if (!access || !access.intent) return "\u2014";
|
|
6496
|
-
const base = access.intent === "
|
|
6496
|
+
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : "Not distributed yet";
|
|
6497
6497
|
const grants = access.hasActiveGrants ? "in use now" : access.lastMintAt ? `last used ${new Date(access.lastMintAt).toLocaleDateString()}` : null;
|
|
6498
6498
|
const detail = access.detail ?? grants;
|
|
6499
6499
|
return detail ? `${base} \xB7 ${detail}` : base;
|
|
@@ -6885,9 +6885,10 @@ var ManageApi = class {
|
|
|
6885
6885
|
};
|
|
6886
6886
|
|
|
6887
6887
|
// src/channelsMode.ts
|
|
6888
|
-
var POLL_MS =
|
|
6888
|
+
var POLL_MS = 3e4;
|
|
6889
6889
|
var MAX_FLAGS = 8;
|
|
6890
6890
|
var SEAT_LIST_PAGE = 300;
|
|
6891
|
+
var MAX_ASSIGNMENT_UNITS = 5e3;
|
|
6891
6892
|
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
6892
6893
|
var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
6893
6894
|
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
@@ -6934,6 +6935,7 @@ var CHANNELS_CSS = (
|
|
|
6934
6935
|
.slm-ch-staged .grow{flex:1}
|
|
6935
6936
|
.slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
|
|
6936
6937
|
background:#f4b740;color:#1a1200;font-weight:800;font-size:12.5px}
|
|
6938
|
+
.slm-ch-staged .go:disabled{opacity:.48;cursor:not-allowed}
|
|
6937
6939
|
.slm-ch-staged .drop{color:var(--slm-muted);font-weight:700;font-size:11.5px;min-height:44px;padding-inline:8px}
|
|
6938
6940
|
.slm-ch-tick{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;
|
|
6939
6941
|
background:#fff;color:#1f7a4d;font-weight:900;font-size:11px;animation:slm-ch-tick var(--slm-mo-base) var(--slm-mo-spring)}
|
|
@@ -6984,6 +6986,12 @@ var CHANNELS_CSS = (
|
|
|
6984
6986
|
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6985
6987
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
6986
6988
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
6989
|
+
/* distribute options \u2014 two actions, each with the one sentence that explains it */
|
|
6990
|
+
.slm-ch-dist{display:flex;flex-direction:column;gap:6px;padding:12px;margin-bottom:8px;border:1px solid var(--slm-line);
|
|
6991
|
+
border-radius:10px;background:var(--slm-surface)}
|
|
6992
|
+
.slm-ch-dist b{font-size:13px;font-weight:800}
|
|
6993
|
+
.slm-ch-dist .why{color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
6994
|
+
.slm-ch-dist .slm-btn{width:100%;margin-top:2px}
|
|
6987
6995
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
6988
6996
|
line-height:1.5;margin-bottom:12px}
|
|
6989
6997
|
.slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
|
|
@@ -6999,7 +7007,7 @@ var CHANNELS_CSS = (
|
|
|
6999
7007
|
/* dialogs */
|
|
7000
7008
|
.slm-ch-scrim{position:absolute;inset:0;z-index:12;background:rgba(4,6,12,.62);display:grid;place-items:center;
|
|
7001
7009
|
padding:18px;animation:slm-ch-fade var(--slm-mo-quick) var(--slm-mo-out)}
|
|
7002
|
-
.slm-ch-dialog{width:min(460px,100%);max-height:100%;overflow:auto;background:#12151f;border:1px solid var(--slm-line);
|
|
7010
|
+
.slm-ch-dialog{width:min(460px,100%);max-height:100%;overflow:auto;overscroll-behavior:contain;background:#12151f;border:1px solid var(--slm-line);
|
|
7003
7011
|
border-radius:14px;padding:20px;box-shadow:0 24px 70px rgba(0,0,0,.6);
|
|
7004
7012
|
animation:slm-ch-rise var(--slm-mo-base) var(--slm-mo-out)}
|
|
7005
7013
|
.slm-ch-dialog h3{margin:0 0 4px;font-size:16px;font-weight:800;letter-spacing:-.01em}
|
|
@@ -7026,7 +7034,7 @@ var CHANNELS_CSS = (
|
|
|
7026
7034
|
overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
7027
7035
|
.slm-ch-err{color:#f1a4a6;font-size:11.5px;margin-top:6px}
|
|
7028
7036
|
|
|
7029
|
-
/*
|
|
7037
|
+
/* buyer links \u2014 STATUS only; there is no Copy control on this card */
|
|
7030
7038
|
.slm-ch-link{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
7031
7039
|
margin-bottom:8px}
|
|
7032
7040
|
.slm-ch-link .lk-head{display:flex;align-items:center;gap:8px}
|
|
@@ -7045,8 +7053,8 @@ var CHANNELS_CSS = (
|
|
|
7045
7053
|
.slm-ch-radio input{flex:none;margin-top:2px}
|
|
7046
7054
|
.slm-ch-radio b{display:block;font-weight:800;margin-bottom:2px}
|
|
7047
7055
|
.slm-ch-radio .why{display:block;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
7048
|
-
.slm-ch-seatlist{max-height:44vh;overflow:auto;border:1px solid var(--slm-line);
|
|
7049
|
-
background:var(--slm-surface);margin-top:10px}
|
|
7056
|
+
.slm-ch-seatlist{max-height:44vh;overflow:auto;overscroll-behavior:contain;border:1px solid var(--slm-line);
|
|
7057
|
+
border-radius:10px;background:var(--slm-surface);margin-top:10px}
|
|
7050
7058
|
.slm-ch-seatgroup{padding:8px 10px;border-bottom:1px solid var(--slm-line);display:flex;align-items:center;
|
|
7051
7059
|
justify-content:space-between;gap:8px;font-size:11px;font-weight:800;color:var(--slm-muted);position:sticky;top:0;
|
|
7052
7060
|
background:var(--slm-surface)}
|
|
@@ -7058,6 +7066,25 @@ var CHANNELS_CSS = (
|
|
|
7058
7066
|
.slm-ch-seatitem[aria-checked="true"] .box{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7059
7067
|
.slm-ch-seatitem .meta{margin-left:auto;color:var(--slm-muted);font-size:10.5px}
|
|
7060
7068
|
|
|
7069
|
+
/* scope chooser: whole sections or many rows in one additive pass */
|
|
7070
|
+
.slm-ch-scopebar{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:10px;margin-top:12px}
|
|
7071
|
+
.slm-ch-scopebar label{display:grid;gap:5px;color:var(--slm-muted);font-size:10.5px;font-weight:800}
|
|
7072
|
+
.slm-ch-scopebar input{width:100%;min-height:40px;padding:8px 10px;border:1px solid var(--slm-line);border-radius:8px;
|
|
7073
|
+
background:var(--slm-surface);color:var(--slm-text);font:inherit}
|
|
7074
|
+
.slm-ch-scopesummary{padding-bottom:10px;color:var(--slm-muted);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
7075
|
+
.slm-ch-scopegroup{position:sticky;top:0;z-index:1;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;
|
|
7076
|
+
gap:6px;padding:6px 8px;border-bottom:1px solid var(--slm-line);background:var(--slm-surface)}
|
|
7077
|
+
.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}
|
|
7078
|
+
.slm-ch-groupcheck .box{width:16px;height:16px;flex:none;display:grid;place-items:center;border:1px solid var(--slm-muted);border-radius:4px;
|
|
7079
|
+
color:transparent;font-size:10px}
|
|
7080
|
+
.slm-ch-groupcheck[aria-checked="true"] .box,.slm-ch-groupcheck[aria-checked="mixed"] .box{border-color:var(--slm-accent);
|
|
7081
|
+
background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
7082
|
+
.slm-ch-groupcheck .meta{min-width:0;margin-left:auto;color:var(--slm-muted);font-size:10.5px;font-weight:600;white-space:nowrap}
|
|
7083
|
+
.slm-ch-grouptoggle{min-height:32px;padding:5px 8px;color:var(--slm-accent);font-size:11px;font-weight:800}
|
|
7084
|
+
.slm-ch-scopehint{padding:8px 10px;color:var(--slm-muted);font-size:10.5px;border-bottom:1px solid var(--slm-line)}
|
|
7085
|
+
.slm-ch-scope-empty{padding:18px 12px;color:var(--slm-muted);font-size:11.5px;text-align:center}
|
|
7086
|
+
@media(max-width:560px){.slm-ch-scopebar{grid-template-columns:1fr}.slm-ch-scopesummary{padding-bottom:0}}
|
|
7087
|
+
|
|
7061
7088
|
/* compact: bottom sheet with three detents (\xA713) */
|
|
7062
7089
|
.slm.compact.ch-sheet .slm-rail{position:absolute;left:0;right:0;bottom:0;z-index:8;border-top:1px solid var(--slm-line);
|
|
7063
7090
|
border-radius:18px 18px 0 0;background:#12151f;
|
|
@@ -7096,12 +7123,12 @@ function bucketRowsHtml(rows) {
|
|
|
7096
7123
|
</div>`).join("");
|
|
7097
7124
|
}
|
|
7098
7125
|
var SERVER_INTEGRATION_HTML = `
|
|
7099
|
-
<p class="slm-eyebrow" style="margin-top:18px">
|
|
7100
|
-
<p class="slm-hint">There is nothing to set up on this screen. Your own server mints a short-lived buyer
|
|
7126
|
+
<p class="slm-eyebrow" style="margin-top:18px">On your website</p>
|
|
7127
|
+
<p class="slm-hint">There is nothing more to set up on this screen. Your own server mints a short-lived buyer
|
|
7101
7128
|
access session for this channel with the SeatLayer server SDK and hands it to the widget. A channel name
|
|
7102
7129
|
on its own never grants access.</p>
|
|
7103
7130
|
<p class="slm-note"><a class="slm-linkbtn" href="https://docs.seatlayer.io/server-api/channels"
|
|
7104
|
-
target="_blank" rel="noreferrer noopener">Read the
|
|
7131
|
+
target="_blank" rel="noreferrer noopener">Read the website integration guide \u2192</a></p>`;
|
|
7105
7132
|
function esc(value) {
|
|
7106
7133
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7107
7134
|
}
|
|
@@ -7182,6 +7209,21 @@ var ChannelsMode = class {
|
|
|
7182
7209
|
this.stagedDoneTimer = null;
|
|
7183
7210
|
this.lastSelectionCount = 0;
|
|
7184
7211
|
this.lastCounts = /* @__PURE__ */ new Map();
|
|
7212
|
+
/** Rows are structural chart data — they do not move while the organizer is
|
|
7213
|
+
* allocating. Deriving them walks every seat, so the view is cached for the
|
|
7214
|
+
* lifetime of this mode entry and ordinary selection repaints stay O(1). */
|
|
7215
|
+
this.assignmentRowsCache = null;
|
|
7216
|
+
/** The markup currently in the rail. An identical repaint is skipped, which is
|
|
7217
|
+
* what keeps the organizer's scroll position (and open <select>) alive. */
|
|
7218
|
+
this.railHtml = null;
|
|
7219
|
+
/**
|
|
7220
|
+
* The `assignmentVersion` the allocation map was built from. Walking every
|
|
7221
|
+
* allocation page is the expensive half of a refresh and the server already
|
|
7222
|
+
* tells us, in the channels response, whether ANY seat moved. Unchanged
|
|
7223
|
+
* version, unchanged allocation — so the walk is skipped entirely.
|
|
7224
|
+
*/
|
|
7225
|
+
this.allocationVersion = null;
|
|
7226
|
+
this.onVisibility = null;
|
|
7185
7227
|
this.host = host;
|
|
7186
7228
|
this.caps = capabilities;
|
|
7187
7229
|
}
|
|
@@ -7192,6 +7234,8 @@ var ChannelsMode = class {
|
|
|
7192
7234
|
this.active = true;
|
|
7193
7235
|
this.mapIntent = "pan";
|
|
7194
7236
|
this.focusedSectionId = null;
|
|
7237
|
+
this.assignmentRowsCache = null;
|
|
7238
|
+
this.railHtml = null;
|
|
7195
7239
|
this.ensureLayer();
|
|
7196
7240
|
this.host.root.classList.add("ch-mode");
|
|
7197
7241
|
this.applySheetClasses();
|
|
@@ -7200,8 +7244,15 @@ var ChannelsMode = class {
|
|
|
7200
7244
|
this.onInteractionChange?.();
|
|
7201
7245
|
void this.refresh();
|
|
7202
7246
|
this.pollTimer = setInterval(() => {
|
|
7247
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
7203
7248
|
void this.refresh({ quiet: true });
|
|
7204
7249
|
}, POLL_MS);
|
|
7250
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
7251
|
+
this.onVisibility = () => {
|
|
7252
|
+
if (this.active && !document.hidden) void this.refresh({ quiet: true });
|
|
7253
|
+
};
|
|
7254
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
7255
|
+
}
|
|
7205
7256
|
}
|
|
7206
7257
|
/** Called when the cockpit leaves Channels mode. Everything this mode painted
|
|
7207
7258
|
* over the map goes with it — no other tool ever inherits a channel overlay. */
|
|
@@ -7210,6 +7261,11 @@ var ChannelsMode = class {
|
|
|
7210
7261
|
this.active = false;
|
|
7211
7262
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
7212
7263
|
this.pollTimer = null;
|
|
7264
|
+
if (this.onVisibility && typeof document !== "undefined") {
|
|
7265
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
7266
|
+
}
|
|
7267
|
+
this.onVisibility = null;
|
|
7268
|
+
this.railHtml = null;
|
|
7213
7269
|
this.closeDialog({ restoreFocus: false });
|
|
7214
7270
|
this.links = [];
|
|
7215
7271
|
this.linksChannelId = null;
|
|
@@ -7298,8 +7354,11 @@ var ChannelsMode = class {
|
|
|
7298
7354
|
if (!this.targetChannelId) {
|
|
7299
7355
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
7300
7356
|
}
|
|
7301
|
-
|
|
7302
|
-
|
|
7357
|
+
if (this.allocationVersion !== list.assignmentVersion) {
|
|
7358
|
+
await this.loadAllocation(seq);
|
|
7359
|
+
if (superseded()) return;
|
|
7360
|
+
this.allocationVersion = list.assignmentVersion;
|
|
7361
|
+
}
|
|
7303
7362
|
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
7304
7363
|
if (superseded()) return;
|
|
7305
7364
|
this.loading = false;
|
|
@@ -7639,6 +7698,7 @@ var ChannelsMode = class {
|
|
|
7639
7698
|
}
|
|
7640
7699
|
const target = this.nameOf(this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
7641
7700
|
const mutations = mutationCount(buckets);
|
|
7701
|
+
const tooLarge = labels.length > MAX_ASSIGNMENT_UNITS;
|
|
7642
7702
|
const skipped = buckets.skippedHeld.count + buckets.skippedBooked.count;
|
|
7643
7703
|
const parts = [
|
|
7644
7704
|
`<b>${labels.length.toLocaleString()}</b> selected`,
|
|
@@ -7650,7 +7710,9 @@ var ChannelsMode = class {
|
|
|
7650
7710
|
<span>${parts.join(" \xB7 ")}</span>
|
|
7651
7711
|
<span class="grow"></span>
|
|
7652
7712
|
<button type="button" class="drop" data-ch-act="discard">Discard</button>
|
|
7653
|
-
<button type="button" class="go" data-ch-act="review">
|
|
7713
|
+
<button type="button" class="go" data-ch-act="review"${tooLarge ? " disabled" : ""}>
|
|
7714
|
+
${tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : "Review changes"}
|
|
7715
|
+
</button>`);
|
|
7654
7716
|
this.stagedEl?.querySelectorAll("[data-ch-act]").forEach((button) => {
|
|
7655
7717
|
button.addEventListener("click", () => {
|
|
7656
7718
|
if (button.dataset.chAct === "discard") this.host.clearSelection();
|
|
@@ -7718,35 +7780,59 @@ var ChannelsMode = class {
|
|
|
7718
7780
|
this.paintOverlay();
|
|
7719
7781
|
}
|
|
7720
7782
|
}
|
|
7783
|
+
/**
|
|
7784
|
+
* Replace the rail's markup — but only when it actually differs, and never at
|
|
7785
|
+
* the cost of where the organizer had scrolled to.
|
|
7786
|
+
*
|
|
7787
|
+
* The rail repaints on a clock, on every selection change and after every
|
|
7788
|
+
* mutation. Rewriting `innerHTML` each time resets `scrollTop`, which is
|
|
7789
|
+
* exactly what "the rail gets stuck" was: scroll down to Create channel, the
|
|
7790
|
+
* poll ticks, and the list snaps back to the top under the cursor. So skip the
|
|
7791
|
+
* write when the markup is byte-identical, and restore the offset when it is
|
|
7792
|
+
* not.
|
|
7793
|
+
*
|
|
7794
|
+
* Returns whether the DOM was rewritten. Callers must only re-wire listeners
|
|
7795
|
+
* when it was — a skipped paint keeps the old nodes AND their listeners, so
|
|
7796
|
+
* re-wiring would double every handler.
|
|
7797
|
+
*/
|
|
7798
|
+
setRailHtml(html) {
|
|
7799
|
+
if (html === this.railHtml) return false;
|
|
7800
|
+
const rail = this.host.rail;
|
|
7801
|
+
const scrollTop = rail.scrollTop;
|
|
7802
|
+
rail.innerHTML = html;
|
|
7803
|
+
this.railHtml = html;
|
|
7804
|
+
if (scrollTop) rail.scrollTop = scrollTop;
|
|
7805
|
+
return true;
|
|
7806
|
+
}
|
|
7721
7807
|
paintRail() {
|
|
7722
7808
|
if (!this.active) return;
|
|
7723
7809
|
const rail = this.host.rail;
|
|
7724
7810
|
if (!this.caps.view) {
|
|
7725
|
-
|
|
7726
|
-
<p class="slm-hint">You need channel-management permission on this event to see allocations.</p
|
|
7811
|
+
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
7812
|
+
<p class="slm-hint">You need channel-management permission on this event to see allocations.</p>`);
|
|
7727
7813
|
return;
|
|
7728
7814
|
}
|
|
7729
7815
|
if (this.loading && !this.list) {
|
|
7730
|
-
|
|
7731
|
-
<div class="slm-empty">Loading allocations\u2026</div
|
|
7816
|
+
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
7817
|
+
<div class="slm-empty">Loading allocations\u2026</div>`);
|
|
7732
7818
|
return;
|
|
7733
7819
|
}
|
|
7734
7820
|
if (!this.list && this.loadError) {
|
|
7735
|
-
|
|
7821
|
+
if (this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
7736
7822
|
<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
7737
7823
|
<span><b>Couldn't load sales channels.</b> Everything else on this event still works.
|
|
7738
|
-
<button type="button" data-ch-act="retry">Try again</button></span></div
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7824
|
+
<button type="button" data-ch-act="retry">Try again</button></span></div>`)) {
|
|
7825
|
+
rail.querySelector('[data-ch-act="retry"]')?.addEventListener("click", () => {
|
|
7826
|
+
void this.refresh();
|
|
7827
|
+
});
|
|
7828
|
+
}
|
|
7742
7829
|
return;
|
|
7743
7830
|
}
|
|
7744
7831
|
const selection = this.host.selectionLabels();
|
|
7745
7832
|
const grab = this.host.isCompact() ? `<div class="slm-ch-grab"><span class="slm-ch-grabbar"></span></div>` : "";
|
|
7746
7833
|
const segment = this.viewSegmentHtml();
|
|
7747
7834
|
const body = this.view === "preview" ? this.previewRailHtml() : this.detailChannelId ? this.detailRailHtml(this.detailChannelId) : selection.length && this.caps.manage ? this.selectionRailHtml(selection) : this.listRailHtml();
|
|
7748
|
-
|
|
7749
|
-
this.wireRail();
|
|
7835
|
+
if (this.setRailHtml(`${grab}${segment}${body}`)) this.wireRail();
|
|
7750
7836
|
this.paintStagedBar();
|
|
7751
7837
|
}
|
|
7752
7838
|
viewSegmentHtml() {
|
|
@@ -7817,8 +7903,9 @@ var ChannelsMode = class {
|
|
|
7817
7903
|
const create = this.caps.manage ? `<button type="button" class="slm-btn ghost" style="width:100%" data-ch-act="create">+ Create channel</button>` : "";
|
|
7818
7904
|
const readOnly = this.caps.manage ? "" : `<p class="slm-note">You can see how inventory is allocated. Changing it needs channel-management permission.</p>`;
|
|
7819
7905
|
return `
|
|
7906
|
+
${this.caps.manage ? this.assignmentToolsHtml() : ""}
|
|
7820
7907
|
<p class="slm-eyebrow">Sales channels</p>
|
|
7821
|
-
<p class="slm-hint">
|
|
7908
|
+
<p class="slm-hint">Channel colours and names are only visible to organizers, never to buyers.</p>
|
|
7822
7909
|
<div class="slm-ch-list">${rows}</div>
|
|
7823
7910
|
${create}
|
|
7824
7911
|
${readOnly}
|
|
@@ -7827,6 +7914,38 @@ var ChannelsMode = class {
|
|
|
7827
7914
|
style="text-align:left">${this.showArchived ? "Hide" : "Show"} archived${archivedCount ? ` (${archivedCount})` : ""}</button>
|
|
7828
7915
|
</p>`;
|
|
7829
7916
|
}
|
|
7917
|
+
/**
|
|
7918
|
+
* Destination-first assignment controls.
|
|
7919
|
+
*
|
|
7920
|
+
* These used to live only inside the selection rail, which meant every route
|
|
7921
|
+
* into them was gated behind "select a seat on the map first" — the section,
|
|
7922
|
+
* row and category choosers were invisible until the organizer had already
|
|
7923
|
+
* done the work by hand. They now paint above the channel list too, so the
|
|
7924
|
+
* destination is chosen first and whole sections and rows are discoverable
|
|
7925
|
+
* without learning a hidden seat-first workflow.
|
|
7926
|
+
*/
|
|
7927
|
+
assignmentToolsHtml() {
|
|
7928
|
+
const options = this.assignableChannels().map((channel) => `<option value="${esc(channel.id)}"${channel.id === this.targetChannelId ? " selected" : ""}>${esc(channel.name)}</option>`).join("");
|
|
7929
|
+
const sections = this.host.sections().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-sections">Sections</button>' : "";
|
|
7930
|
+
const rows = this.assignmentRows().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-rows">Rows</button>' : "";
|
|
7931
|
+
const dragClass = this.mapIntent === "assign" ? "slm-btn" : "slm-btn ghost";
|
|
7932
|
+
return `
|
|
7933
|
+
<p class="slm-eyebrow">Assign inventory</p>
|
|
7934
|
+
<div class="slm-field">
|
|
7935
|
+
<label for="slm-ch-target">Assign to</label>
|
|
7936
|
+
<select class="slm-select" id="slm-ch-target" data-ch-target>${options}</select>
|
|
7937
|
+
</div>
|
|
7938
|
+
<p class="slm-eyebrow" style="margin-top:12px">Select by</p>
|
|
7939
|
+
<div class="slm-ch-row2" style="margin-top:2px">
|
|
7940
|
+
${sections}${rows}
|
|
7941
|
+
<button type="button" class="${dragClass}" data-ch-act="drag-select">Drag box</button>
|
|
7942
|
+
</div>
|
|
7943
|
+
<div class="slm-ch-row2">
|
|
7944
|
+
<button type="button" class="slm-btn ghost" data-ch-act="pick-category">Category</button>
|
|
7945
|
+
<button type="button" class="slm-btn ghost" data-ch-act="seatlist">Seat list \u2328</button>
|
|
7946
|
+
</div>
|
|
7947
|
+
<p class="slm-note">Choose a destination, then add whole sections, multiple rows, a dragged area, or individual seats.</p>`;
|
|
7948
|
+
}
|
|
7830
7949
|
selectionRailHtml(selection) {
|
|
7831
7950
|
const sources = selectionSources(selection, this.allocation, this.list);
|
|
7832
7951
|
const conflict = this.conflict ? `<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
@@ -7835,34 +7954,27 @@ var ChannelsMode = class {
|
|
|
7835
7954
|
<button type="button" data-ch-act="refresh-review">Refresh and review \u2192</button></span></div>` : "";
|
|
7836
7955
|
const bump = selection.length !== this.lastSelectionCount ? " bump" : "";
|
|
7837
7956
|
this.lastSelectionCount = selection.length;
|
|
7838
|
-
const
|
|
7957
|
+
const tooLarge = selection.length > MAX_ASSIGNMENT_UNITS;
|
|
7839
7958
|
const sourceRows = sources.map((row) => {
|
|
7840
7959
|
const marker = this.markerFor(row.channelId);
|
|
7841
7960
|
return `<div class="slm-ch-selsrc-row">
|
|
7842
7961
|
<span class="mk" style="background:${esc(marker.color)}" aria-hidden="true">${esc(marker.letter)}</span>
|
|
7843
7962
|
<b>${row.count.toLocaleString()}</b><span>${esc(row.name)}</span></div>`;
|
|
7844
7963
|
}).join("");
|
|
7845
|
-
const sectionSelect = this.host.sections().length ? `<button type="button" class="slm-btn ghost" data-ch-act="pick-section">Section</button>` : "";
|
|
7846
7964
|
return `
|
|
7847
7965
|
${conflict}
|
|
7966
|
+
${this.assignmentToolsHtml()}
|
|
7848
7967
|
<div class="slm-selbar"><span class="slm-selnum slm-ch-selnum${bump}">${selection.length.toLocaleString()}</span>
|
|
7849
7968
|
<span class="slm-sellabel">selected</span></div>
|
|
7850
7969
|
<div class="slm-ch-selsrc" aria-live="polite" aria-label="Selection sources">${sourceRows}</div>
|
|
7851
|
-
|
|
7852
|
-
<
|
|
7853
|
-
|
|
7854
|
-
</div>
|
|
7970
|
+
${tooLarge ? `<div class="slm-ch-alert warn" role="alert"><span>\u2139</span><span>
|
|
7971
|
+
<b>This selection is too large to apply at once.</b> Choose at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()}
|
|
7972
|
+
seats, for example by splitting the venue into row groups.</span></div>` : ""}
|
|
7855
7973
|
<p class="slm-note">Changes are staged \u2014 nothing moves until you review and apply.
|
|
7856
7974
|
Seats in checkout or already sold are never moved.</p>
|
|
7857
7975
|
<div class="slm-ch-row2">
|
|
7858
7976
|
<button type="button" class="slm-btn ghost" data-ch-act="discard">Clear selection</button>
|
|
7859
|
-
<button type="button" class="slm-btn" data-ch-act="review">Review changes</button>
|
|
7860
|
-
</div>
|
|
7861
|
-
<p class="slm-eyebrow" style="margin-top:18px">Select by</p>
|
|
7862
|
-
<div class="slm-ch-row2" style="margin-top:2px">
|
|
7863
|
-
${sectionSelect}
|
|
7864
|
-
<button type="button" class="slm-btn ghost" data-ch-act="pick-category">Category</button>
|
|
7865
|
-
<button type="button" class="slm-btn ghost" data-ch-act="seatlist">List \u2328</button>
|
|
7977
|
+
<button type="button" class="slm-btn" data-ch-act="review"${tooLarge ? " disabled" : ""}>Review changes</button>
|
|
7866
7978
|
</div>
|
|
7867
7979
|
<p class="slm-note">The seat list offers the same selection with checkboxes for keyboard and screen-reader use.</p>`;
|
|
7868
7980
|
}
|
|
@@ -7877,22 +7989,7 @@ var ChannelsMode = class {
|
|
|
7877
7989
|
<button type="button" class="slm-btn ghost" data-ch-act="archive">Archive\u2026</button>
|
|
7878
7990
|
</div>
|
|
7879
7991
|
<p class="slm-note">Archive returns the allocation to a destination you choose. Nothing is ever deleted silently.</p>` : "";
|
|
7880
|
-
const
|
|
7881
|
-
const intents = ["none", "internal", "server", "hosted_link"];
|
|
7882
|
-
const selfServiceGap = (intent === "server" || intent === "hosted_link") && !channel.access?.hasActiveGrants;
|
|
7883
|
-
const access = this.caps.manage ? `
|
|
7884
|
-
<p class="slm-eyebrow" style="margin-top:14px">Buyer access</p>
|
|
7885
|
-
<div class="slm-field">
|
|
7886
|
-
<label for="slm-ch-intent">How should buyers reach this channel?</label>
|
|
7887
|
-
<select class="slm-select" id="slm-ch-intent" data-ch-intent>
|
|
7888
|
-
${intents.map((value) => `<option value="${value}"${value === intent ? " selected" : ""}>${esc(accessIntentLabel(value))}</option>`).join("")}
|
|
7889
|
-
</select>
|
|
7890
|
-
</div>
|
|
7891
|
-
<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>
|
|
7892
|
-
${selfServiceGap ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
7893
|
-
<span>This channel is marked for buyer self-service but no buyer has been let in yet.</span></div>` : ""}
|
|
7894
|
-
${this.hostedLinksHtml()}
|
|
7895
|
-
${SERVER_INTEGRATION_HTML}` : "";
|
|
7992
|
+
const access = this.caps.manage ? this.distributeHtml(channel) : "";
|
|
7896
7993
|
return `
|
|
7897
7994
|
<p class="slm-eyebrow">
|
|
7898
7995
|
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
@@ -7902,7 +7999,54 @@ var ChannelsMode = class {
|
|
|
7902
7999
|
${access}
|
|
7903
8000
|
${lifecycle}`;
|
|
7904
8001
|
}
|
|
7905
|
-
|
|
8002
|
+
/**
|
|
8003
|
+
* "Distribute" — how the seats in this channel actually reach a buyer.
|
|
8004
|
+
*
|
|
8005
|
+
* This replaced a four-value "access intent" picker (none / internal /
|
|
8006
|
+
* hosted_link / server). Two of those values did nothing anywhere: no buyer or
|
|
8007
|
+
* inventory path reads `access_intent`, so `none` and `internal` were labels an
|
|
8008
|
+
* organizer could set and then wait forever for something to happen. A third,
|
|
8009
|
+
* `hosted_link`, is not the organizer's to choose at all — the server sets it
|
|
8010
|
+
* when a buyer link is created and clears it when the last live one is revoked.
|
|
8011
|
+
*
|
|
8012
|
+
* So this is two ACTIONS, not a setting: create a buyer link, or point the
|
|
8013
|
+
* channel at a website integration. Both of them do something the moment they
|
|
8014
|
+
* are pressed. A legacy row still carrying `none` or `internal` renders the
|
|
8015
|
+
* neutral "not distributed yet" state with both actions offered — the stored
|
|
8016
|
+
* value is left alone (it is organizer-declared metadata and the API that
|
|
8017
|
+
* writes it is unchanged), it simply no longer has a control of its own.
|
|
8018
|
+
*/
|
|
8019
|
+
distributeHtml(channel) {
|
|
8020
|
+
const intent = channel.access?.intent ?? "none";
|
|
8021
|
+
const live = channel.access?.hasActiveGrants === true;
|
|
8022
|
+
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.";
|
|
8023
|
+
const option = (title, why, action, cta, primary) => `<div class="slm-ch-dist">
|
|
8024
|
+
<b>${esc(title)}</b>
|
|
8025
|
+
<span class="why">${esc(why)}</span>
|
|
8026
|
+
<button type="button" class="slm-btn${primary ? "" : " ghost"}" data-ch-act="${esc(action)}">${esc(cta)}</button>
|
|
8027
|
+
</div>`;
|
|
8028
|
+
const linksSupported = this.linksState !== "unsupported";
|
|
8029
|
+
return `
|
|
8030
|
+
<p class="slm-eyebrow" style="margin-top:14px">Distribute</p>
|
|
8031
|
+
<p class="slm-hint">${esc(state)}</p>
|
|
8032
|
+
${linksSupported ? option(
|
|
8033
|
+
"Sell with a buyer link",
|
|
8034
|
+
"SeatLayer makes a link you send to a named group. They open it and buy only these seats.",
|
|
8035
|
+
"link-create",
|
|
8036
|
+
this.links.some(accessLinkIsLive) ? "Create another buyer link" : "Create buyer link",
|
|
8037
|
+
true
|
|
8038
|
+
) : ""}
|
|
8039
|
+
${option(
|
|
8040
|
+
"Integrate a website or app",
|
|
8041
|
+
"Your website's backend grants each buyer access \u2014 set up on the Embed page.",
|
|
8042
|
+
"embed-code",
|
|
8043
|
+
"Get embed code",
|
|
8044
|
+
false
|
|
8045
|
+
)}
|
|
8046
|
+
${this.hostedLinksHtml()}
|
|
8047
|
+
${intent === "server" ? SERVER_INTEGRATION_HTML : ""}`;
|
|
8048
|
+
}
|
|
8049
|
+
// ---- buyer links ----------------------------------------------------------
|
|
7906
8050
|
/**
|
|
7907
8051
|
* Read the status projection for the open channel. Never paints — the caller
|
|
7908
8052
|
* decides when the rail repaints, so a poll-driven reload does not fight a
|
|
@@ -7932,32 +8076,34 @@ var ChannelsMode = class {
|
|
|
7932
8076
|
}
|
|
7933
8077
|
}
|
|
7934
8078
|
/**
|
|
7935
|
-
* The
|
|
8079
|
+
* The buyer-link status section of the detail panel.
|
|
7936
8080
|
*
|
|
7937
8081
|
* STATUS ONLY, by design (comp 06 `hosted`): label, state, expiry,
|
|
7938
8082
|
* redemptions, seats per buyer, live sessions. There is no Copy control here
|
|
7939
8083
|
* and no field to hang one on — the URL was shown once at creation and cannot
|
|
7940
8084
|
* be produced again. Rotation is the recovery path, and it says so.
|
|
8085
|
+
*
|
|
8086
|
+
* Creating a link is the Distribute card's action, not this section's, so a
|
|
8087
|
+
* channel with no links renders nothing here rather than an empty heading and
|
|
8088
|
+
* a second button saying the same thing.
|
|
7941
8089
|
*/
|
|
7942
8090
|
hostedLinksHtml() {
|
|
7943
|
-
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">
|
|
8091
|
+
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Buyer links</p>`;
|
|
7944
8092
|
if (this.linksState === "unsupported") {
|
|
7945
8093
|
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
7946
|
-
<span><b>
|
|
8094
|
+
<span><b>Buyer links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
7947
8095
|
}
|
|
7948
8096
|
if (this.linksState === "error") {
|
|
7949
8097
|
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
7950
8098
|
<span><b>Couldn't load this channel's links.</b>
|
|
7951
8099
|
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
7952
8100
|
}
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
${live ? "Create another hosted link" : "Create hosted access link"}</button>` : "";
|
|
8101
|
+
if (this.linksState === "loading" && !this.links.length) {
|
|
8102
|
+
return `${eyebrow}<div class="slm-empty">Loading links\u2026</div>`;
|
|
8103
|
+
}
|
|
8104
|
+
if (!this.links.length) return "";
|
|
7958
8105
|
return `${eyebrow}
|
|
7959
|
-
${
|
|
7960
|
-
${create}
|
|
8106
|
+
${this.links.map((link) => this.linkCardHtml(link)).join("")}
|
|
7961
8107
|
<p class="slm-note">A link is shown once, when you create it. SeatLayer keeps only a fingerprint of it, so it
|
|
7962
8108
|
can never be shown again \u2014 if a link is lost, rotate it and send the fresh one.</p>`;
|
|
7963
8109
|
}
|
|
@@ -7976,7 +8122,7 @@ var ChannelsMode = class {
|
|
|
7976
8122
|
</div>` : "";
|
|
7977
8123
|
return `<div class="slm-ch-link">
|
|
7978
8124
|
<span class="lk-head">
|
|
7979
|
-
<span class="lk-name">${esc(link.label || "
|
|
8125
|
+
<span class="lk-name">${esc(link.label || "Buyer link")}</span>
|
|
7980
8126
|
<span class="slm-ch-badge ${badge.kind}">${esc(badge.text)}</span>
|
|
7981
8127
|
</span>
|
|
7982
8128
|
<div class="slm-ch-meter" role="img"
|
|
@@ -8079,10 +8225,6 @@ var ChannelsMode = class {
|
|
|
8079
8225
|
this.previewIncludePublic = includePublic.checked;
|
|
8080
8226
|
void this.loadPreview();
|
|
8081
8227
|
});
|
|
8082
|
-
const intent = rail.querySelector("[data-ch-intent]");
|
|
8083
|
-
intent?.addEventListener("change", () => {
|
|
8084
|
-
void this.setAccessIntent(intent.value);
|
|
8085
|
-
});
|
|
8086
8228
|
const grab = rail.querySelector(".slm-ch-grab");
|
|
8087
8229
|
grab?.addEventListener("click", () => this.cycleDetent());
|
|
8088
8230
|
rail.querySelectorAll("[data-ch-act]").forEach((button) => {
|
|
@@ -8127,6 +8269,13 @@ var ChannelsMode = class {
|
|
|
8127
8269
|
case "link-create":
|
|
8128
8270
|
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
8129
8271
|
break;
|
|
8272
|
+
// The one thing this screen can actually do for a website integration is
|
|
8273
|
+
// record that the channel is meant for one — which is exactly the flag the
|
|
8274
|
+
// dashboard's Embed page reads before it offers the snippet. Saying so is
|
|
8275
|
+
// honest; a fake "copy code" button on a screen with no code would not be.
|
|
8276
|
+
case "embed-code":
|
|
8277
|
+
void this.chooseWebsiteIntegration();
|
|
8278
|
+
break;
|
|
8130
8279
|
case "link-reload":
|
|
8131
8280
|
if (this.detailChannelId) void this.reloadLinks();
|
|
8132
8281
|
break;
|
|
@@ -8141,8 +8290,16 @@ var ChannelsMode = class {
|
|
|
8141
8290
|
this.conflict = false;
|
|
8142
8291
|
void this.refresh().then(() => this.openDialog({ kind: "review" }));
|
|
8143
8292
|
break;
|
|
8144
|
-
case "pick-
|
|
8145
|
-
this.
|
|
8293
|
+
case "pick-sections":
|
|
8294
|
+
this.openDialog({ kind: "scope", scope: "sections" });
|
|
8295
|
+
break;
|
|
8296
|
+
case "pick-rows":
|
|
8297
|
+
this.openDialog({ kind: "scope", scope: "rows" });
|
|
8298
|
+
break;
|
|
8299
|
+
case "drag-select":
|
|
8300
|
+
this.mapIntent = "assign";
|
|
8301
|
+
this.paintRail();
|
|
8302
|
+
this.onInteractionChange?.();
|
|
8146
8303
|
break;
|
|
8147
8304
|
case "pick-category":
|
|
8148
8305
|
this.pickCategory();
|
|
@@ -8152,12 +8309,10 @@ var ChannelsMode = class {
|
|
|
8152
8309
|
}
|
|
8153
8310
|
}
|
|
8154
8311
|
// ---- selection helpers ----------------------------------------------------
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
if (!
|
|
8158
|
-
this.
|
|
8159
|
-
this.host.selectSection(value);
|
|
8160
|
-
});
|
|
8312
|
+
/** Derived once per mode entry — see `assignmentRowsCache`. */
|
|
8313
|
+
assignmentRows() {
|
|
8314
|
+
if (!this.assignmentRowsCache) this.assignmentRowsCache = this.host.rows();
|
|
8315
|
+
return this.assignmentRowsCache;
|
|
8161
8316
|
}
|
|
8162
8317
|
pickCategory() {
|
|
8163
8318
|
const categories = this.host.categories();
|
|
@@ -8188,6 +8343,209 @@ var ChannelsMode = class {
|
|
|
8188
8343
|
});
|
|
8189
8344
|
});
|
|
8190
8345
|
}
|
|
8346
|
+
/**
|
|
8347
|
+
* Add several whole sections, or several whole rows, in one operation.
|
|
8348
|
+
*
|
|
8349
|
+
* ADDITIVE by contract: the chooser starts from the labels already selected
|
|
8350
|
+
* and only ever grows that set, so opening it never destroys a hard-won
|
|
8351
|
+
* marquee or seat-list selection. The confirm button always states the net
|
|
8352
|
+
* number of seats it will add, and refuses to exceed `MAX_ASSIGNMENT_UNITS`.
|
|
8353
|
+
*
|
|
8354
|
+
* Rows get the richer variant. A large venue has thousands of them, so they
|
|
8355
|
+
* arrive collapsed under their section, with a section-level tri-state
|
|
8356
|
+
* checkbox, a search across every row, and a render cap — the flat list used
|
|
8357
|
+
* for sections would be an unusable wall of buttons.
|
|
8358
|
+
*/
|
|
8359
|
+
renderScopeDialog(state) {
|
|
8360
|
+
const scope = state.scope === "rows" ? "rows" : "sections";
|
|
8361
|
+
const options = scope === "sections" ? this.host.sections().map((section) => ({
|
|
8362
|
+
id: section.id,
|
|
8363
|
+
label: section.label,
|
|
8364
|
+
group: "Sections",
|
|
8365
|
+
labels: this.host.labelsInSection(section.id)
|
|
8366
|
+
})) : this.assignmentRows().map((row) => ({
|
|
8367
|
+
id: row.id,
|
|
8368
|
+
label: row.label,
|
|
8369
|
+
group: row.sectionLabel,
|
|
8370
|
+
labels: row.labels
|
|
8371
|
+
}));
|
|
8372
|
+
const available = options.filter((option) => option.labels.length > 0);
|
|
8373
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
8374
|
+
for (const option of available) {
|
|
8375
|
+
const group = grouped.get(option.group) ?? [];
|
|
8376
|
+
group.push(option);
|
|
8377
|
+
grouped.set(option.group, group);
|
|
8378
|
+
}
|
|
8379
|
+
if (scope === "rows") {
|
|
8380
|
+
const groups = [...grouped.entries()];
|
|
8381
|
+
this.renderScrim(`
|
|
8382
|
+
<h3 id="slm-ch-dlg-title">Add multiple rows</h3>
|
|
8383
|
+
<p class="sub">Sections stay collapsed for speed. Select a whole section, expand only the rows you need, or search across every row.</p>
|
|
8384
|
+
<div class="slm-ch-scopebar">
|
|
8385
|
+
<label>Find section or row<input type="search" data-ch-scope-search placeholder="e.g. Orchestra or AA"></label>
|
|
8386
|
+
<span class="slm-ch-scopesummary" data-ch-scope-summary aria-live="polite">0 rows selected</span>
|
|
8387
|
+
</div>
|
|
8388
|
+
<p class="slm-ch-scopehint" data-ch-scope-filter>Showing section groups. Expand one or search to see rows.</p>
|
|
8389
|
+
<div class="slm-ch-seatlist" data-ch-scope-list></div>
|
|
8390
|
+
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8391
|
+
<div class="foot">
|
|
8392
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8393
|
+
<button type="button" class="slm-btn" data-ch-add-scope disabled>Add seats</button>
|
|
8394
|
+
</div>`, (dialog) => {
|
|
8395
|
+
const byId = new Map(available.map((option) => [option.id, option]));
|
|
8396
|
+
const picked = /* @__PURE__ */ new Set();
|
|
8397
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
8398
|
+
const current = new Set(this.host.selectionLabels());
|
|
8399
|
+
const confirm = dialog.querySelector("[data-ch-add-scope]");
|
|
8400
|
+
const error = dialog.querySelector("[data-ch-error]");
|
|
8401
|
+
const list = dialog.querySelector("[data-ch-scope-list]");
|
|
8402
|
+
const search = dialog.querySelector("[data-ch-scope-search]");
|
|
8403
|
+
const summary = dialog.querySelector("[data-ch-scope-summary]");
|
|
8404
|
+
const filterHint = dialog.querySelector("[data-ch-scope-filter]");
|
|
8405
|
+
const renderLimit = 200;
|
|
8406
|
+
let query = "";
|
|
8407
|
+
const selectedLabels = () => {
|
|
8408
|
+
const labels = new Set(current);
|
|
8409
|
+
for (const id of picked) for (const label of byId.get(id)?.labels ?? []) labels.add(label);
|
|
8410
|
+
return labels;
|
|
8411
|
+
};
|
|
8412
|
+
const update = () => {
|
|
8413
|
+
const labels = selectedLabels();
|
|
8414
|
+
const added = labels.size - current.size;
|
|
8415
|
+
const tooLarge = labels.size > MAX_ASSIGNMENT_UNITS;
|
|
8416
|
+
confirm.disabled = added === 0 || tooLarge;
|
|
8417
|
+
confirm.textContent = tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : `Add ${added.toLocaleString()} seat${added === 1 ? "" : "s"}`;
|
|
8418
|
+
error.hidden = !tooLarge;
|
|
8419
|
+
error.textContent = tooLarge ? `That would make ${labels.size.toLocaleString()} selected seats. Choose fewer rows or sections.` : "";
|
|
8420
|
+
summary.textContent = `${picked.size.toLocaleString()} row${picked.size === 1 ? "" : "s"} \xB7 ${added.toLocaleString()} seat${added === 1 ? "" : "s"} added`;
|
|
8421
|
+
};
|
|
8422
|
+
const renderList = () => {
|
|
8423
|
+
const blocks = [];
|
|
8424
|
+
let totalMatches = 0;
|
|
8425
|
+
let rendered = 0;
|
|
8426
|
+
groups.forEach(([group, items], groupIndex) => {
|
|
8427
|
+
const normalizedGroup = group.toLocaleLowerCase();
|
|
8428
|
+
const matches = query ? items.filter((item) => `${normalizedGroup} ${item.label.toLocaleLowerCase()}`.includes(query)) : items;
|
|
8429
|
+
if (query && !matches.length) return;
|
|
8430
|
+
totalMatches += matches.length;
|
|
8431
|
+
const allPicked = items.every((item) => picked.has(item.id));
|
|
8432
|
+
const somePicked = !allPicked && items.some((item) => picked.has(item.id));
|
|
8433
|
+
const open = Boolean(query) || expanded.has(groupIndex);
|
|
8434
|
+
const room = Math.max(0, renderLimit - rendered);
|
|
8435
|
+
const visible = open ? matches.slice(0, room) : [];
|
|
8436
|
+
rendered += visible.length;
|
|
8437
|
+
const seatCount = items.reduce((sum, item) => sum + item.labels.length, 0);
|
|
8438
|
+
blocks.push(`<div class="slm-ch-scopegroup">
|
|
8439
|
+
<button type="button" class="slm-ch-groupcheck" role="checkbox" aria-checked="${allPicked ? "true" : somePicked ? "mixed" : "false"}"
|
|
8440
|
+
aria-label="Select all ${items.length.toLocaleString()} rows in ${esc(group)}" data-ch-scope-group="${groupIndex}">
|
|
8441
|
+
<span class="box" aria-hidden="true">\u2713</span><span>${esc(group)}</span>
|
|
8442
|
+
<span class="meta">${items.length.toLocaleString()} rows \xB7 ${seatCount.toLocaleString()} seats</span>
|
|
8443
|
+
</button>
|
|
8444
|
+
<button type="button" class="slm-ch-grouptoggle" aria-expanded="${open}" aria-label="${open ? "Hide" : "Show"} rows in ${esc(group)}"
|
|
8445
|
+
data-ch-scope-toggle="${groupIndex}">${open ? "Hide" : "Show"}</button>
|
|
8446
|
+
</div>`);
|
|
8447
|
+
blocks.push(...visible.map((option) => `<button type="button" class="slm-ch-seatitem" role="checkbox"
|
|
8448
|
+
aria-checked="${picked.has(option.id)}" data-ch-scope-id="${esc(option.id)}">
|
|
8449
|
+
<span class="box" aria-hidden="true">\u2713</span><span>${esc(option.label)}</span>
|
|
8450
|
+
<span class="meta">${option.labels.length.toLocaleString()} seat${option.labels.length === 1 ? "" : "s"}</span>
|
|
8451
|
+
</button>`));
|
|
8452
|
+
if (open && matches.length > visible.length) {
|
|
8453
|
+
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>`);
|
|
8454
|
+
}
|
|
8455
|
+
});
|
|
8456
|
+
list.innerHTML = blocks.join("") || `<div class="slm-ch-scope-empty">No sections or rows match \u201C${esc(query)}\u201D.</div>`;
|
|
8457
|
+
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.";
|
|
8458
|
+
list.querySelectorAll("[data-ch-scope-group]").forEach((button) => button.addEventListener("click", () => {
|
|
8459
|
+
const items = groups[Number(button.dataset.chScopeGroup)]?.[1] ?? [];
|
|
8460
|
+
const remove = items.every((item) => picked.has(item.id));
|
|
8461
|
+
for (const item of items) remove ? picked.delete(item.id) : picked.add(item.id);
|
|
8462
|
+
renderList();
|
|
8463
|
+
update();
|
|
8464
|
+
}));
|
|
8465
|
+
list.querySelectorAll("[data-ch-scope-toggle]").forEach((button) => button.addEventListener("click", () => {
|
|
8466
|
+
const index = Number(button.dataset.chScopeToggle);
|
|
8467
|
+
if (expanded.has(index)) expanded.delete(index);
|
|
8468
|
+
else expanded.add(index);
|
|
8469
|
+
renderList();
|
|
8470
|
+
}));
|
|
8471
|
+
list.querySelectorAll("[data-ch-scope-id]").forEach((button) => button.addEventListener("click", () => {
|
|
8472
|
+
const id = button.dataset.chScopeId;
|
|
8473
|
+
if (picked.has(id)) picked.delete(id);
|
|
8474
|
+
else picked.add(id);
|
|
8475
|
+
renderList();
|
|
8476
|
+
update();
|
|
8477
|
+
}));
|
|
8478
|
+
};
|
|
8479
|
+
search.addEventListener("input", () => {
|
|
8480
|
+
query = search.value.trim().toLocaleLowerCase();
|
|
8481
|
+
renderList();
|
|
8482
|
+
update();
|
|
8483
|
+
});
|
|
8484
|
+
confirm.addEventListener("click", () => {
|
|
8485
|
+
const labels = [...selectedLabels()];
|
|
8486
|
+
if (!picked.size || labels.length > MAX_ASSIGNMENT_UNITS) return;
|
|
8487
|
+
this.closeDialog();
|
|
8488
|
+
this.host.selectByLabels(labels);
|
|
8489
|
+
});
|
|
8490
|
+
renderList();
|
|
8491
|
+
update();
|
|
8492
|
+
});
|
|
8493
|
+
return;
|
|
8494
|
+
}
|
|
8495
|
+
const body = [...grouped.entries()].map(([group, items]) => `
|
|
8496
|
+
<div class="slm-ch-seatgroup"><span>${esc(group)}</span></div>
|
|
8497
|
+
${items.map((option) => `<button type="button" class="slm-ch-seatitem" role="checkbox"
|
|
8498
|
+
aria-checked="false" data-ch-scope-id="${esc(option.id)}">
|
|
8499
|
+
<span class="box" aria-hidden="true">\u2713</span>
|
|
8500
|
+
<span>${esc(option.label)}</span>
|
|
8501
|
+
<span class="meta">${option.labels.length.toLocaleString()} seat${option.labels.length === 1 ? "" : "s"}</span>
|
|
8502
|
+
</button>`).join("")}`).join("");
|
|
8503
|
+
this.renderScrim(`
|
|
8504
|
+
<h3 id="slm-ch-dlg-title">Add whole sections</h3>
|
|
8505
|
+
<p class="sub">Choose one or more. They are added to the seats already selected on the map.</p>
|
|
8506
|
+
<div class="slm-ch-seatlist">${body || `<div class="slm-empty">No sections are available.</div>`}</div>
|
|
8507
|
+
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8508
|
+
<div class="foot">
|
|
8509
|
+
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
8510
|
+
<button type="button" class="slm-btn" data-ch-add-scope disabled>Add seats</button>
|
|
8511
|
+
</div>`, (dialog) => {
|
|
8512
|
+
const byId = new Map(available.map((option) => [option.id, option]));
|
|
8513
|
+
const picked = /* @__PURE__ */ new Set();
|
|
8514
|
+
const current = new Set(this.host.selectionLabels());
|
|
8515
|
+
const confirm = dialog.querySelector("[data-ch-add-scope]");
|
|
8516
|
+
const error = dialog.querySelector("[data-ch-error]");
|
|
8517
|
+
const selectedLabels = () => {
|
|
8518
|
+
const labels = new Set(current);
|
|
8519
|
+
for (const id of picked) for (const label of byId.get(id)?.labels ?? []) labels.add(label);
|
|
8520
|
+
return labels;
|
|
8521
|
+
};
|
|
8522
|
+
const update = () => {
|
|
8523
|
+
const labels = selectedLabels();
|
|
8524
|
+
const added = labels.size - current.size;
|
|
8525
|
+
const tooLarge = labels.size > MAX_ASSIGNMENT_UNITS;
|
|
8526
|
+
confirm.disabled = added === 0 || tooLarge;
|
|
8527
|
+
confirm.textContent = tooLarge ? `Maximum ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats` : `Add ${added.toLocaleString()} seat${added === 1 ? "" : "s"}`;
|
|
8528
|
+
error.hidden = !tooLarge;
|
|
8529
|
+
error.textContent = tooLarge ? `That would make ${labels.size.toLocaleString()} selected seats. Choose fewer rows or sections.` : "";
|
|
8530
|
+
};
|
|
8531
|
+
dialog.querySelectorAll("[data-ch-scope-id]").forEach((button) => {
|
|
8532
|
+
button.addEventListener("click", () => {
|
|
8533
|
+
const id = button.dataset.chScopeId;
|
|
8534
|
+
if (picked.has(id)) picked.delete(id);
|
|
8535
|
+
else picked.add(id);
|
|
8536
|
+
button.setAttribute("aria-checked", String(picked.has(id)));
|
|
8537
|
+
update();
|
|
8538
|
+
});
|
|
8539
|
+
});
|
|
8540
|
+
confirm.addEventListener("click", () => {
|
|
8541
|
+
const labels = [...selectedLabels()];
|
|
8542
|
+
if (!picked.size || labels.length > MAX_ASSIGNMENT_UNITS) return;
|
|
8543
|
+
this.closeDialog();
|
|
8544
|
+
this.host.selectByLabels(labels);
|
|
8545
|
+
});
|
|
8546
|
+
update();
|
|
8547
|
+
});
|
|
8548
|
+
}
|
|
8191
8549
|
// ---- dialogs --------------------------------------------------------------
|
|
8192
8550
|
openDialog(state) {
|
|
8193
8551
|
this.dialog = state;
|
|
@@ -8198,6 +8556,7 @@ var ChannelsMode = class {
|
|
|
8198
8556
|
if (!state) return;
|
|
8199
8557
|
if (state.kind === "create") this.renderCreateDialog(state);
|
|
8200
8558
|
else if (state.kind === "review") this.renderReviewDialog(state);
|
|
8559
|
+
else if (state.kind === "scope") this.renderScopeDialog(state);
|
|
8201
8560
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
8202
8561
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
8203
8562
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
@@ -8342,24 +8701,28 @@ var ChannelsMode = class {
|
|
|
8342
8701
|
const { labels, buckets } = this.currentPlan();
|
|
8343
8702
|
const authoritative = state.applied;
|
|
8344
8703
|
const shown = authoritative ? authoritative.buckets : buckets;
|
|
8345
|
-
const targetName = this.nameOf(this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8704
|
+
const targetName = this.nameOf(authoritative?.targetChannelId ?? this.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8346
8705
|
const rows = bucketRows(shown, targetName);
|
|
8347
8706
|
const mutations = authoritative ? authoritative.applied : mutationCount(buckets);
|
|
8348
8707
|
const allSkipped = !authoritative && labels.length > 0 && mutations === 0;
|
|
8708
|
+
const tooLarge = !authoritative && labels.length > MAX_ASSIGNMENT_UNITS;
|
|
8349
8709
|
const rowsHtml = bucketRowsHtml(rows);
|
|
8350
8710
|
const foot = authoritative ? `<div class="foot"><button type="button" class="slm-btn" data-ch-close>Done</button></div>` : `<div class="foot">
|
|
8351
8711
|
<button type="button" class="quiet" data-ch-close>Back</button>
|
|
8352
|
-
<button type="button" class="slm-btn" data-ch-apply ${allSkipped || state.busy ? "disabled" : ""}>
|
|
8712
|
+
<button type="button" class="slm-btn" data-ch-apply ${allSkipped || tooLarge || state.busy ? "disabled" : ""}>
|
|
8353
8713
|
${state.busy ? "Applying\u2026" : `Apply ${mutations.toLocaleString()} change${mutations === 1 ? "" : "s"}`}
|
|
8354
8714
|
</button>
|
|
8355
8715
|
</div>`;
|
|
8356
8716
|
const confirmNote = !authoritative && needsMoveConfirmation(buckets) ? `<p class="slm-note">Applying moves inventory out of another private channel. That is the line marked above.</p>` : "";
|
|
8357
8717
|
const skippedNote = allSkipped ? `<div class="slm-ch-alert warn"><span>\u2139</span><span>Nothing in this selection can move right now \u2014
|
|
8358
8718
|
every seat is in a buyer's checkout, already sold, or already in ${esc(targetName)}.</span></div>` : "";
|
|
8719
|
+
const limitNote = tooLarge ? `<div class="slm-ch-alert warn" role="alert"><span>\u2139</span><span><b>Choose fewer seats.</b>
|
|
8720
|
+
One Apply can cover at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats.</span></div>` : "";
|
|
8359
8721
|
this.renderScrim(`
|
|
8360
8722
|
<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>
|
|
8361
8723
|
<p class="sub">${authoritative ? "These are the exact counts the server applied." : "Every selected seat is in exactly one line below."}</p>
|
|
8362
8724
|
${skippedNote}
|
|
8725
|
+
${limitNote}
|
|
8363
8726
|
${rowsHtml || '<div class="slm-empty">Nothing selected.</div>'}
|
|
8364
8727
|
${confirmNote}
|
|
8365
8728
|
<p class="slm-ch-err" data-ch-error ${state.error ? "" : "hidden"}>${esc(state.error ?? "")}</p>
|
|
@@ -8371,15 +8734,21 @@ var ChannelsMode = class {
|
|
|
8371
8734
|
}
|
|
8372
8735
|
}
|
|
8373
8736
|
/**
|
|
8374
|
-
* Apply. On success the
|
|
8375
|
-
*
|
|
8376
|
-
*
|
|
8377
|
-
*
|
|
8737
|
+
* Apply. On success the sheet CLOSES and the staged bar confirms what moved,
|
|
8738
|
+
* naming the destination and any skipped seats, with the AUTHORITATIVE server
|
|
8739
|
+
* buckets still one Details press away. Leaving the modal up on success made
|
|
8740
|
+
* the organizer dismiss a sheet to get back to a map they had just changed.
|
|
8741
|
+
* On a stale version the server mutated nothing: keep the selection, shake
|
|
8742
|
+
* the bar once, and offer exactly one action — Refresh and review.
|
|
8378
8743
|
*/
|
|
8379
8744
|
async apply() {
|
|
8380
8745
|
if (!this.dialog || !this.caps.manage) return;
|
|
8381
8746
|
const { labels } = this.currentPlan();
|
|
8382
8747
|
if (!labels.length) return;
|
|
8748
|
+
if (labels.length > MAX_ASSIGNMENT_UNITS) {
|
|
8749
|
+
this.showDialogError(`Choose at most ${MAX_ASSIGNMENT_UNITS.toLocaleString()} seats for one Apply.`);
|
|
8750
|
+
return;
|
|
8751
|
+
}
|
|
8383
8752
|
this.dialog = { ...this.dialog, busy: true, error: null };
|
|
8384
8753
|
this.renderDialog();
|
|
8385
8754
|
try {
|
|
@@ -8392,11 +8761,10 @@ var ChannelsMode = class {
|
|
|
8392
8761
|
});
|
|
8393
8762
|
this.assignmentVersion = result.assignmentVersion;
|
|
8394
8763
|
this.conflict = false;
|
|
8395
|
-
this.dialog = { kind: "review", applied: result };
|
|
8396
8764
|
await this.refresh({ quiet: true });
|
|
8397
|
-
this.
|
|
8398
|
-
this.showApplied(result);
|
|
8765
|
+
this.closeDialog({ restoreFocus: false });
|
|
8399
8766
|
this.host.clearSelection();
|
|
8767
|
+
this.showApplied(result);
|
|
8400
8768
|
} catch (err) {
|
|
8401
8769
|
const conflict = err instanceof ManageApiError && err.status === 409 && err.code === "channel_assignment_conflict";
|
|
8402
8770
|
if (conflict) {
|
|
@@ -8419,12 +8787,26 @@ var ChannelsMode = class {
|
|
|
8419
8787
|
this.host.onError(err);
|
|
8420
8788
|
}
|
|
8421
8789
|
}
|
|
8790
|
+
/**
|
|
8791
|
+
* The success receipt, now that the review sheet closes on Apply. It names the
|
|
8792
|
+
* destination and the seats that could not move, keeps the authoritative
|
|
8793
|
+
* buckets reachable through Details, and stays up long enough to be read —
|
|
8794
|
+
* 1.2s was tuned for a confirmation the organizer was already looking at.
|
|
8795
|
+
*/
|
|
8422
8796
|
showApplied(result) {
|
|
8797
|
+
const targetName = this.nameOf(result.targetChannelId) ?? PUBLIC_CHANNEL_NAME;
|
|
8798
|
+
const skipped = result.buckets.skippedHeld.count + result.buckets.skippedBooked.count + result.buckets.notFound.count;
|
|
8423
8799
|
this.setStaged(`<span class="slm-ch-tick" aria-hidden="true">\u2713</span>
|
|
8424
|
-
<span>
|
|
8425
|
-
|
|
8800
|
+
<span>Assigned <b>${result.applied.toLocaleString()}</b> seat${result.applied === 1 ? "" : "s"} to ${esc(targetName)}
|
|
8801
|
+
${skipped ? ` \xB7 ${skipped.toLocaleString()} skipped` : ""}</span>
|
|
8802
|
+
<span class="grow"></span>
|
|
8803
|
+
<button type="button" class="drop" data-ch-applied-details>Details</button>`, "done");
|
|
8804
|
+
this.stagedEl?.querySelector("[data-ch-applied-details]")?.addEventListener("click", () => {
|
|
8805
|
+
this.openDialog({ kind: "review", applied: result });
|
|
8806
|
+
});
|
|
8807
|
+
this.announce(`Assigned ${result.applied} seat${result.applied === 1 ? "" : "s"} to ${targetName}${skipped ? `; ${skipped} skipped` : ""}.`);
|
|
8426
8808
|
if (this.stagedDoneTimer) clearTimeout(this.stagedDoneTimer);
|
|
8427
|
-
this.stagedDoneTimer = setTimeout(() => this.setStaged(null),
|
|
8809
|
+
this.stagedDoneTimer = setTimeout(() => this.setStaged(null), 5e3);
|
|
8428
8810
|
}
|
|
8429
8811
|
shakeStaged() {
|
|
8430
8812
|
const bar = this.stagedEl;
|
|
@@ -8467,12 +8849,27 @@ var ChannelsMode = class {
|
|
|
8467
8849
|
});
|
|
8468
8850
|
});
|
|
8469
8851
|
}
|
|
8852
|
+
/**
|
|
8853
|
+
* "Get embed code" — mark the channel as reached through the organizer's own
|
|
8854
|
+
* backend. The write itself is `setChannelAccessIntent(…, 'server')`, the same
|
|
8855
|
+
* API the old picker called; the difference is that it is now a deliberate
|
|
8856
|
+
* action with a consequence the organizer is told about, rather than one of
|
|
8857
|
+
* four dropdown values with no observable effect.
|
|
8858
|
+
*/
|
|
8859
|
+
async chooseWebsiteIntegration() {
|
|
8860
|
+
const channelId = this.detailChannelId;
|
|
8861
|
+
if (!channelId) return;
|
|
8862
|
+
await this.setAccessIntent("server");
|
|
8863
|
+
}
|
|
8470
8864
|
async setAccessIntent(accessIntent) {
|
|
8471
8865
|
const channelId = this.detailChannelId;
|
|
8472
8866
|
if (!channelId || !this.caps.manage) return;
|
|
8473
8867
|
try {
|
|
8474
8868
|
await this.host.api.setChannelAccessIntent(this.host.eventKey, channelId, accessIntent);
|
|
8475
8869
|
await this.refresh();
|
|
8870
|
+
if (accessIntent === "server") {
|
|
8871
|
+
this.host.toast("Marked for your website. The embed code is on the Embed page.", "ok");
|
|
8872
|
+
}
|
|
8476
8873
|
} catch (err) {
|
|
8477
8874
|
this.host.toast("Couldn't save how buyers reach this channel.", "err");
|
|
8478
8875
|
this.host.onError(err);
|
|
@@ -8613,7 +9010,7 @@ var ChannelsMode = class {
|
|
|
8613
9010
|
});
|
|
8614
9011
|
});
|
|
8615
9012
|
}
|
|
8616
|
-
// ----
|
|
9013
|
+
// ---- buyer-link dialogs ---------------------------------------------------
|
|
8617
9014
|
async reloadLinks() {
|
|
8618
9015
|
const channelId = this.detailChannelId;
|
|
8619
9016
|
if (!channelId) return;
|
|
@@ -8656,7 +9053,7 @@ var ChannelsMode = class {
|
|
|
8656
9053
|
return;
|
|
8657
9054
|
}
|
|
8658
9055
|
this.renderScrim(`
|
|
8659
|
-
<h3 id="slm-ch-dlg-title">Create a
|
|
9056
|
+
<h3 id="slm-ch-dlg-title">Create a buyer link for ${esc(channel.name)}</h3>
|
|
8660
9057
|
<p class="sub">Anyone who opens the link can buy from this channel's allocation \u2014 and only from it.
|
|
8661
9058
|
You'll see the link once, right after you create it.</p>
|
|
8662
9059
|
<div class="slm-field">
|
|
@@ -8781,7 +9178,7 @@ var ChannelsMode = class {
|
|
|
8781
9178
|
copy?.addEventListener("click", () => {
|
|
8782
9179
|
const ok = () => {
|
|
8783
9180
|
copy.textContent = "Copied";
|
|
8784
|
-
this.announce("
|
|
9181
|
+
this.announce("Buyer link copied.");
|
|
8785
9182
|
};
|
|
8786
9183
|
const clipboard = typeof navigator === "undefined" ? null : navigator.clipboard;
|
|
8787
9184
|
if (clipboard?.writeText) {
|
|
@@ -8791,7 +9188,7 @@ var ChannelsMode = class {
|
|
|
8791
9188
|
selectSecret(dialog);
|
|
8792
9189
|
});
|
|
8793
9190
|
});
|
|
8794
|
-
this.announce("Your
|
|
9191
|
+
this.announce("Your buyer link is ready and is shown once.");
|
|
8795
9192
|
}
|
|
8796
9193
|
/**
|
|
8797
9194
|
* Rotate. The organizer must SAY what happens to the buyers already inside —
|
|
@@ -8810,7 +9207,7 @@ var ChannelsMode = class {
|
|
|
8810
9207
|
<span><b>${sessions.toLocaleString()} buyer${sessions === 1 ? "" : "s"}</b> got in with the current link
|
|
8811
9208
|
and still ${sessions === 1 ? "has" : "have"} active access.</span></div>` : "";
|
|
8812
9209
|
this.renderScrim(`
|
|
8813
|
-
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "
|
|
9210
|
+
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "buyer")} link?</h3>
|
|
8814
9211
|
<p class="sub">The current link stops opening immediately and cannot be restored. You will get a new
|
|
8815
9212
|
link to copy \u2014 shown once.</p>
|
|
8816
9213
|
${warning}
|
|
@@ -8869,7 +9266,7 @@ var ChannelsMode = class {
|
|
|
8869
9266
|
}
|
|
8870
9267
|
const sessions = link.activeSessions ?? 0;
|
|
8871
9268
|
this.renderScrim(`
|
|
8872
|
-
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "
|
|
9269
|
+
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "buyer")} link?</h3>
|
|
8873
9270
|
<p class="sub">It stops opening immediately and cannot be restored \u2014 there is no undo, and no way to
|
|
8874
9271
|
bring the same URL back. Seats already bought through it keep their sale.</p>
|
|
8875
9272
|
${sessions ? `<label class="slm-ch-radio">
|
|
@@ -8986,7 +9383,12 @@ var LEGEND = [
|
|
|
8986
9383
|
var MANAGER_CSS = (
|
|
8987
9384
|
/* @sl-css */
|
|
8988
9385
|
`
|
|
8989
|
-
|
|
9386
|
+
/* The floor was 480px, which trapped the cockpit in any host shorter than that:
|
|
9387
|
+
every ancestor here is overflow:hidden, so the bottom of the rail (Create
|
|
9388
|
+
channel, Show archived) was rendered but clipped away by the host and could
|
|
9389
|
+
not be reached by scrolling anything. 320px still keeps an unsized host from
|
|
9390
|
+
collapsing to the top bar, and the rail scrolls to its full content below. */
|
|
9391
|
+
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:320px;overflow:hidden;
|
|
8990
9392
|
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
8991
9393
|
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
8992
9394
|
borrowed from CHANNELS_CSS. The base cockpit animates whether or not
|
|
@@ -9054,7 +9456,7 @@ var MANAGER_CSS = (
|
|
|
9054
9456
|
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
9055
9457
|
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
9056
9458
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
9057
|
-
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
9459
|
+
.slm-railscroll{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:16px}
|
|
9058
9460
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
9059
9461
|
.slm-hint{font-size:12.5px;color:var(--slm-muted);line-height:1.5;margin-bottom:14px}
|
|
9060
9462
|
|
|
@@ -9119,7 +9521,8 @@ var MANAGER_CSS = (
|
|
|
9119
9521
|
color:var(--slm-muted);font-size:10.5px}
|
|
9120
9522
|
.slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
|
|
9121
9523
|
.slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
|
|
9122
|
-
.slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);
|
|
9524
|
+
.slm-blockedlist{max-height:246px;overflow:auto;overscroll-behavior:contain;border:1px solid var(--slm-line);
|
|
9525
|
+
border-radius:10px;background:var(--slm-surface)}
|
|
9123
9526
|
.slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
|
|
9124
9527
|
border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
|
|
9125
9528
|
.slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
|
|
@@ -9514,6 +9917,30 @@ var SeatManager = class {
|
|
|
9514
9917
|
this.selectSection(sectionId);
|
|
9515
9918
|
},
|
|
9516
9919
|
sections: () => this.sectionOptions,
|
|
9920
|
+
labelsInSection: (sectionId) => this.renderer?.getSelectableInSection(sectionId).map((seat) => seat.label) ?? [],
|
|
9921
|
+
// Logical rows, not physical row objects: a segmented row is authored as
|
|
9922
|
+
// several objects sharing one `logicalRowId`, and the organizer thinks of
|
|
9923
|
+
// it as one row. Tables carry seats too, so they are rows here as well.
|
|
9924
|
+
rows: () => {
|
|
9925
|
+
const objects = new Map((this.doc?.objects ?? []).map((object) => [object.id, object]));
|
|
9926
|
+
const rows = /* @__PURE__ */ new Map();
|
|
9927
|
+
for (const seat of this.labelToSeat.values()) {
|
|
9928
|
+
const object = objects.get(seat.rowId);
|
|
9929
|
+
if (!object || object.type !== "row" && object.type !== "table") continue;
|
|
9930
|
+
const id = seat.logicalRowId ?? seat.rowId;
|
|
9931
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
|
|
9932
|
+
const row = rows.get(id) ?? {
|
|
9933
|
+
id,
|
|
9934
|
+
label: object.type === "row" ? object.segmentedRow?.displayLabel ?? object.displayLabel ?? object.label : object.displayLabel ?? object.label,
|
|
9935
|
+
sectionId,
|
|
9936
|
+
sectionLabel: this.sectionLabelById.get(sectionId) ?? "Other seats",
|
|
9937
|
+
labels: []
|
|
9938
|
+
};
|
|
9939
|
+
row.labels.push(seat.label);
|
|
9940
|
+
rows.set(id, row);
|
|
9941
|
+
}
|
|
9942
|
+
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" }));
|
|
9943
|
+
},
|
|
9517
9944
|
categories: () => (this.doc?.categories ?? []).map((category) => ({
|
|
9518
9945
|
key: category.key,
|
|
9519
9946
|
label: category.label ?? category.key,
|