@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.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
- startSession: (input) => this.pubApi.startCheckout(this.opts.event, input),
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 === "internal" ? "Internal selling \xB7 no buyer access needed" : access.intent === "server" ? "Server integration" : access.intent === "hosted_link" ? "Hosted access link" : "No buyer access configured";
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 = 1e4;
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
- /* hosted access links \u2014 STATUS only; there is no Copy control on this card */
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);border-radius:10px;
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">Server integration</p>
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 server integration guide \u2192</a></p>`;
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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
- await this.loadAllocation(seq);
7266
- if (superseded()) return;
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
- rail.innerHTML = `<p class="slm-eyebrow">Sales channels</p>
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
- rail.innerHTML = `<p class="slm-eyebrow">Sales channels</p>
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
- rail.innerHTML = `<p class="slm-eyebrow">Sales channels</p>
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
- rail.querySelector('[data-ch-act="retry"]')?.addEventListener("click", () => {
7704
- void this.refresh();
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
- rail.innerHTML = `${grab}${segment}${body}`;
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 intent = channel.access?.intent ?? "none";
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
- // ---- hosted access links --------------------------------------------------
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 hosted-link section of the detail panel.
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">Hosted access links</p>`;
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>Hosted links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
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
- const live = this.links.filter(accessLinkIsLive).length;
7918
- const cards = this.linksState === "loading" && !this.links.length ? `<div class="slm-empty">Loading links\u2026</div>` : this.links.length ? this.links.map((link) => this.linkCardHtml(link)).join("") : `<p class="slm-hint">No hosted link yet. Create one to send this allocation to a named group \u2014
7919
- they open the link and buy only these seats.</p>`;
7920
- const create = this.caps.manage ? `<button type="button" class="slm-btn" style="width:100%" data-ch-act="link-create">
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
- ${cards}
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 || "Hosted link")}</span>
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
- // ---- hosted-link dialogs --------------------------------------------------
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 hosted access link for ${esc(channel.name)}</h3>
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("Hosted access link copied.");
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 hosted access link is ready and is shown once.");
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 || "hosted")} link?</h3>
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 || "hosted")} link?</h3>
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
- .slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
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);border-radius:10px;background:var(--slm-surface)}
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}