@seatlayer/js 0.40.0 → 0.42.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 +215 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -10
- package/dist/index.d.ts +106 -10
- package/dist/index.js +215 -64
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -906,6 +906,7 @@ var SeatingChart = class {
|
|
|
906
906
|
this.mount = null;
|
|
907
907
|
this.hostEl = null;
|
|
908
908
|
this.rendered = false;
|
|
909
|
+
this.retryBtn = null;
|
|
909
910
|
this.mode_ = null;
|
|
910
911
|
this.tipEl = null;
|
|
911
912
|
this.tipPos = { x: 0, y: 0 };
|
|
@@ -967,6 +968,7 @@ var SeatingChart = class {
|
|
|
967
968
|
const info = await this.controller.render(host);
|
|
968
969
|
if (!info) {
|
|
969
970
|
this.rendered = false;
|
|
971
|
+
if (this.opts.errorDisplay !== "none") this.showLoadFailure(host);
|
|
970
972
|
return this;
|
|
971
973
|
}
|
|
972
974
|
this.controller.setViewMode(this.opts.initialView ?? "flat");
|
|
@@ -1257,10 +1259,36 @@ var SeatingChart = class {
|
|
|
1257
1259
|
}
|
|
1258
1260
|
return ok;
|
|
1259
1261
|
}
|
|
1262
|
+
/**
|
|
1263
|
+
* The visible failure state. Deliberately inline-styled and dependency-free:
|
|
1264
|
+
* this renders on a stranger's website, where our stylesheet may not have
|
|
1265
|
+
* loaded (the chart fetch just failed) and where inheriting the host's own
|
|
1266
|
+
* styles is likelier to produce something unreadable than something on-brand.
|
|
1267
|
+
*/
|
|
1268
|
+
showLoadFailure(host) {
|
|
1269
|
+
const box = document.createElement("div");
|
|
1270
|
+
box.setAttribute("role", "status");
|
|
1271
|
+
box.style.cssText = 'display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;width:100%;height:100%;min-height:180px;box-sizing:border-box;padding:24px;text-align:center;font:500 14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#3b4256;';
|
|
1272
|
+
const text = document.createElement("div");
|
|
1273
|
+
text.textContent = "The seat map didn\u2019t load.";
|
|
1274
|
+
box.appendChild(text);
|
|
1275
|
+
const btn = document.createElement("button");
|
|
1276
|
+
btn.type = "button";
|
|
1277
|
+
btn.textContent = "Try again";
|
|
1278
|
+
btn.style.cssText = "appearance:none;border:1px solid #c9cede;background:#fff;color:#10162a;border-radius:8px;padding:8px 16px;font:600 13px/1 inherit;cursor:pointer;";
|
|
1279
|
+
btn.addEventListener("click", () => {
|
|
1280
|
+
this.destroy();
|
|
1281
|
+
void this.render().catch((err) => this.opts.onError?.(err));
|
|
1282
|
+
});
|
|
1283
|
+
box.appendChild(btn);
|
|
1284
|
+
this.retryBtn = btn;
|
|
1285
|
+
host.appendChild(box);
|
|
1286
|
+
}
|
|
1260
1287
|
destroy() {
|
|
1261
1288
|
this.realtime?.stop();
|
|
1262
1289
|
this.realtime = null;
|
|
1263
1290
|
this.access?.clear();
|
|
1291
|
+
this.retryBtn = null;
|
|
1264
1292
|
if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
|
|
1265
1293
|
this.tipEl = null;
|
|
1266
1294
|
this.onTipMove = null;
|
|
@@ -5447,7 +5475,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
5447
5475
|
this.checkoutPanel = mountCheckout({
|
|
5448
5476
|
root: this.root,
|
|
5449
5477
|
state,
|
|
5450
|
-
|
|
5478
|
+
// `returnUrl` rides along so a redirecting gateway can come back to the
|
|
5479
|
+
// HOST's page. The server validates its origin against the organizer's
|
|
5480
|
+
// declared embed domains and silently falls back to our own buyer page
|
|
5481
|
+
// when it does not match, so passing one can never redirect a paid buyer
|
|
5482
|
+
// somewhere the organizer did not sanction.
|
|
5483
|
+
startSession: (input) => this.pubApi.startCheckout(this.opts.event, {
|
|
5484
|
+
...input,
|
|
5485
|
+
...this.opts.returnUrl ? { returnUrl: this.opts.returnUrl } : {}
|
|
5486
|
+
}),
|
|
5451
5487
|
orderStatus: (orderId) => this.pubApi.orderStatus(orderId),
|
|
5452
5488
|
onCancel: () => {
|
|
5453
5489
|
this.checkoutPanel = null;
|
|
@@ -6457,7 +6493,7 @@ function retryAfterCopy(details) {
|
|
|
6457
6493
|
}
|
|
6458
6494
|
function accessLine(access) {
|
|
6459
6495
|
if (!access || !access.intent) return "\u2014";
|
|
6460
|
-
const base = access.intent === "
|
|
6496
|
+
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : "Not distributed yet";
|
|
6461
6497
|
const grants = access.hasActiveGrants ? "in use now" : access.lastMintAt ? `last used ${new Date(access.lastMintAt).toLocaleDateString()}` : null;
|
|
6462
6498
|
const detail = access.detail ?? grants;
|
|
6463
6499
|
return detail ? `${base} \xB7 ${detail}` : base;
|
|
@@ -6849,7 +6885,7 @@ var ManageApi = class {
|
|
|
6849
6885
|
};
|
|
6850
6886
|
|
|
6851
6887
|
// src/channelsMode.ts
|
|
6852
|
-
var POLL_MS =
|
|
6888
|
+
var POLL_MS = 3e4;
|
|
6853
6889
|
var MAX_FLAGS = 8;
|
|
6854
6890
|
var SEAT_LIST_PAGE = 300;
|
|
6855
6891
|
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
@@ -6948,6 +6984,12 @@ var CHANNELS_CSS = (
|
|
|
6948
6984
|
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6949
6985
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
6950
6986
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
6987
|
+
/* distribute options \u2014 two actions, each with the one sentence that explains it */
|
|
6988
|
+
.slm-ch-dist{display:flex;flex-direction:column;gap:6px;padding:12px;margin-bottom:8px;border:1px solid var(--slm-line);
|
|
6989
|
+
border-radius:10px;background:var(--slm-surface)}
|
|
6990
|
+
.slm-ch-dist b{font-size:13px;font-weight:800}
|
|
6991
|
+
.slm-ch-dist .why{color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
6992
|
+
.slm-ch-dist .slm-btn{width:100%;margin-top:2px}
|
|
6951
6993
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
6952
6994
|
line-height:1.5;margin-bottom:12px}
|
|
6953
6995
|
.slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
|
|
@@ -6963,7 +7005,7 @@ var CHANNELS_CSS = (
|
|
|
6963
7005
|
/* dialogs */
|
|
6964
7006
|
.slm-ch-scrim{position:absolute;inset:0;z-index:12;background:rgba(4,6,12,.62);display:grid;place-items:center;
|
|
6965
7007
|
padding:18px;animation:slm-ch-fade var(--slm-mo-quick) var(--slm-mo-out)}
|
|
6966
|
-
.slm-ch-dialog{width:min(460px,100%);max-height:100%;overflow:auto;background:#12151f;border:1px solid var(--slm-line);
|
|
7008
|
+
.slm-ch-dialog{width:min(460px,100%);max-height:100%;overflow:auto;overscroll-behavior:contain;background:#12151f;border:1px solid var(--slm-line);
|
|
6967
7009
|
border-radius:14px;padding:20px;box-shadow:0 24px 70px rgba(0,0,0,.6);
|
|
6968
7010
|
animation:slm-ch-rise var(--slm-mo-base) var(--slm-mo-out)}
|
|
6969
7011
|
.slm-ch-dialog h3{margin:0 0 4px;font-size:16px;font-weight:800;letter-spacing:-.01em}
|
|
@@ -6990,7 +7032,7 @@ var CHANNELS_CSS = (
|
|
|
6990
7032
|
overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
6991
7033
|
.slm-ch-err{color:#f1a4a6;font-size:11.5px;margin-top:6px}
|
|
6992
7034
|
|
|
6993
|
-
/*
|
|
7035
|
+
/* buyer links \u2014 STATUS only; there is no Copy control on this card */
|
|
6994
7036
|
.slm-ch-link{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
6995
7037
|
margin-bottom:8px}
|
|
6996
7038
|
.slm-ch-link .lk-head{display:flex;align-items:center;gap:8px}
|
|
@@ -7009,8 +7051,8 @@ var CHANNELS_CSS = (
|
|
|
7009
7051
|
.slm-ch-radio input{flex:none;margin-top:2px}
|
|
7010
7052
|
.slm-ch-radio b{display:block;font-weight:800;margin-bottom:2px}
|
|
7011
7053
|
.slm-ch-radio .why{display:block;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
7012
|
-
.slm-ch-seatlist{max-height:44vh;overflow:auto;border:1px solid var(--slm-line);
|
|
7013
|
-
background:var(--slm-surface);margin-top:10px}
|
|
7054
|
+
.slm-ch-seatlist{max-height:44vh;overflow:auto;overscroll-behavior:contain;border:1px solid var(--slm-line);
|
|
7055
|
+
border-radius:10px;background:var(--slm-surface);margin-top:10px}
|
|
7014
7056
|
.slm-ch-seatgroup{padding:8px 10px;border-bottom:1px solid var(--slm-line);display:flex;align-items:center;
|
|
7015
7057
|
justify-content:space-between;gap:8px;font-size:11px;font-weight:800;color:var(--slm-muted);position:sticky;top:0;
|
|
7016
7058
|
background:var(--slm-surface)}
|
|
@@ -7060,12 +7102,12 @@ function bucketRowsHtml(rows) {
|
|
|
7060
7102
|
</div>`).join("");
|
|
7061
7103
|
}
|
|
7062
7104
|
var SERVER_INTEGRATION_HTML = `
|
|
7063
|
-
<p class="slm-eyebrow" style="margin-top:18px">
|
|
7064
|
-
<p class="slm-hint">There is nothing to set up on this screen. Your own server mints a short-lived buyer
|
|
7105
|
+
<p class="slm-eyebrow" style="margin-top:18px">On your website</p>
|
|
7106
|
+
<p class="slm-hint">There is nothing more to set up on this screen. Your own server mints a short-lived buyer
|
|
7065
7107
|
access session for this channel with the SeatLayer server SDK and hands it to the widget. A channel name
|
|
7066
7108
|
on its own never grants access.</p>
|
|
7067
7109
|
<p class="slm-note"><a class="slm-linkbtn" href="https://docs.seatlayer.io/server-api/channels"
|
|
7068
|
-
target="_blank" rel="noreferrer noopener">Read the
|
|
7110
|
+
target="_blank" rel="noreferrer noopener">Read the website integration guide \u2192</a></p>`;
|
|
7069
7111
|
function esc(value) {
|
|
7070
7112
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7071
7113
|
}
|
|
@@ -7146,6 +7188,17 @@ var ChannelsMode = class {
|
|
|
7146
7188
|
this.stagedDoneTimer = null;
|
|
7147
7189
|
this.lastSelectionCount = 0;
|
|
7148
7190
|
this.lastCounts = /* @__PURE__ */ new Map();
|
|
7191
|
+
/** The markup currently in the rail. An identical repaint is skipped, which is
|
|
7192
|
+
* what keeps the organizer's scroll position (and open <select>) alive. */
|
|
7193
|
+
this.railHtml = null;
|
|
7194
|
+
/**
|
|
7195
|
+
* The `assignmentVersion` the allocation map was built from. Walking every
|
|
7196
|
+
* allocation page is the expensive half of a refresh and the server already
|
|
7197
|
+
* tells us, in the channels response, whether ANY seat moved. Unchanged
|
|
7198
|
+
* version, unchanged allocation — so the walk is skipped entirely.
|
|
7199
|
+
*/
|
|
7200
|
+
this.allocationVersion = null;
|
|
7201
|
+
this.onVisibility = null;
|
|
7149
7202
|
this.host = host;
|
|
7150
7203
|
this.caps = capabilities;
|
|
7151
7204
|
}
|
|
@@ -7156,6 +7209,7 @@ var ChannelsMode = class {
|
|
|
7156
7209
|
this.active = true;
|
|
7157
7210
|
this.mapIntent = "pan";
|
|
7158
7211
|
this.focusedSectionId = null;
|
|
7212
|
+
this.railHtml = null;
|
|
7159
7213
|
this.ensureLayer();
|
|
7160
7214
|
this.host.root.classList.add("ch-mode");
|
|
7161
7215
|
this.applySheetClasses();
|
|
@@ -7164,8 +7218,15 @@ var ChannelsMode = class {
|
|
|
7164
7218
|
this.onInteractionChange?.();
|
|
7165
7219
|
void this.refresh();
|
|
7166
7220
|
this.pollTimer = setInterval(() => {
|
|
7221
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
7167
7222
|
void this.refresh({ quiet: true });
|
|
7168
7223
|
}, POLL_MS);
|
|
7224
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
7225
|
+
this.onVisibility = () => {
|
|
7226
|
+
if (this.active && !document.hidden) void this.refresh({ quiet: true });
|
|
7227
|
+
};
|
|
7228
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
7229
|
+
}
|
|
7169
7230
|
}
|
|
7170
7231
|
/** Called when the cockpit leaves Channels mode. Everything this mode painted
|
|
7171
7232
|
* over the map goes with it — no other tool ever inherits a channel overlay. */
|
|
@@ -7174,6 +7235,11 @@ var ChannelsMode = class {
|
|
|
7174
7235
|
this.active = false;
|
|
7175
7236
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
7176
7237
|
this.pollTimer = null;
|
|
7238
|
+
if (this.onVisibility && typeof document !== "undefined") {
|
|
7239
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
7240
|
+
}
|
|
7241
|
+
this.onVisibility = null;
|
|
7242
|
+
this.railHtml = null;
|
|
7177
7243
|
this.closeDialog({ restoreFocus: false });
|
|
7178
7244
|
this.links = [];
|
|
7179
7245
|
this.linksChannelId = null;
|
|
@@ -7262,8 +7328,11 @@ var ChannelsMode = class {
|
|
|
7262
7328
|
if (!this.targetChannelId) {
|
|
7263
7329
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
7264
7330
|
}
|
|
7265
|
-
|
|
7266
|
-
|
|
7331
|
+
if (this.allocationVersion !== list.assignmentVersion) {
|
|
7332
|
+
await this.loadAllocation(seq);
|
|
7333
|
+
if (superseded()) return;
|
|
7334
|
+
this.allocationVersion = list.assignmentVersion;
|
|
7335
|
+
}
|
|
7267
7336
|
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
7268
7337
|
if (superseded()) return;
|
|
7269
7338
|
this.loading = false;
|
|
@@ -7682,35 +7751,59 @@ var ChannelsMode = class {
|
|
|
7682
7751
|
this.paintOverlay();
|
|
7683
7752
|
}
|
|
7684
7753
|
}
|
|
7754
|
+
/**
|
|
7755
|
+
* Replace the rail's markup — but only when it actually differs, and never at
|
|
7756
|
+
* the cost of where the organizer had scrolled to.
|
|
7757
|
+
*
|
|
7758
|
+
* The rail repaints on a clock, on every selection change and after every
|
|
7759
|
+
* mutation. Rewriting `innerHTML` each time resets `scrollTop`, which is
|
|
7760
|
+
* exactly what "the rail gets stuck" was: scroll down to Create channel, the
|
|
7761
|
+
* poll ticks, and the list snaps back to the top under the cursor. So skip the
|
|
7762
|
+
* write when the markup is byte-identical, and restore the offset when it is
|
|
7763
|
+
* not.
|
|
7764
|
+
*
|
|
7765
|
+
* Returns whether the DOM was rewritten. Callers must only re-wire listeners
|
|
7766
|
+
* when it was — a skipped paint keeps the old nodes AND their listeners, so
|
|
7767
|
+
* re-wiring would double every handler.
|
|
7768
|
+
*/
|
|
7769
|
+
setRailHtml(html) {
|
|
7770
|
+
if (html === this.railHtml) return false;
|
|
7771
|
+
const rail = this.host.rail;
|
|
7772
|
+
const scrollTop = rail.scrollTop;
|
|
7773
|
+
rail.innerHTML = html;
|
|
7774
|
+
this.railHtml = html;
|
|
7775
|
+
if (scrollTop) rail.scrollTop = scrollTop;
|
|
7776
|
+
return true;
|
|
7777
|
+
}
|
|
7685
7778
|
paintRail() {
|
|
7686
7779
|
if (!this.active) return;
|
|
7687
7780
|
const rail = this.host.rail;
|
|
7688
7781
|
if (!this.caps.view) {
|
|
7689
|
-
|
|
7690
|
-
<p class="slm-hint">You need channel-management permission on this event to see allocations.</p
|
|
7782
|
+
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
7783
|
+
<p class="slm-hint">You need channel-management permission on this event to see allocations.</p>`);
|
|
7691
7784
|
return;
|
|
7692
7785
|
}
|
|
7693
7786
|
if (this.loading && !this.list) {
|
|
7694
|
-
|
|
7695
|
-
<div class="slm-empty">Loading allocations\u2026</div
|
|
7787
|
+
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
7788
|
+
<div class="slm-empty">Loading allocations\u2026</div>`);
|
|
7696
7789
|
return;
|
|
7697
7790
|
}
|
|
7698
7791
|
if (!this.list && this.loadError) {
|
|
7699
|
-
|
|
7792
|
+
if (this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
7700
7793
|
<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
7701
7794
|
<span><b>Couldn't load sales channels.</b> Everything else on this event still works.
|
|
7702
|
-
<button type="button" data-ch-act="retry">Try again</button></span></div
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7795
|
+
<button type="button" data-ch-act="retry">Try again</button></span></div>`)) {
|
|
7796
|
+
rail.querySelector('[data-ch-act="retry"]')?.addEventListener("click", () => {
|
|
7797
|
+
void this.refresh();
|
|
7798
|
+
});
|
|
7799
|
+
}
|
|
7706
7800
|
return;
|
|
7707
7801
|
}
|
|
7708
7802
|
const selection = this.host.selectionLabels();
|
|
7709
7803
|
const grab = this.host.isCompact() ? `<div class="slm-ch-grab"><span class="slm-ch-grabbar"></span></div>` : "";
|
|
7710
7804
|
const segment = this.viewSegmentHtml();
|
|
7711
7805
|
const body = this.view === "preview" ? this.previewRailHtml() : this.detailChannelId ? this.detailRailHtml(this.detailChannelId) : selection.length && this.caps.manage ? this.selectionRailHtml(selection) : this.listRailHtml();
|
|
7712
|
-
|
|
7713
|
-
this.wireRail();
|
|
7806
|
+
if (this.setRailHtml(`${grab}${segment}${body}`)) this.wireRail();
|
|
7714
7807
|
this.paintStagedBar();
|
|
7715
7808
|
}
|
|
7716
7809
|
viewSegmentHtml() {
|
|
@@ -7841,22 +7934,7 @@ var ChannelsMode = class {
|
|
|
7841
7934
|
<button type="button" class="slm-btn ghost" data-ch-act="archive">Archive\u2026</button>
|
|
7842
7935
|
</div>
|
|
7843
7936
|
<p class="slm-note">Archive returns the allocation to a destination you choose. Nothing is ever deleted silently.</p>` : "";
|
|
7844
|
-
const
|
|
7845
|
-
const intents = ["none", "internal", "server", "hosted_link"];
|
|
7846
|
-
const selfServiceGap = (intent === "server" || intent === "hosted_link") && !channel.access?.hasActiveGrants;
|
|
7847
|
-
const access = this.caps.manage ? `
|
|
7848
|
-
<p class="slm-eyebrow" style="margin-top:14px">Buyer access</p>
|
|
7849
|
-
<div class="slm-field">
|
|
7850
|
-
<label for="slm-ch-intent">How should buyers reach this channel?</label>
|
|
7851
|
-
<select class="slm-select" id="slm-ch-intent" data-ch-intent>
|
|
7852
|
-
${intents.map((value) => `<option value="${value}"${value === intent ? " selected" : ""}>${esc(accessIntentLabel(value))}</option>`).join("")}
|
|
7853
|
-
</select>
|
|
7854
|
-
</div>
|
|
7855
|
-
<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>
|
|
7856
|
-
${selfServiceGap ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
7857
|
-
<span>This channel is marked for buyer self-service but no buyer has been let in yet.</span></div>` : ""}
|
|
7858
|
-
${this.hostedLinksHtml()}
|
|
7859
|
-
${SERVER_INTEGRATION_HTML}` : "";
|
|
7937
|
+
const access = this.caps.manage ? this.distributeHtml(channel) : "";
|
|
7860
7938
|
return `
|
|
7861
7939
|
<p class="slm-eyebrow">
|
|
7862
7940
|
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
@@ -7866,7 +7944,54 @@ var ChannelsMode = class {
|
|
|
7866
7944
|
${access}
|
|
7867
7945
|
${lifecycle}`;
|
|
7868
7946
|
}
|
|
7869
|
-
|
|
7947
|
+
/**
|
|
7948
|
+
* "Distribute" — how the seats in this channel actually reach a buyer.
|
|
7949
|
+
*
|
|
7950
|
+
* This replaced a four-value "access intent" picker (none / internal /
|
|
7951
|
+
* hosted_link / server). Two of those values did nothing anywhere: no buyer or
|
|
7952
|
+
* inventory path reads `access_intent`, so `none` and `internal` were labels an
|
|
7953
|
+
* organizer could set and then wait forever for something to happen. A third,
|
|
7954
|
+
* `hosted_link`, is not the organizer's to choose at all — the server sets it
|
|
7955
|
+
* when a buyer link is created and clears it when the last live one is revoked.
|
|
7956
|
+
*
|
|
7957
|
+
* So this is two ACTIONS, not a setting: create a buyer link, or point the
|
|
7958
|
+
* channel at a website integration. Both of them do something the moment they
|
|
7959
|
+
* are pressed. A legacy row still carrying `none` or `internal` renders the
|
|
7960
|
+
* neutral "not distributed yet" state with both actions offered — the stored
|
|
7961
|
+
* value is left alone (it is organizer-declared metadata and the API that
|
|
7962
|
+
* writes it is unchanged), it simply no longer has a control of its own.
|
|
7963
|
+
*/
|
|
7964
|
+
distributeHtml(channel) {
|
|
7965
|
+
const intent = channel.access?.intent ?? "none";
|
|
7966
|
+
const live = channel.access?.hasActiveGrants === true;
|
|
7967
|
+
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.";
|
|
7968
|
+
const option = (title, why, action, cta, primary) => `<div class="slm-ch-dist">
|
|
7969
|
+
<b>${esc(title)}</b>
|
|
7970
|
+
<span class="why">${esc(why)}</span>
|
|
7971
|
+
<button type="button" class="slm-btn${primary ? "" : " ghost"}" data-ch-act="${esc(action)}">${esc(cta)}</button>
|
|
7972
|
+
</div>`;
|
|
7973
|
+
const linksSupported = this.linksState !== "unsupported";
|
|
7974
|
+
return `
|
|
7975
|
+
<p class="slm-eyebrow" style="margin-top:14px">Distribute</p>
|
|
7976
|
+
<p class="slm-hint">${esc(state)}</p>
|
|
7977
|
+
${linksSupported ? option(
|
|
7978
|
+
"Sell with a buyer link",
|
|
7979
|
+
"SeatLayer makes a link you send to a named group. They open it and buy only these seats.",
|
|
7980
|
+
"link-create",
|
|
7981
|
+
this.links.some(accessLinkIsLive) ? "Create another buyer link" : "Create buyer link",
|
|
7982
|
+
true
|
|
7983
|
+
) : ""}
|
|
7984
|
+
${option(
|
|
7985
|
+
"Integrate a website or app",
|
|
7986
|
+
"Your website's backend grants each buyer access \u2014 set up on the Embed page.",
|
|
7987
|
+
"embed-code",
|
|
7988
|
+
"Get embed code",
|
|
7989
|
+
false
|
|
7990
|
+
)}
|
|
7991
|
+
${this.hostedLinksHtml()}
|
|
7992
|
+
${intent === "server" ? SERVER_INTEGRATION_HTML : ""}`;
|
|
7993
|
+
}
|
|
7994
|
+
// ---- buyer links ----------------------------------------------------------
|
|
7870
7995
|
/**
|
|
7871
7996
|
* Read the status projection for the open channel. Never paints — the caller
|
|
7872
7997
|
* decides when the rail repaints, so a poll-driven reload does not fight a
|
|
@@ -7896,32 +8021,34 @@ var ChannelsMode = class {
|
|
|
7896
8021
|
}
|
|
7897
8022
|
}
|
|
7898
8023
|
/**
|
|
7899
|
-
* The
|
|
8024
|
+
* The buyer-link status section of the detail panel.
|
|
7900
8025
|
*
|
|
7901
8026
|
* STATUS ONLY, by design (comp 06 `hosted`): label, state, expiry,
|
|
7902
8027
|
* redemptions, seats per buyer, live sessions. There is no Copy control here
|
|
7903
8028
|
* and no field to hang one on — the URL was shown once at creation and cannot
|
|
7904
8029
|
* be produced again. Rotation is the recovery path, and it says so.
|
|
8030
|
+
*
|
|
8031
|
+
* Creating a link is the Distribute card's action, not this section's, so a
|
|
8032
|
+
* channel with no links renders nothing here rather than an empty heading and
|
|
8033
|
+
* a second button saying the same thing.
|
|
7905
8034
|
*/
|
|
7906
8035
|
hostedLinksHtml() {
|
|
7907
|
-
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">
|
|
8036
|
+
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Buyer links</p>`;
|
|
7908
8037
|
if (this.linksState === "unsupported") {
|
|
7909
8038
|
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
7910
|
-
<span><b>
|
|
8039
|
+
<span><b>Buyer links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
7911
8040
|
}
|
|
7912
8041
|
if (this.linksState === "error") {
|
|
7913
8042
|
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
7914
8043
|
<span><b>Couldn't load this channel's links.</b>
|
|
7915
8044
|
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
7916
8045
|
}
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
${live ? "Create another hosted link" : "Create hosted access link"}</button>` : "";
|
|
8046
|
+
if (this.linksState === "loading" && !this.links.length) {
|
|
8047
|
+
return `${eyebrow}<div class="slm-empty">Loading links\u2026</div>`;
|
|
8048
|
+
}
|
|
8049
|
+
if (!this.links.length) return "";
|
|
7922
8050
|
return `${eyebrow}
|
|
7923
|
-
${
|
|
7924
|
-
${create}
|
|
8051
|
+
${this.links.map((link) => this.linkCardHtml(link)).join("")}
|
|
7925
8052
|
<p class="slm-note">A link is shown once, when you create it. SeatLayer keeps only a fingerprint of it, so it
|
|
7926
8053
|
can never be shown again \u2014 if a link is lost, rotate it and send the fresh one.</p>`;
|
|
7927
8054
|
}
|
|
@@ -7940,7 +8067,7 @@ var ChannelsMode = class {
|
|
|
7940
8067
|
</div>` : "";
|
|
7941
8068
|
return `<div class="slm-ch-link">
|
|
7942
8069
|
<span class="lk-head">
|
|
7943
|
-
<span class="lk-name">${esc(link.label || "
|
|
8070
|
+
<span class="lk-name">${esc(link.label || "Buyer link")}</span>
|
|
7944
8071
|
<span class="slm-ch-badge ${badge.kind}">${esc(badge.text)}</span>
|
|
7945
8072
|
</span>
|
|
7946
8073
|
<div class="slm-ch-meter" role="img"
|
|
@@ -8043,10 +8170,6 @@ var ChannelsMode = class {
|
|
|
8043
8170
|
this.previewIncludePublic = includePublic.checked;
|
|
8044
8171
|
void this.loadPreview();
|
|
8045
8172
|
});
|
|
8046
|
-
const intent = rail.querySelector("[data-ch-intent]");
|
|
8047
|
-
intent?.addEventListener("change", () => {
|
|
8048
|
-
void this.setAccessIntent(intent.value);
|
|
8049
|
-
});
|
|
8050
8173
|
const grab = rail.querySelector(".slm-ch-grab");
|
|
8051
8174
|
grab?.addEventListener("click", () => this.cycleDetent());
|
|
8052
8175
|
rail.querySelectorAll("[data-ch-act]").forEach((button) => {
|
|
@@ -8091,6 +8214,13 @@ var ChannelsMode = class {
|
|
|
8091
8214
|
case "link-create":
|
|
8092
8215
|
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
8093
8216
|
break;
|
|
8217
|
+
// The one thing this screen can actually do for a website integration is
|
|
8218
|
+
// record that the channel is meant for one — which is exactly the flag the
|
|
8219
|
+
// dashboard's Embed page reads before it offers the snippet. Saying so is
|
|
8220
|
+
// honest; a fake "copy code" button on a screen with no code would not be.
|
|
8221
|
+
case "embed-code":
|
|
8222
|
+
void this.chooseWebsiteIntegration();
|
|
8223
|
+
break;
|
|
8094
8224
|
case "link-reload":
|
|
8095
8225
|
if (this.detailChannelId) void this.reloadLinks();
|
|
8096
8226
|
break;
|
|
@@ -8431,12 +8561,27 @@ var ChannelsMode = class {
|
|
|
8431
8561
|
});
|
|
8432
8562
|
});
|
|
8433
8563
|
}
|
|
8564
|
+
/**
|
|
8565
|
+
* "Get embed code" — mark the channel as reached through the organizer's own
|
|
8566
|
+
* backend. The write itself is `setChannelAccessIntent(…, 'server')`, the same
|
|
8567
|
+
* API the old picker called; the difference is that it is now a deliberate
|
|
8568
|
+
* action with a consequence the organizer is told about, rather than one of
|
|
8569
|
+
* four dropdown values with no observable effect.
|
|
8570
|
+
*/
|
|
8571
|
+
async chooseWebsiteIntegration() {
|
|
8572
|
+
const channelId = this.detailChannelId;
|
|
8573
|
+
if (!channelId) return;
|
|
8574
|
+
await this.setAccessIntent("server");
|
|
8575
|
+
}
|
|
8434
8576
|
async setAccessIntent(accessIntent) {
|
|
8435
8577
|
const channelId = this.detailChannelId;
|
|
8436
8578
|
if (!channelId || !this.caps.manage) return;
|
|
8437
8579
|
try {
|
|
8438
8580
|
await this.host.api.setChannelAccessIntent(this.host.eventKey, channelId, accessIntent);
|
|
8439
8581
|
await this.refresh();
|
|
8582
|
+
if (accessIntent === "server") {
|
|
8583
|
+
this.host.toast("Marked for your website. The embed code is on the Embed page.", "ok");
|
|
8584
|
+
}
|
|
8440
8585
|
} catch (err) {
|
|
8441
8586
|
this.host.toast("Couldn't save how buyers reach this channel.", "err");
|
|
8442
8587
|
this.host.onError(err);
|
|
@@ -8577,7 +8722,7 @@ var ChannelsMode = class {
|
|
|
8577
8722
|
});
|
|
8578
8723
|
});
|
|
8579
8724
|
}
|
|
8580
|
-
// ----
|
|
8725
|
+
// ---- buyer-link dialogs ---------------------------------------------------
|
|
8581
8726
|
async reloadLinks() {
|
|
8582
8727
|
const channelId = this.detailChannelId;
|
|
8583
8728
|
if (!channelId) return;
|
|
@@ -8620,7 +8765,7 @@ var ChannelsMode = class {
|
|
|
8620
8765
|
return;
|
|
8621
8766
|
}
|
|
8622
8767
|
this.renderScrim(`
|
|
8623
|
-
<h3 id="slm-ch-dlg-title">Create a
|
|
8768
|
+
<h3 id="slm-ch-dlg-title">Create a buyer link for ${esc(channel.name)}</h3>
|
|
8624
8769
|
<p class="sub">Anyone who opens the link can buy from this channel's allocation \u2014 and only from it.
|
|
8625
8770
|
You'll see the link once, right after you create it.</p>
|
|
8626
8771
|
<div class="slm-field">
|
|
@@ -8745,7 +8890,7 @@ var ChannelsMode = class {
|
|
|
8745
8890
|
copy?.addEventListener("click", () => {
|
|
8746
8891
|
const ok = () => {
|
|
8747
8892
|
copy.textContent = "Copied";
|
|
8748
|
-
this.announce("
|
|
8893
|
+
this.announce("Buyer link copied.");
|
|
8749
8894
|
};
|
|
8750
8895
|
const clipboard = typeof navigator === "undefined" ? null : navigator.clipboard;
|
|
8751
8896
|
if (clipboard?.writeText) {
|
|
@@ -8755,7 +8900,7 @@ var ChannelsMode = class {
|
|
|
8755
8900
|
selectSecret(dialog);
|
|
8756
8901
|
});
|
|
8757
8902
|
});
|
|
8758
|
-
this.announce("Your
|
|
8903
|
+
this.announce("Your buyer link is ready and is shown once.");
|
|
8759
8904
|
}
|
|
8760
8905
|
/**
|
|
8761
8906
|
* Rotate. The organizer must SAY what happens to the buyers already inside —
|
|
@@ -8774,7 +8919,7 @@ var ChannelsMode = class {
|
|
|
8774
8919
|
<span><b>${sessions.toLocaleString()} buyer${sessions === 1 ? "" : "s"}</b> got in with the current link
|
|
8775
8920
|
and still ${sessions === 1 ? "has" : "have"} active access.</span></div>` : "";
|
|
8776
8921
|
this.renderScrim(`
|
|
8777
|
-
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "
|
|
8922
|
+
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "buyer")} link?</h3>
|
|
8778
8923
|
<p class="sub">The current link stops opening immediately and cannot be restored. You will get a new
|
|
8779
8924
|
link to copy \u2014 shown once.</p>
|
|
8780
8925
|
${warning}
|
|
@@ -8833,7 +8978,7 @@ var ChannelsMode = class {
|
|
|
8833
8978
|
}
|
|
8834
8979
|
const sessions = link.activeSessions ?? 0;
|
|
8835
8980
|
this.renderScrim(`
|
|
8836
|
-
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "
|
|
8981
|
+
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "buyer")} link?</h3>
|
|
8837
8982
|
<p class="sub">It stops opening immediately and cannot be restored \u2014 there is no undo, and no way to
|
|
8838
8983
|
bring the same URL back. Seats already bought through it keep their sale.</p>
|
|
8839
8984
|
${sessions ? `<label class="slm-ch-radio">
|
|
@@ -8950,7 +9095,12 @@ var LEGEND = [
|
|
|
8950
9095
|
var MANAGER_CSS = (
|
|
8951
9096
|
/* @sl-css */
|
|
8952
9097
|
`
|
|
8953
|
-
|
|
9098
|
+
/* The floor was 480px, which trapped the cockpit in any host shorter than that:
|
|
9099
|
+
every ancestor here is overflow:hidden, so the bottom of the rail (Create
|
|
9100
|
+
channel, Show archived) was rendered but clipped away by the host and could
|
|
9101
|
+
not be reached by scrolling anything. 320px still keeps an unsized host from
|
|
9102
|
+
collapsing to the top bar, and the rail scrolls to its full content below. */
|
|
9103
|
+
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:320px;overflow:hidden;
|
|
8954
9104
|
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
8955
9105
|
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
8956
9106
|
borrowed from CHANNELS_CSS. The base cockpit animates whether or not
|
|
@@ -9018,7 +9168,7 @@ var MANAGER_CSS = (
|
|
|
9018
9168
|
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
9019
9169
|
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
9020
9170
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
9021
|
-
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
9171
|
+
.slm-railscroll{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:16px}
|
|
9022
9172
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
9023
9173
|
.slm-hint{font-size:12.5px;color:var(--slm-muted);line-height:1.5;margin-bottom:14px}
|
|
9024
9174
|
|
|
@@ -9083,7 +9233,8 @@ var MANAGER_CSS = (
|
|
|
9083
9233
|
color:var(--slm-muted);font-size:10.5px}
|
|
9084
9234
|
.slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
|
|
9085
9235
|
.slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
|
|
9086
|
-
.slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);
|
|
9236
|
+
.slm-blockedlist{max-height:246px;overflow:auto;overscroll-behavior:contain;border:1px solid var(--slm-line);
|
|
9237
|
+
border-radius:10px;background:var(--slm-surface)}
|
|
9087
9238
|
.slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
|
|
9088
9239
|
border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
|
|
9089
9240
|
.slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
|