@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.cjs
CHANGED
|
@@ -1329,6 +1329,7 @@ var SeatingChart = class {
|
|
|
1329
1329
|
this.mount = null;
|
|
1330
1330
|
this.hostEl = null;
|
|
1331
1331
|
this.rendered = false;
|
|
1332
|
+
this.retryBtn = null;
|
|
1332
1333
|
this.mode_ = null;
|
|
1333
1334
|
this.tipEl = null;
|
|
1334
1335
|
this.tipPos = { x: 0, y: 0 };
|
|
@@ -1390,6 +1391,7 @@ var SeatingChart = class {
|
|
|
1390
1391
|
const info = await this.controller.render(host);
|
|
1391
1392
|
if (!info) {
|
|
1392
1393
|
this.rendered = false;
|
|
1394
|
+
if (this.opts.errorDisplay !== "none") this.showLoadFailure(host);
|
|
1393
1395
|
return this;
|
|
1394
1396
|
}
|
|
1395
1397
|
this.controller.setViewMode(this.opts.initialView ?? "flat");
|
|
@@ -1680,10 +1682,36 @@ var SeatingChart = class {
|
|
|
1680
1682
|
}
|
|
1681
1683
|
return ok;
|
|
1682
1684
|
}
|
|
1685
|
+
/**
|
|
1686
|
+
* The visible failure state. Deliberately inline-styled and dependency-free:
|
|
1687
|
+
* this renders on a stranger's website, where our stylesheet may not have
|
|
1688
|
+
* loaded (the chart fetch just failed) and where inheriting the host's own
|
|
1689
|
+
* styles is likelier to produce something unreadable than something on-brand.
|
|
1690
|
+
*/
|
|
1691
|
+
showLoadFailure(host) {
|
|
1692
|
+
const box = document.createElement("div");
|
|
1693
|
+
box.setAttribute("role", "status");
|
|
1694
|
+
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;';
|
|
1695
|
+
const text = document.createElement("div");
|
|
1696
|
+
text.textContent = "The seat map didn\u2019t load.";
|
|
1697
|
+
box.appendChild(text);
|
|
1698
|
+
const btn = document.createElement("button");
|
|
1699
|
+
btn.type = "button";
|
|
1700
|
+
btn.textContent = "Try again";
|
|
1701
|
+
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;";
|
|
1702
|
+
btn.addEventListener("click", () => {
|
|
1703
|
+
this.destroy();
|
|
1704
|
+
void this.render().catch((err) => this.opts.onError?.(err));
|
|
1705
|
+
});
|
|
1706
|
+
box.appendChild(btn);
|
|
1707
|
+
this.retryBtn = btn;
|
|
1708
|
+
host.appendChild(box);
|
|
1709
|
+
}
|
|
1683
1710
|
destroy() {
|
|
1684
1711
|
this.realtime?.stop();
|
|
1685
1712
|
this.realtime = null;
|
|
1686
1713
|
this.access?.clear();
|
|
1714
|
+
this.retryBtn = null;
|
|
1687
1715
|
if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
|
|
1688
1716
|
this.tipEl = null;
|
|
1689
1717
|
this.onTipMove = null;
|
|
@@ -5862,7 +5890,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
5862
5890
|
this.checkoutPanel = mountCheckout2({
|
|
5863
5891
|
root: this.root,
|
|
5864
5892
|
state,
|
|
5865
|
-
|
|
5893
|
+
// `returnUrl` rides along so a redirecting gateway can come back to the
|
|
5894
|
+
// HOST's page. The server validates its origin against the organizer's
|
|
5895
|
+
// declared embed domains and silently falls back to our own buyer page
|
|
5896
|
+
// when it does not match, so passing one can never redirect a paid buyer
|
|
5897
|
+
// somewhere the organizer did not sanction.
|
|
5898
|
+
startSession: (input) => this.pubApi.startCheckout(this.opts.event, {
|
|
5899
|
+
...input,
|
|
5900
|
+
...this.opts.returnUrl ? { returnUrl: this.opts.returnUrl } : {}
|
|
5901
|
+
}),
|
|
5866
5902
|
orderStatus: (orderId) => this.pubApi.orderStatus(orderId),
|
|
5867
5903
|
onCancel: () => {
|
|
5868
5904
|
this.checkoutPanel = null;
|
|
@@ -6865,7 +6901,7 @@ function retryAfterCopy(details) {
|
|
|
6865
6901
|
}
|
|
6866
6902
|
function accessLine(access) {
|
|
6867
6903
|
if (!access || !access.intent) return "\u2014";
|
|
6868
|
-
const base = access.intent === "
|
|
6904
|
+
const base = access.intent === "server" ? "Website integration" : access.intent === "hosted_link" ? "Buyer link" : "Not distributed yet";
|
|
6869
6905
|
const grants = access.hasActiveGrants ? "in use now" : access.lastMintAt ? `last used ${new Date(access.lastMintAt).toLocaleDateString()}` : null;
|
|
6870
6906
|
const detail = access.detail ?? grants;
|
|
6871
6907
|
return detail ? `${base} \xB7 ${detail}` : base;
|
|
@@ -7257,7 +7293,7 @@ var ManageApi = class {
|
|
|
7257
7293
|
};
|
|
7258
7294
|
|
|
7259
7295
|
// src/channelsMode.ts
|
|
7260
|
-
var POLL_MS =
|
|
7296
|
+
var POLL_MS = 3e4;
|
|
7261
7297
|
var MAX_FLAGS = 8;
|
|
7262
7298
|
var SEAT_LIST_PAGE = 300;
|
|
7263
7299
|
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
@@ -7356,6 +7392,12 @@ var CHANNELS_CSS = (
|
|
|
7356
7392
|
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
7357
7393
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
7358
7394
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
7395
|
+
/* distribute options \u2014 two actions, each with the one sentence that explains it */
|
|
7396
|
+
.slm-ch-dist{display:flex;flex-direction:column;gap:6px;padding:12px;margin-bottom:8px;border:1px solid var(--slm-line);
|
|
7397
|
+
border-radius:10px;background:var(--slm-surface)}
|
|
7398
|
+
.slm-ch-dist b{font-size:13px;font-weight:800}
|
|
7399
|
+
.slm-ch-dist .why{color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
7400
|
+
.slm-ch-dist .slm-btn{width:100%;margin-top:2px}
|
|
7359
7401
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
7360
7402
|
line-height:1.5;margin-bottom:12px}
|
|
7361
7403
|
.slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
|
|
@@ -7371,7 +7413,7 @@ var CHANNELS_CSS = (
|
|
|
7371
7413
|
/* dialogs */
|
|
7372
7414
|
.slm-ch-scrim{position:absolute;inset:0;z-index:12;background:rgba(4,6,12,.62);display:grid;place-items:center;
|
|
7373
7415
|
padding:18px;animation:slm-ch-fade var(--slm-mo-quick) var(--slm-mo-out)}
|
|
7374
|
-
.slm-ch-dialog{width:min(460px,100%);max-height:100%;overflow:auto;background:#12151f;border:1px solid var(--slm-line);
|
|
7416
|
+
.slm-ch-dialog{width:min(460px,100%);max-height:100%;overflow:auto;overscroll-behavior:contain;background:#12151f;border:1px solid var(--slm-line);
|
|
7375
7417
|
border-radius:14px;padding:20px;box-shadow:0 24px 70px rgba(0,0,0,.6);
|
|
7376
7418
|
animation:slm-ch-rise var(--slm-mo-base) var(--slm-mo-out)}
|
|
7377
7419
|
.slm-ch-dialog h3{margin:0 0 4px;font-size:16px;font-weight:800;letter-spacing:-.01em}
|
|
@@ -7398,7 +7440,7 @@ var CHANNELS_CSS = (
|
|
|
7398
7440
|
overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
7399
7441
|
.slm-ch-err{color:#f1a4a6;font-size:11.5px;margin-top:6px}
|
|
7400
7442
|
|
|
7401
|
-
/*
|
|
7443
|
+
/* buyer links \u2014 STATUS only; there is no Copy control on this card */
|
|
7402
7444
|
.slm-ch-link{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
7403
7445
|
margin-bottom:8px}
|
|
7404
7446
|
.slm-ch-link .lk-head{display:flex;align-items:center;gap:8px}
|
|
@@ -7417,8 +7459,8 @@ var CHANNELS_CSS = (
|
|
|
7417
7459
|
.slm-ch-radio input{flex:none;margin-top:2px}
|
|
7418
7460
|
.slm-ch-radio b{display:block;font-weight:800;margin-bottom:2px}
|
|
7419
7461
|
.slm-ch-radio .why{display:block;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
7420
|
-
.slm-ch-seatlist{max-height:44vh;overflow:auto;border:1px solid var(--slm-line);
|
|
7421
|
-
background:var(--slm-surface);margin-top:10px}
|
|
7462
|
+
.slm-ch-seatlist{max-height:44vh;overflow:auto;overscroll-behavior:contain;border:1px solid var(--slm-line);
|
|
7463
|
+
border-radius:10px;background:var(--slm-surface);margin-top:10px}
|
|
7422
7464
|
.slm-ch-seatgroup{padding:8px 10px;border-bottom:1px solid var(--slm-line);display:flex;align-items:center;
|
|
7423
7465
|
justify-content:space-between;gap:8px;font-size:11px;font-weight:800;color:var(--slm-muted);position:sticky;top:0;
|
|
7424
7466
|
background:var(--slm-surface)}
|
|
@@ -7468,12 +7510,12 @@ function bucketRowsHtml(rows) {
|
|
|
7468
7510
|
</div>`).join("");
|
|
7469
7511
|
}
|
|
7470
7512
|
var SERVER_INTEGRATION_HTML = `
|
|
7471
|
-
<p class="slm-eyebrow" style="margin-top:18px">
|
|
7472
|
-
<p class="slm-hint">There is nothing to set up on this screen. Your own server mints a short-lived buyer
|
|
7513
|
+
<p class="slm-eyebrow" style="margin-top:18px">On your website</p>
|
|
7514
|
+
<p class="slm-hint">There is nothing more to set up on this screen. Your own server mints a short-lived buyer
|
|
7473
7515
|
access session for this channel with the SeatLayer server SDK and hands it to the widget. A channel name
|
|
7474
7516
|
on its own never grants access.</p>
|
|
7475
7517
|
<p class="slm-note"><a class="slm-linkbtn" href="https://docs.seatlayer.io/server-api/channels"
|
|
7476
|
-
target="_blank" rel="noreferrer noopener">Read the
|
|
7518
|
+
target="_blank" rel="noreferrer noopener">Read the website integration guide \u2192</a></p>`;
|
|
7477
7519
|
function esc(value) {
|
|
7478
7520
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7479
7521
|
}
|
|
@@ -7554,6 +7596,17 @@ var ChannelsMode = class {
|
|
|
7554
7596
|
this.stagedDoneTimer = null;
|
|
7555
7597
|
this.lastSelectionCount = 0;
|
|
7556
7598
|
this.lastCounts = /* @__PURE__ */ new Map();
|
|
7599
|
+
/** The markup currently in the rail. An identical repaint is skipped, which is
|
|
7600
|
+
* what keeps the organizer's scroll position (and open <select>) alive. */
|
|
7601
|
+
this.railHtml = null;
|
|
7602
|
+
/**
|
|
7603
|
+
* The `assignmentVersion` the allocation map was built from. Walking every
|
|
7604
|
+
* allocation page is the expensive half of a refresh and the server already
|
|
7605
|
+
* tells us, in the channels response, whether ANY seat moved. Unchanged
|
|
7606
|
+
* version, unchanged allocation — so the walk is skipped entirely.
|
|
7607
|
+
*/
|
|
7608
|
+
this.allocationVersion = null;
|
|
7609
|
+
this.onVisibility = null;
|
|
7557
7610
|
this.host = host;
|
|
7558
7611
|
this.caps = capabilities;
|
|
7559
7612
|
}
|
|
@@ -7564,6 +7617,7 @@ var ChannelsMode = class {
|
|
|
7564
7617
|
this.active = true;
|
|
7565
7618
|
this.mapIntent = "pan";
|
|
7566
7619
|
this.focusedSectionId = null;
|
|
7620
|
+
this.railHtml = null;
|
|
7567
7621
|
this.ensureLayer();
|
|
7568
7622
|
this.host.root.classList.add("ch-mode");
|
|
7569
7623
|
this.applySheetClasses();
|
|
@@ -7572,8 +7626,15 @@ var ChannelsMode = class {
|
|
|
7572
7626
|
this.onInteractionChange?.();
|
|
7573
7627
|
void this.refresh();
|
|
7574
7628
|
this.pollTimer = setInterval(() => {
|
|
7629
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
7575
7630
|
void this.refresh({ quiet: true });
|
|
7576
7631
|
}, POLL_MS);
|
|
7632
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
7633
|
+
this.onVisibility = () => {
|
|
7634
|
+
if (this.active && !document.hidden) void this.refresh({ quiet: true });
|
|
7635
|
+
};
|
|
7636
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
7637
|
+
}
|
|
7577
7638
|
}
|
|
7578
7639
|
/** Called when the cockpit leaves Channels mode. Everything this mode painted
|
|
7579
7640
|
* over the map goes with it — no other tool ever inherits a channel overlay. */
|
|
@@ -7582,6 +7643,11 @@ var ChannelsMode = class {
|
|
|
7582
7643
|
this.active = false;
|
|
7583
7644
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
7584
7645
|
this.pollTimer = null;
|
|
7646
|
+
if (this.onVisibility && typeof document !== "undefined") {
|
|
7647
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
7648
|
+
}
|
|
7649
|
+
this.onVisibility = null;
|
|
7650
|
+
this.railHtml = null;
|
|
7585
7651
|
this.closeDialog({ restoreFocus: false });
|
|
7586
7652
|
this.links = [];
|
|
7587
7653
|
this.linksChannelId = null;
|
|
@@ -7670,8 +7736,11 @@ var ChannelsMode = class {
|
|
|
7670
7736
|
if (!this.targetChannelId) {
|
|
7671
7737
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
7672
7738
|
}
|
|
7673
|
-
|
|
7674
|
-
|
|
7739
|
+
if (this.allocationVersion !== list.assignmentVersion) {
|
|
7740
|
+
await this.loadAllocation(seq);
|
|
7741
|
+
if (superseded()) return;
|
|
7742
|
+
this.allocationVersion = list.assignmentVersion;
|
|
7743
|
+
}
|
|
7675
7744
|
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
7676
7745
|
if (superseded()) return;
|
|
7677
7746
|
this.loading = false;
|
|
@@ -8090,35 +8159,59 @@ var ChannelsMode = class {
|
|
|
8090
8159
|
this.paintOverlay();
|
|
8091
8160
|
}
|
|
8092
8161
|
}
|
|
8162
|
+
/**
|
|
8163
|
+
* Replace the rail's markup — but only when it actually differs, and never at
|
|
8164
|
+
* the cost of where the organizer had scrolled to.
|
|
8165
|
+
*
|
|
8166
|
+
* The rail repaints on a clock, on every selection change and after every
|
|
8167
|
+
* mutation. Rewriting `innerHTML` each time resets `scrollTop`, which is
|
|
8168
|
+
* exactly what "the rail gets stuck" was: scroll down to Create channel, the
|
|
8169
|
+
* poll ticks, and the list snaps back to the top under the cursor. So skip the
|
|
8170
|
+
* write when the markup is byte-identical, and restore the offset when it is
|
|
8171
|
+
* not.
|
|
8172
|
+
*
|
|
8173
|
+
* Returns whether the DOM was rewritten. Callers must only re-wire listeners
|
|
8174
|
+
* when it was — a skipped paint keeps the old nodes AND their listeners, so
|
|
8175
|
+
* re-wiring would double every handler.
|
|
8176
|
+
*/
|
|
8177
|
+
setRailHtml(html) {
|
|
8178
|
+
if (html === this.railHtml) return false;
|
|
8179
|
+
const rail = this.host.rail;
|
|
8180
|
+
const scrollTop = rail.scrollTop;
|
|
8181
|
+
rail.innerHTML = html;
|
|
8182
|
+
this.railHtml = html;
|
|
8183
|
+
if (scrollTop) rail.scrollTop = scrollTop;
|
|
8184
|
+
return true;
|
|
8185
|
+
}
|
|
8093
8186
|
paintRail() {
|
|
8094
8187
|
if (!this.active) return;
|
|
8095
8188
|
const rail = this.host.rail;
|
|
8096
8189
|
if (!this.caps.view) {
|
|
8097
|
-
|
|
8098
|
-
<p class="slm-hint">You need channel-management permission on this event to see allocations.</p
|
|
8190
|
+
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8191
|
+
<p class="slm-hint">You need channel-management permission on this event to see allocations.</p>`);
|
|
8099
8192
|
return;
|
|
8100
8193
|
}
|
|
8101
8194
|
if (this.loading && !this.list) {
|
|
8102
|
-
|
|
8103
|
-
<div class="slm-empty">Loading allocations\u2026</div
|
|
8195
|
+
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8196
|
+
<div class="slm-empty">Loading allocations\u2026</div>`);
|
|
8104
8197
|
return;
|
|
8105
8198
|
}
|
|
8106
8199
|
if (!this.list && this.loadError) {
|
|
8107
|
-
|
|
8200
|
+
if (this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8108
8201
|
<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8109
8202
|
<span><b>Couldn't load sales channels.</b> Everything else on this event still works.
|
|
8110
|
-
<button type="button" data-ch-act="retry">Try again</button></span></div
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8203
|
+
<button type="button" data-ch-act="retry">Try again</button></span></div>`)) {
|
|
8204
|
+
rail.querySelector('[data-ch-act="retry"]')?.addEventListener("click", () => {
|
|
8205
|
+
void this.refresh();
|
|
8206
|
+
});
|
|
8207
|
+
}
|
|
8114
8208
|
return;
|
|
8115
8209
|
}
|
|
8116
8210
|
const selection = this.host.selectionLabels();
|
|
8117
8211
|
const grab = this.host.isCompact() ? `<div class="slm-ch-grab"><span class="slm-ch-grabbar"></span></div>` : "";
|
|
8118
8212
|
const segment = this.viewSegmentHtml();
|
|
8119
8213
|
const body = this.view === "preview" ? this.previewRailHtml() : this.detailChannelId ? this.detailRailHtml(this.detailChannelId) : selection.length && this.caps.manage ? this.selectionRailHtml(selection) : this.listRailHtml();
|
|
8120
|
-
|
|
8121
|
-
this.wireRail();
|
|
8214
|
+
if (this.setRailHtml(`${grab}${segment}${body}`)) this.wireRail();
|
|
8122
8215
|
this.paintStagedBar();
|
|
8123
8216
|
}
|
|
8124
8217
|
viewSegmentHtml() {
|
|
@@ -8249,22 +8342,7 @@ var ChannelsMode = class {
|
|
|
8249
8342
|
<button type="button" class="slm-btn ghost" data-ch-act="archive">Archive\u2026</button>
|
|
8250
8343
|
</div>
|
|
8251
8344
|
<p class="slm-note">Archive returns the allocation to a destination you choose. Nothing is ever deleted silently.</p>` : "";
|
|
8252
|
-
const
|
|
8253
|
-
const intents = ["none", "internal", "server", "hosted_link"];
|
|
8254
|
-
const selfServiceGap = (intent === "server" || intent === "hosted_link") && !channel.access?.hasActiveGrants;
|
|
8255
|
-
const access = this.caps.manage ? `
|
|
8256
|
-
<p class="slm-eyebrow" style="margin-top:14px">Buyer access</p>
|
|
8257
|
-
<div class="slm-field">
|
|
8258
|
-
<label for="slm-ch-intent">How should buyers reach this channel?</label>
|
|
8259
|
-
<select class="slm-select" id="slm-ch-intent" data-ch-intent>
|
|
8260
|
-
${intents.map((value) => `<option value="${value}"${value === intent ? " selected" : ""}>${esc(accessIntentLabel(value))}</option>`).join("")}
|
|
8261
|
-
</select>
|
|
8262
|
-
</div>
|
|
8263
|
-
<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>
|
|
8264
|
-
${selfServiceGap ? `<div class="slm-ch-alert warn"><span>\u26A0</span>
|
|
8265
|
-
<span>This channel is marked for buyer self-service but no buyer has been let in yet.</span></div>` : ""}
|
|
8266
|
-
${this.hostedLinksHtml()}
|
|
8267
|
-
${SERVER_INTEGRATION_HTML}` : "";
|
|
8345
|
+
const access = this.caps.manage ? this.distributeHtml(channel) : "";
|
|
8268
8346
|
return `
|
|
8269
8347
|
<p class="slm-eyebrow">
|
|
8270
8348
|
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
@@ -8274,7 +8352,54 @@ var ChannelsMode = class {
|
|
|
8274
8352
|
${access}
|
|
8275
8353
|
${lifecycle}`;
|
|
8276
8354
|
}
|
|
8277
|
-
|
|
8355
|
+
/**
|
|
8356
|
+
* "Distribute" — how the seats in this channel actually reach a buyer.
|
|
8357
|
+
*
|
|
8358
|
+
* This replaced a four-value "access intent" picker (none / internal /
|
|
8359
|
+
* hosted_link / server). Two of those values did nothing anywhere: no buyer or
|
|
8360
|
+
* inventory path reads `access_intent`, so `none` and `internal` were labels an
|
|
8361
|
+
* organizer could set and then wait forever for something to happen. A third,
|
|
8362
|
+
* `hosted_link`, is not the organizer's to choose at all — the server sets it
|
|
8363
|
+
* when a buyer link is created and clears it when the last live one is revoked.
|
|
8364
|
+
*
|
|
8365
|
+
* So this is two ACTIONS, not a setting: create a buyer link, or point the
|
|
8366
|
+
* channel at a website integration. Both of them do something the moment they
|
|
8367
|
+
* are pressed. A legacy row still carrying `none` or `internal` renders the
|
|
8368
|
+
* neutral "not distributed yet" state with both actions offered — the stored
|
|
8369
|
+
* value is left alone (it is organizer-declared metadata and the API that
|
|
8370
|
+
* writes it is unchanged), it simply no longer has a control of its own.
|
|
8371
|
+
*/
|
|
8372
|
+
distributeHtml(channel) {
|
|
8373
|
+
const intent = channel.access?.intent ?? "none";
|
|
8374
|
+
const live = channel.access?.hasActiveGrants === true;
|
|
8375
|
+
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.";
|
|
8376
|
+
const option = (title, why, action, cta, primary) => `<div class="slm-ch-dist">
|
|
8377
|
+
<b>${esc(title)}</b>
|
|
8378
|
+
<span class="why">${esc(why)}</span>
|
|
8379
|
+
<button type="button" class="slm-btn${primary ? "" : " ghost"}" data-ch-act="${esc(action)}">${esc(cta)}</button>
|
|
8380
|
+
</div>`;
|
|
8381
|
+
const linksSupported = this.linksState !== "unsupported";
|
|
8382
|
+
return `
|
|
8383
|
+
<p class="slm-eyebrow" style="margin-top:14px">Distribute</p>
|
|
8384
|
+
<p class="slm-hint">${esc(state)}</p>
|
|
8385
|
+
${linksSupported ? option(
|
|
8386
|
+
"Sell with a buyer link",
|
|
8387
|
+
"SeatLayer makes a link you send to a named group. They open it and buy only these seats.",
|
|
8388
|
+
"link-create",
|
|
8389
|
+
this.links.some(accessLinkIsLive) ? "Create another buyer link" : "Create buyer link",
|
|
8390
|
+
true
|
|
8391
|
+
) : ""}
|
|
8392
|
+
${option(
|
|
8393
|
+
"Integrate a website or app",
|
|
8394
|
+
"Your website's backend grants each buyer access \u2014 set up on the Embed page.",
|
|
8395
|
+
"embed-code",
|
|
8396
|
+
"Get embed code",
|
|
8397
|
+
false
|
|
8398
|
+
)}
|
|
8399
|
+
${this.hostedLinksHtml()}
|
|
8400
|
+
${intent === "server" ? SERVER_INTEGRATION_HTML : ""}`;
|
|
8401
|
+
}
|
|
8402
|
+
// ---- buyer links ----------------------------------------------------------
|
|
8278
8403
|
/**
|
|
8279
8404
|
* Read the status projection for the open channel. Never paints — the caller
|
|
8280
8405
|
* decides when the rail repaints, so a poll-driven reload does not fight a
|
|
@@ -8304,32 +8429,34 @@ var ChannelsMode = class {
|
|
|
8304
8429
|
}
|
|
8305
8430
|
}
|
|
8306
8431
|
/**
|
|
8307
|
-
* The
|
|
8432
|
+
* The buyer-link status section of the detail panel.
|
|
8308
8433
|
*
|
|
8309
8434
|
* STATUS ONLY, by design (comp 06 `hosted`): label, state, expiry,
|
|
8310
8435
|
* redemptions, seats per buyer, live sessions. There is no Copy control here
|
|
8311
8436
|
* and no field to hang one on — the URL was shown once at creation and cannot
|
|
8312
8437
|
* be produced again. Rotation is the recovery path, and it says so.
|
|
8438
|
+
*
|
|
8439
|
+
* Creating a link is the Distribute card's action, not this section's, so a
|
|
8440
|
+
* channel with no links renders nothing here rather than an empty heading and
|
|
8441
|
+
* a second button saying the same thing.
|
|
8313
8442
|
*/
|
|
8314
8443
|
hostedLinksHtml() {
|
|
8315
|
-
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">
|
|
8444
|
+
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Buyer links</p>`;
|
|
8316
8445
|
if (this.linksState === "unsupported") {
|
|
8317
8446
|
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
8318
|
-
<span><b>
|
|
8447
|
+
<span><b>Buyer links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
8319
8448
|
}
|
|
8320
8449
|
if (this.linksState === "error") {
|
|
8321
8450
|
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8322
8451
|
<span><b>Couldn't load this channel's links.</b>
|
|
8323
8452
|
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
8324
8453
|
}
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
${live ? "Create another hosted link" : "Create hosted access link"}</button>` : "";
|
|
8454
|
+
if (this.linksState === "loading" && !this.links.length) {
|
|
8455
|
+
return `${eyebrow}<div class="slm-empty">Loading links\u2026</div>`;
|
|
8456
|
+
}
|
|
8457
|
+
if (!this.links.length) return "";
|
|
8330
8458
|
return `${eyebrow}
|
|
8331
|
-
${
|
|
8332
|
-
${create}
|
|
8459
|
+
${this.links.map((link) => this.linkCardHtml(link)).join("")}
|
|
8333
8460
|
<p class="slm-note">A link is shown once, when you create it. SeatLayer keeps only a fingerprint of it, so it
|
|
8334
8461
|
can never be shown again \u2014 if a link is lost, rotate it and send the fresh one.</p>`;
|
|
8335
8462
|
}
|
|
@@ -8348,7 +8475,7 @@ var ChannelsMode = class {
|
|
|
8348
8475
|
</div>` : "";
|
|
8349
8476
|
return `<div class="slm-ch-link">
|
|
8350
8477
|
<span class="lk-head">
|
|
8351
|
-
<span class="lk-name">${esc(link.label || "
|
|
8478
|
+
<span class="lk-name">${esc(link.label || "Buyer link")}</span>
|
|
8352
8479
|
<span class="slm-ch-badge ${badge.kind}">${esc(badge.text)}</span>
|
|
8353
8480
|
</span>
|
|
8354
8481
|
<div class="slm-ch-meter" role="img"
|
|
@@ -8451,10 +8578,6 @@ var ChannelsMode = class {
|
|
|
8451
8578
|
this.previewIncludePublic = includePublic.checked;
|
|
8452
8579
|
void this.loadPreview();
|
|
8453
8580
|
});
|
|
8454
|
-
const intent = rail.querySelector("[data-ch-intent]");
|
|
8455
|
-
intent?.addEventListener("change", () => {
|
|
8456
|
-
void this.setAccessIntent(intent.value);
|
|
8457
|
-
});
|
|
8458
8581
|
const grab = rail.querySelector(".slm-ch-grab");
|
|
8459
8582
|
grab?.addEventListener("click", () => this.cycleDetent());
|
|
8460
8583
|
rail.querySelectorAll("[data-ch-act]").forEach((button) => {
|
|
@@ -8499,6 +8622,13 @@ var ChannelsMode = class {
|
|
|
8499
8622
|
case "link-create":
|
|
8500
8623
|
this.openDialog({ kind: "linkCreate", channelId: this.detailChannelId });
|
|
8501
8624
|
break;
|
|
8625
|
+
// The one thing this screen can actually do for a website integration is
|
|
8626
|
+
// record that the channel is meant for one — which is exactly the flag the
|
|
8627
|
+
// dashboard's Embed page reads before it offers the snippet. Saying so is
|
|
8628
|
+
// honest; a fake "copy code" button on a screen with no code would not be.
|
|
8629
|
+
case "embed-code":
|
|
8630
|
+
void this.chooseWebsiteIntegration();
|
|
8631
|
+
break;
|
|
8502
8632
|
case "link-reload":
|
|
8503
8633
|
if (this.detailChannelId) void this.reloadLinks();
|
|
8504
8634
|
break;
|
|
@@ -8839,12 +8969,27 @@ var ChannelsMode = class {
|
|
|
8839
8969
|
});
|
|
8840
8970
|
});
|
|
8841
8971
|
}
|
|
8972
|
+
/**
|
|
8973
|
+
* "Get embed code" — mark the channel as reached through the organizer's own
|
|
8974
|
+
* backend. The write itself is `setChannelAccessIntent(…, 'server')`, the same
|
|
8975
|
+
* API the old picker called; the difference is that it is now a deliberate
|
|
8976
|
+
* action with a consequence the organizer is told about, rather than one of
|
|
8977
|
+
* four dropdown values with no observable effect.
|
|
8978
|
+
*/
|
|
8979
|
+
async chooseWebsiteIntegration() {
|
|
8980
|
+
const channelId = this.detailChannelId;
|
|
8981
|
+
if (!channelId) return;
|
|
8982
|
+
await this.setAccessIntent("server");
|
|
8983
|
+
}
|
|
8842
8984
|
async setAccessIntent(accessIntent) {
|
|
8843
8985
|
const channelId = this.detailChannelId;
|
|
8844
8986
|
if (!channelId || !this.caps.manage) return;
|
|
8845
8987
|
try {
|
|
8846
8988
|
await this.host.api.setChannelAccessIntent(this.host.eventKey, channelId, accessIntent);
|
|
8847
8989
|
await this.refresh();
|
|
8990
|
+
if (accessIntent === "server") {
|
|
8991
|
+
this.host.toast("Marked for your website. The embed code is on the Embed page.", "ok");
|
|
8992
|
+
}
|
|
8848
8993
|
} catch (err) {
|
|
8849
8994
|
this.host.toast("Couldn't save how buyers reach this channel.", "err");
|
|
8850
8995
|
this.host.onError(err);
|
|
@@ -8985,7 +9130,7 @@ var ChannelsMode = class {
|
|
|
8985
9130
|
});
|
|
8986
9131
|
});
|
|
8987
9132
|
}
|
|
8988
|
-
// ----
|
|
9133
|
+
// ---- buyer-link dialogs ---------------------------------------------------
|
|
8989
9134
|
async reloadLinks() {
|
|
8990
9135
|
const channelId = this.detailChannelId;
|
|
8991
9136
|
if (!channelId) return;
|
|
@@ -9028,7 +9173,7 @@ var ChannelsMode = class {
|
|
|
9028
9173
|
return;
|
|
9029
9174
|
}
|
|
9030
9175
|
this.renderScrim(`
|
|
9031
|
-
<h3 id="slm-ch-dlg-title">Create a
|
|
9176
|
+
<h3 id="slm-ch-dlg-title">Create a buyer link for ${esc(channel.name)}</h3>
|
|
9032
9177
|
<p class="sub">Anyone who opens the link can buy from this channel's allocation \u2014 and only from it.
|
|
9033
9178
|
You'll see the link once, right after you create it.</p>
|
|
9034
9179
|
<div class="slm-field">
|
|
@@ -9153,7 +9298,7 @@ var ChannelsMode = class {
|
|
|
9153
9298
|
copy?.addEventListener("click", () => {
|
|
9154
9299
|
const ok = () => {
|
|
9155
9300
|
copy.textContent = "Copied";
|
|
9156
|
-
this.announce("
|
|
9301
|
+
this.announce("Buyer link copied.");
|
|
9157
9302
|
};
|
|
9158
9303
|
const clipboard = typeof navigator === "undefined" ? null : navigator.clipboard;
|
|
9159
9304
|
if (clipboard?.writeText) {
|
|
@@ -9163,7 +9308,7 @@ var ChannelsMode = class {
|
|
|
9163
9308
|
selectSecret(dialog);
|
|
9164
9309
|
});
|
|
9165
9310
|
});
|
|
9166
|
-
this.announce("Your
|
|
9311
|
+
this.announce("Your buyer link is ready and is shown once.");
|
|
9167
9312
|
}
|
|
9168
9313
|
/**
|
|
9169
9314
|
* Rotate. The organizer must SAY what happens to the buyers already inside —
|
|
@@ -9182,7 +9327,7 @@ var ChannelsMode = class {
|
|
|
9182
9327
|
<span><b>${sessions.toLocaleString()} buyer${sessions === 1 ? "" : "s"}</b> got in with the current link
|
|
9183
9328
|
and still ${sessions === 1 ? "has" : "have"} active access.</span></div>` : "";
|
|
9184
9329
|
this.renderScrim(`
|
|
9185
|
-
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "
|
|
9330
|
+
<h3 id="slm-ch-dlg-title">Rotate the ${esc(link.label || "buyer")} link?</h3>
|
|
9186
9331
|
<p class="sub">The current link stops opening immediately and cannot be restored. You will get a new
|
|
9187
9332
|
link to copy \u2014 shown once.</p>
|
|
9188
9333
|
${warning}
|
|
@@ -9241,7 +9386,7 @@ var ChannelsMode = class {
|
|
|
9241
9386
|
}
|
|
9242
9387
|
const sessions = link.activeSessions ?? 0;
|
|
9243
9388
|
this.renderScrim(`
|
|
9244
|
-
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "
|
|
9389
|
+
<h3 id="slm-ch-dlg-title">Revoke the ${esc(link.label || "buyer")} link?</h3>
|
|
9245
9390
|
<p class="sub">It stops opening immediately and cannot be restored \u2014 there is no undo, and no way to
|
|
9246
9391
|
bring the same URL back. Seats already bought through it keep their sale.</p>
|
|
9247
9392
|
${sessions ? `<label class="slm-ch-radio">
|
|
@@ -9358,7 +9503,12 @@ var LEGEND = [
|
|
|
9358
9503
|
var MANAGER_CSS = (
|
|
9359
9504
|
/* @sl-css */
|
|
9360
9505
|
`
|
|
9361
|
-
|
|
9506
|
+
/* The floor was 480px, which trapped the cockpit in any host shorter than that:
|
|
9507
|
+
every ancestor here is overflow:hidden, so the bottom of the rail (Create
|
|
9508
|
+
channel, Show archived) was rendered but clipped away by the host and could
|
|
9509
|
+
not be reached by scrolling anything. 320px still keeps an unsized host from
|
|
9510
|
+
collapsing to the top bar, and the rail scrolls to its full content below. */
|
|
9511
|
+
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:320px;overflow:hidden;
|
|
9362
9512
|
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
9363
9513
|
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
9364
9514
|
borrowed from CHANNELS_CSS. The base cockpit animates whether or not
|
|
@@ -9426,7 +9576,7 @@ var MANAGER_CSS = (
|
|
|
9426
9576
|
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
9427
9577
|
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
9428
9578
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
9429
|
-
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
9579
|
+
.slm-railscroll{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:16px}
|
|
9430
9580
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
9431
9581
|
.slm-hint{font-size:12.5px;color:var(--slm-muted);line-height:1.5;margin-bottom:14px}
|
|
9432
9582
|
|
|
@@ -9491,7 +9641,8 @@ var MANAGER_CSS = (
|
|
|
9491
9641
|
color:var(--slm-muted);font-size:10.5px}
|
|
9492
9642
|
.slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
|
|
9493
9643
|
.slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
|
|
9494
|
-
.slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);
|
|
9644
|
+
.slm-blockedlist{max-height:246px;overflow:auto;overscroll-behavior:contain;border:1px solid var(--slm-line);
|
|
9645
|
+
border-radius:10px;background:var(--slm-surface)}
|
|
9495
9646
|
.slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
|
|
9496
9647
|
border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
|
|
9497
9648
|
.slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
|