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