@seatlayer/js 0.43.2 → 0.44.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 +219 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -4
- package/dist/index.d.ts +52 -4
- package/dist/index.js +219 -42
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -7384,7 +7384,23 @@ var CHANNELS_CSS = (
|
|
|
7384
7384
|
.slm-ch-counts b.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
7385
7385
|
@keyframes slm-ch-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08)}}
|
|
7386
7386
|
.slm-ch-access{margin-top:6px;font-size:10.5px;color:var(--slm-muted)}
|
|
7387
|
-
|
|
7387
|
+
/* A row that opens its channel must READ as pressable, and be one for a keyboard
|
|
7388
|
+
too. It is a role="button" div rather than a <button> because the \u22EF control
|
|
7389
|
+
lives inside it, and a button inside a button is not valid HTML. */
|
|
7390
|
+
.slm-ch-row.open{cursor:pointer}
|
|
7391
|
+
.slm-ch-row.open:hover{border-color:var(--slm-accent);background:color-mix(in srgb,var(--slm-accent) 6%,var(--slm-surface))}
|
|
7392
|
+
.slm-ch-row.open:focus-visible{outline:2px solid var(--slm-accent);outline-offset:2px}
|
|
7393
|
+
.slm-ch-row.open:active{border-color:var(--slm-accent)}
|
|
7394
|
+
.slm-ch-more{flex:none;color:var(--slm-muted);font-weight:800;padding:0 4px;min-height:28px;border-radius:6px}
|
|
7395
|
+
.slm-ch-more:hover{color:var(--slm-text)}
|
|
7396
|
+
.slm-ch-more:focus-visible{outline:2px solid var(--slm-accent);outline-offset:1px;color:var(--slm-text)}
|
|
7397
|
+
.slm-ch-menu{display:flex;flex-direction:column;gap:8px}
|
|
7398
|
+
.slm-ch-menu .slm-btn{width:100%}
|
|
7399
|
+
/* loading is a state, never a flash of empty: same slot, visibly working */
|
|
7400
|
+
.slm-ch-busy{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
7401
|
+
.slm-ch-busy::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--slm-accent);flex:none;
|
|
7402
|
+
animation:slm-ch-pulse var(--slm-mo-ambient) var(--slm-mo-in-out) infinite}
|
|
7403
|
+
@keyframes slm-ch-pulse{0%,100%{opacity:.25;transform:scale(.7)}50%{opacity:1;transform:scale(1)}}
|
|
7388
7404
|
.slm-ch-selsrc{display:flex;flex-direction:column;gap:5px;margin:8px 0 12px}
|
|
7389
7405
|
.slm-ch-selsrc-row{display:flex;align-items:center;gap:8px;font-size:12px;font-variant-numeric:tabular-nums}
|
|
7390
7406
|
.slm-ch-selsrc-row .mk{width:15px;height:15px;border-radius:4px;display:grid;place-items:center;font-size:8px;
|
|
@@ -7516,6 +7532,7 @@ var CHANNELS_CSS = (
|
|
|
7516
7532
|
.slm-ch-meter i,.slm-ch-radio{transition:none!important}
|
|
7517
7533
|
.slm-ch-staged.shake,.slm-ch-tick,.slm-ch-bucket,.slm-ch-scrim,.slm-ch-dialog,
|
|
7518
7534
|
.slm-ch-counts b.bump,.slm-ch-selnum.bump{animation:none!important}
|
|
7535
|
+
.slm-ch-busy::before{animation:none!important;opacity:1}
|
|
7519
7536
|
.slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
|
|
7520
7537
|
.slm-ch-staged.done{outline:2px solid #5bd39b;outline-offset:2px}
|
|
7521
7538
|
}
|
|
@@ -7576,6 +7593,18 @@ var ChannelsMode = class {
|
|
|
7576
7593
|
this.focusedSectionId = null;
|
|
7577
7594
|
this.showArchived = false;
|
|
7578
7595
|
this.detailChannelId = null;
|
|
7596
|
+
/**
|
|
7597
|
+
* Whether the organizer has asked to assign seats.
|
|
7598
|
+
*
|
|
7599
|
+
* The list rail leads with the CHANNELS. The assignment tooling — destination
|
|
7600
|
+
* picker plus five select-by routes — used to paint unconditionally above that
|
|
7601
|
+
* list, so a first-time organizer met a workbench for a job they had not asked
|
|
7602
|
+
* to do, with the thing they came for pushed below the fold. It is now
|
|
7603
|
+
* disclosed on intent: the "Assign seats to a channel" action under the list,
|
|
7604
|
+
* the map's own "Assign seats" segment, or simply having a selection. The
|
|
7605
|
+
* selection rail always shows the tools, because there the intent is proven.
|
|
7606
|
+
*/
|
|
7607
|
+
this.assignOpen = false;
|
|
7579
7608
|
this.targetChannelId = "";
|
|
7580
7609
|
this.conflict = false;
|
|
7581
7610
|
this.dialog = null;
|
|
@@ -7602,8 +7631,16 @@ var ChannelsMode = class {
|
|
|
7602
7631
|
this.previewAudience = [];
|
|
7603
7632
|
this.previewIncludePublic = false;
|
|
7604
7633
|
this.previewProjection = null;
|
|
7605
|
-
|
|
7606
|
-
|
|
7634
|
+
/**
|
|
7635
|
+
* The buyer-preview read, as one honest state rather than a boolean.
|
|
7636
|
+
*
|
|
7637
|
+
* `previewSupported: boolean | null` could say "this worker has no projection
|
|
7638
|
+
* route", but it could not say "the read is in flight" or "the read failed" —
|
|
7639
|
+
* both of those painted an audience picker with no result underneath, which
|
|
7640
|
+
* reads as "this audience can buy nothing". Loading and error are now states
|
|
7641
|
+
* of their own, and the error one offers a retry.
|
|
7642
|
+
*/
|
|
7643
|
+
this.previewState = "idle";
|
|
7607
7644
|
this.pollTimer = null;
|
|
7608
7645
|
this.layer = null;
|
|
7609
7646
|
this.canvas = null;
|
|
@@ -7641,6 +7678,7 @@ var ChannelsMode = class {
|
|
|
7641
7678
|
if (this.active) return;
|
|
7642
7679
|
this.active = true;
|
|
7643
7680
|
this.mapIntent = "pan";
|
|
7681
|
+
this.assignOpen = false;
|
|
7644
7682
|
this.focusedSectionId = null;
|
|
7645
7683
|
this.assignmentRowsCache = null;
|
|
7646
7684
|
this.railHtml = null;
|
|
@@ -8150,6 +8188,7 @@ var ChannelsMode = class {
|
|
|
8150
8188
|
this.view = view;
|
|
8151
8189
|
if (view === "inspect") {
|
|
8152
8190
|
this.previewProjection = null;
|
|
8191
|
+
this.previewState = "idle";
|
|
8153
8192
|
this.setBanner(false);
|
|
8154
8193
|
} else {
|
|
8155
8194
|
if (!this.previewAudience.length) {
|
|
@@ -8167,6 +8206,9 @@ var ChannelsMode = class {
|
|
|
8167
8206
|
const audience = [...this.previewAudience];
|
|
8168
8207
|
const names = audience.map((id) => this.nameOf(id) ?? PUBLIC_CHANNEL_NAME).join(" + ");
|
|
8169
8208
|
this.setBanner(true, names);
|
|
8209
|
+
this.previewProjection = null;
|
|
8210
|
+
this.previewState = "loading";
|
|
8211
|
+
if (this.active) this.paintRail();
|
|
8170
8212
|
try {
|
|
8171
8213
|
this.previewProjection = await this.host.api.channelPreview(this.host.eventKey, audience, {
|
|
8172
8214
|
// Naming Public sale as the audience IS asking for public inventory; the
|
|
@@ -8174,14 +8216,14 @@ var ChannelsMode = class {
|
|
|
8174
8216
|
// this the request would resolve to an empty scope and 422.
|
|
8175
8217
|
includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
|
|
8176
8218
|
});
|
|
8177
|
-
this.
|
|
8219
|
+
this.previewState = "ready";
|
|
8178
8220
|
const eligibleSeats = this.previewProjection.available === false ? void 0 : this.previewProjection.counts?.eligible ?? this.previewProjection.eligible?.length;
|
|
8179
8221
|
this.setBanner(true, names, eligibleSeats);
|
|
8180
8222
|
} catch (err) {
|
|
8181
8223
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
8182
|
-
this.
|
|
8224
|
+
this.previewState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
|
|
8183
8225
|
this.previewProjection = null;
|
|
8184
|
-
if (this.
|
|
8226
|
+
if (this.previewState === "error") this.host.onError(err);
|
|
8185
8227
|
}
|
|
8186
8228
|
if (this.active) {
|
|
8187
8229
|
this.paintRail();
|
|
@@ -8222,13 +8264,14 @@ var ChannelsMode = class {
|
|
|
8222
8264
|
}
|
|
8223
8265
|
if (this.loading && !this.list) {
|
|
8224
8266
|
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8225
|
-
<div class="slm-
|
|
8267
|
+
<div class="slm-ch-busy" role="status" data-ch-state="list-loading">Loading channels and allocations\u2026</div>`);
|
|
8226
8268
|
return;
|
|
8227
8269
|
}
|
|
8228
8270
|
if (!this.list && this.loadError) {
|
|
8229
8271
|
if (this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
8230
|
-
<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8231
|
-
<span><b>Couldn't load sales channels.</b>
|
|
8272
|
+
<div class="slm-ch-alert err" role="alert" data-ch-state="list-error"><span>\u26A0</span>
|
|
8273
|
+
<span><b>Couldn't load sales channels.</b> This event may well have channels \u2014 we could not read them.
|
|
8274
|
+
Everything else on this event still works.
|
|
8232
8275
|
<button type="button" data-ch-act="retry">Try again</button></span></div>`)) {
|
|
8233
8276
|
rail.querySelector('[data-ch-act="retry"]')?.addEventListener("click", () => {
|
|
8234
8277
|
void this.refresh();
|
|
@@ -8287,10 +8330,13 @@ var ChannelsMode = class {
|
|
|
8287
8330
|
const marker = this.markerFor(channel.id);
|
|
8288
8331
|
const badgeKind = opts.builtin ? "builtin" : channel.state;
|
|
8289
8332
|
const dim = channel.state === "paused" || channel.state === "archived" ? " dim" : "";
|
|
8290
|
-
const
|
|
8291
|
-
const
|
|
8292
|
-
|
|
8293
|
-
|
|
8333
|
+
const opens = !opts.builtin && this.caps.manage && this.detailChannelId !== channel.id;
|
|
8334
|
+
const cls = `slm-ch-row${opts.builtin ? " public" : ""}${channel.state === "archived" ? " archived" : ""}${this.detailChannelId === channel.id ? " on" : ""}${opens ? " open" : ""}`;
|
|
8335
|
+
const more = !opts.builtin && this.caps.manage ? `<button type="button" class="slm-ch-more" data-ch-menu="${esc(channel.id)}"
|
|
8336
|
+
aria-label="More actions for ${esc(channel.name)}" aria-haspopup="dialog">\u22EF</button>` : "";
|
|
8337
|
+
const rowAttrs = opens ? ` role="button" tabindex="0" data-ch-open="${esc(channel.id)}"
|
|
8338
|
+
aria-label="Open ${esc(channel.name)}"` : "";
|
|
8339
|
+
return `<div class="${cls}"${rowAttrs}>
|
|
8294
8340
|
<span class="slm-ch-head">
|
|
8295
8341
|
<span class="slm-ch-mk${dim}" style="background:${esc(marker.color)}" aria-hidden="true">${esc(marker.letter)}</span>
|
|
8296
8342
|
<span class="slm-ch-name">${esc(channel.name)}</span>
|
|
@@ -8304,41 +8350,68 @@ var ChannelsMode = class {
|
|
|
8304
8350
|
listRailHtml() {
|
|
8305
8351
|
const list = this.list;
|
|
8306
8352
|
const archivedCount = list.channels.filter((channel) => channel.state === "archived").length;
|
|
8353
|
+
const visible = list.channels.filter((channel) => this.showArchived || channel.state !== "archived");
|
|
8307
8354
|
const rows = [
|
|
8308
8355
|
this.channelRowHtml(list.publicSale, { builtin: true }),
|
|
8309
|
-
...
|
|
8356
|
+
...visible.map((channel, index) => this.channelRowHtml(channel, { index }))
|
|
8310
8357
|
].join("");
|
|
8358
|
+
const empty = visible.length ? "" : `<p class="slm-note" data-ch-state="list-empty">${archivedCount && !this.showArchived ? `No open channels \u2014 every seat is on public sale. ${archivedCount.toLocaleString()} archived channel${archivedCount === 1 ? " is" : "s are"} hidden below.` : "No private channels yet \u2014 every seat is on public sale."}</p>`;
|
|
8311
8359
|
const create = this.caps.manage ? `<button type="button" class="slm-btn ghost" style="width:100%" data-ch-act="create">+ Create channel</button>` : "";
|
|
8312
8360
|
const readOnly = this.caps.manage ? "" : `<p class="slm-note">You can see how inventory is allocated. Changing it needs channel-management permission.</p>`;
|
|
8313
8361
|
return `
|
|
8314
|
-
${this.caps.manage ? this.assignmentToolsHtml() : ""}
|
|
8315
8362
|
<p class="slm-eyebrow">Sales channels</p>
|
|
8316
8363
|
<p class="slm-hint">Channel colours and names are only visible to organizers, never to buyers.</p>
|
|
8317
8364
|
<div class="slm-ch-list">${rows}</div>
|
|
8365
|
+
${empty}
|
|
8318
8366
|
${create}
|
|
8319
8367
|
${readOnly}
|
|
8368
|
+
${this.assignEntryHtml()}
|
|
8320
8369
|
<p class="slm-note" style="margin-top:10px">
|
|
8321
8370
|
<button type="button" class="slm-linkbtn" data-ch-act="toggle-archived" aria-pressed="${this.showArchived}"
|
|
8322
8371
|
style="text-align:left">${this.showArchived ? "Hide" : "Show"} archived${archivedCount ? ` (${archivedCount})` : ""}</button>
|
|
8323
8372
|
</p>`;
|
|
8324
8373
|
}
|
|
8374
|
+
/**
|
|
8375
|
+
* The assignment entry point on the list rail.
|
|
8376
|
+
*
|
|
8377
|
+
* Collapsed it is ONE plain-language action, so the channels the organizer came
|
|
8378
|
+
* for stay at the top of the panel. Opened it is the same tool set that has
|
|
8379
|
+
* always worked, in the same order, plus the way back out — and opening it is
|
|
8380
|
+
* also what the map's "Assign seats" segment does, so the two routes into the
|
|
8381
|
+
* job cannot disagree about whether it is running.
|
|
8382
|
+
*/
|
|
8383
|
+
assignEntryHtml() {
|
|
8384
|
+
if (!this.caps.manage) return "";
|
|
8385
|
+
if (!this.assignOpen) {
|
|
8386
|
+
return `
|
|
8387
|
+
<button type="button" class="slm-btn ghost" style="width:100%;margin-top:8px"
|
|
8388
|
+
data-ch-act="assign-open" aria-expanded="false">Assign seats to a channel</button>
|
|
8389
|
+
<p class="slm-note">Move whole sections, rows, a dragged area or single seats out of public sale
|
|
8390
|
+
and into a channel. Nothing moves until you review and apply.</p>`;
|
|
8391
|
+
}
|
|
8392
|
+
return `<div style="margin-top:8px">${this.assignmentToolsHtml({ collapsible: true })}</div>`;
|
|
8393
|
+
}
|
|
8325
8394
|
/**
|
|
8326
8395
|
* Destination-first assignment controls.
|
|
8327
8396
|
*
|
|
8328
8397
|
* These used to live only inside the selection rail, which meant every route
|
|
8329
8398
|
* into them was gated behind "select a seat on the map first" — the section,
|
|
8330
8399
|
* row and category choosers were invisible until the organizer had already
|
|
8331
|
-
* done the work by hand. They
|
|
8332
|
-
*
|
|
8333
|
-
*
|
|
8400
|
+
* done the work by hand. They are still reachable before a seat is selected,
|
|
8401
|
+
* but they are no longer the first thing in the panel: on the list rail they
|
|
8402
|
+
* are disclosed by `assignEntryHtml`, and there they carry a way back out
|
|
8403
|
+
* (`collapsible`). In the selection rail there is nothing to disclose — a
|
|
8404
|
+
* selection IS the intent — so they paint unconditionally and without Done.
|
|
8334
8405
|
*/
|
|
8335
|
-
assignmentToolsHtml() {
|
|
8406
|
+
assignmentToolsHtml(opts = {}) {
|
|
8336
8407
|
const options = this.assignableChannels().map((channel) => `<option value="${esc(channel.id)}"${channel.id === this.targetChannelId ? " selected" : ""}>${esc(channel.name)}</option>`).join("");
|
|
8337
8408
|
const sections = this.host.sections().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-sections">Sections</button>' : "";
|
|
8338
8409
|
const rows = this.assignmentRows().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-rows">Rows</button>' : "";
|
|
8339
8410
|
const dragClass = this.mapIntent === "assign" ? "slm-btn" : "slm-btn ghost";
|
|
8411
|
+
const done = opts.collapsible ? `<button type="button" class="slm-linkbtn" data-ch-act="assign-close"
|
|
8412
|
+
aria-expanded="true" style="float:right;font-weight:800">Done</button>` : "";
|
|
8340
8413
|
return `
|
|
8341
|
-
<p class="slm-eyebrow"
|
|
8414
|
+
<p class="slm-eyebrow">${done}Assign inventory</p>
|
|
8342
8415
|
<div class="slm-field">
|
|
8343
8416
|
<label for="slm-ch-target">Assign to</label>
|
|
8344
8417
|
<select class="slm-select" id="slm-ch-target" data-ch-target>${options}</select>
|
|
@@ -8388,8 +8461,16 @@ var ChannelsMode = class {
|
|
|
8388
8461
|
<p class="slm-note">The seat list offers the same selection with checkboxes for keyboard and screen-reader use.</p>`;
|
|
8389
8462
|
}
|
|
8390
8463
|
detailRailHtml(channelId) {
|
|
8464
|
+
const back = `<p class="slm-eyebrow">
|
|
8465
|
+
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
8466
|
+
</p>`;
|
|
8391
8467
|
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
8392
|
-
if (!channel)
|
|
8468
|
+
if (!channel) {
|
|
8469
|
+
return `${back}
|
|
8470
|
+
<div class="slm-ch-alert warn" role="status" data-ch-state="detail-gone"><span>\u2139</span>
|
|
8471
|
+
<span><b>This channel is no longer on this event.</b> It was archived or removed while you had it open.
|
|
8472
|
+
${this.showArchived ? "" : "Archived channels are hidden \u2014 use \u201CShow archived\u201D on the list to see it."}</span></div>`;
|
|
8473
|
+
}
|
|
8393
8474
|
const lifecycle = this.caps.manage ? `
|
|
8394
8475
|
<p class="slm-eyebrow" style="margin-top:18px">Lifecycle</p>
|
|
8395
8476
|
<div class="slm-ch-row2" style="margin-top:2px">
|
|
@@ -8400,9 +8481,7 @@ var ChannelsMode = class {
|
|
|
8400
8481
|
<p class="slm-note">Archive returns the allocation to a destination you choose. Nothing is ever deleted silently.</p>` : "";
|
|
8401
8482
|
const access = this.caps.manage ? this.distributeHtml(channel) : "";
|
|
8402
8483
|
return `
|
|
8403
|
-
|
|
8404
|
-
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
8405
|
-
</p>
|
|
8484
|
+
${back}
|
|
8406
8485
|
<p class="slm-eyebrow">Channel \xB7 ${esc(channel.name)}</p>
|
|
8407
8486
|
<div class="slm-ch-list">${this.channelRowHtml(channel)}</div>
|
|
8408
8487
|
${access}
|
|
@@ -8499,16 +8578,17 @@ var ChannelsMode = class {
|
|
|
8499
8578
|
hostedLinksHtml() {
|
|
8500
8579
|
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Buyer links</p>`;
|
|
8501
8580
|
if (this.linksState === "unsupported") {
|
|
8502
|
-
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
8581
|
+
return `${eyebrow}<div class="slm-ch-alert warn" data-ch-state="links-unsupported"><span>\u2139</span>
|
|
8503
8582
|
<span><b>Buyer links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
8504
8583
|
}
|
|
8505
8584
|
if (this.linksState === "error") {
|
|
8506
|
-
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8507
|
-
<span><b>Couldn't load this channel's links.</b>
|
|
8585
|
+
return `${eyebrow}<div class="slm-ch-alert err" role="alert" data-ch-state="links-error"><span>\u26A0</span>
|
|
8586
|
+
<span><b>Couldn't load this channel's links.</b> This is a failed read, not an empty list \u2014
|
|
8587
|
+
any live link is still working.
|
|
8508
8588
|
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
8509
8589
|
}
|
|
8510
|
-
if (this.linksState === "loading" && !this.links.length) {
|
|
8511
|
-
return `${eyebrow}<div class="slm-
|
|
8590
|
+
if (this.linksState === "idle" || this.linksState === "loading" && !this.links.length) {
|
|
8591
|
+
return `${eyebrow}<div class="slm-ch-busy" role="status" data-ch-state="links-loading">Loading buyer links\u2026</div>`;
|
|
8512
8592
|
}
|
|
8513
8593
|
if (!this.links.length) return "";
|
|
8514
8594
|
return `${eyebrow}
|
|
@@ -8550,9 +8630,14 @@ var ChannelsMode = class {
|
|
|
8550
8630
|
];
|
|
8551
8631
|
const current = this.previewAudience[0] ?? PUBLIC_CHANNEL_ID;
|
|
8552
8632
|
const options = audienceOptions.map((entry) => `<option value="${esc(entry.id)}"${entry.id === current ? " selected" : ""}>${esc(entry.name)}</option>`).join("");
|
|
8553
|
-
const unsupported = this.
|
|
8633
|
+
const unsupported = this.previewState === "unsupported" ? `<div class="slm-ch-alert warn" data-ch-state="preview-unsupported"><span>\u2139</span>
|
|
8554
8634
|
<span><b>Preview needs a newer server.</b> Allocation management works normally;
|
|
8555
8635
|
the buyer-view simulation will appear once this event's API is updated.</span></div>` : "";
|
|
8636
|
+
const busy = this.previewState === "loading" ? `<div class="slm-ch-busy" role="status" data-ch-state="preview-loading">Asking the server what this audience sees\u2026</div>` : "";
|
|
8637
|
+
const failed = this.previewState === "error" ? `<div class="slm-ch-alert err" role="alert" data-ch-state="preview-error"><span>\u26A0</span>
|
|
8638
|
+
<span><b>Couldn't load the buyer preview.</b> Nothing is wrong with this audience's seats \u2014
|
|
8639
|
+
the simulation itself failed to load.
|
|
8640
|
+
<button type="button" data-ch-act="preview-retry">Try again</button></span></div>` : "";
|
|
8556
8641
|
const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
|
|
8557
8642
|
<span><b>This private sale is not available.</b> ${esc((this.previewProjection.unavailable ?? []).map((entry) => `${this.nameOf(entry.channelId) ?? "This channel"} is ${entry.state}`).join("; ") || "The audience cannot buy right now")}.
|
|
8558
8643
|
A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
|
|
@@ -8573,7 +8658,7 @@ var ChannelsMode = class {
|
|
|
8573
8658
|
${includePublic}
|
|
8574
8659
|
<p class="slm-hint">This is the same projection the buyer SDK receives for this audience \u2014 not a local
|
|
8575
8660
|
approximation. It is read-only: clicks open seat details, and no holds are created.</p>
|
|
8576
|
-
${unsupported}${unavailable}
|
|
8661
|
+
${busy}${failed}${unsupported}${unavailable}
|
|
8577
8662
|
<div class="slm-ch-legend">
|
|
8578
8663
|
<div class="r"><span class="sw" style="background:#6e7bff"></span> Eligible & free \u2014 buyable by this audience</div>
|
|
8579
8664
|
<div class="r"><span class="sw" style="background:#3a4051"></span> Unavailable to this audience (one neutral state)</div>
|
|
@@ -8582,6 +8667,7 @@ var ChannelsMode = class {
|
|
|
8582
8667
|
${summary}`;
|
|
8583
8668
|
}
|
|
8584
8669
|
paintSelection() {
|
|
8670
|
+
if (this.caps.manage && this.host.selectionLabels().length) this.assignOpen = true;
|
|
8585
8671
|
if (this.view === "inspect" && !this.detailChannelId) this.paintRail();
|
|
8586
8672
|
}
|
|
8587
8673
|
wireRail() {
|
|
@@ -8592,16 +8678,27 @@ var ChannelsMode = class {
|
|
|
8592
8678
|
rail.querySelectorAll("[data-ch-map]").forEach((button) => {
|
|
8593
8679
|
button.addEventListener("click", () => {
|
|
8594
8680
|
this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
|
|
8681
|
+
if (this.mapIntent === "assign") this.assignOpen = true;
|
|
8595
8682
|
this.paintRail();
|
|
8596
8683
|
this.onInteractionChange?.();
|
|
8597
8684
|
});
|
|
8598
8685
|
});
|
|
8599
|
-
rail.querySelectorAll("[data-ch-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8686
|
+
rail.querySelectorAll("[data-ch-open]").forEach((row) => {
|
|
8687
|
+
const open = () => this.openChannel(row.dataset.chOpen);
|
|
8688
|
+
row.addEventListener("click", open);
|
|
8689
|
+
row.addEventListener("keydown", (event) => {
|
|
8690
|
+
if (event.key !== "Enter" && event.key !== " " && event.key !== "Spacebar") return;
|
|
8691
|
+
event.preventDefault();
|
|
8692
|
+
open();
|
|
8693
|
+
});
|
|
8694
|
+
});
|
|
8695
|
+
rail.querySelectorAll("[data-ch-menu]").forEach((button) => {
|
|
8696
|
+
button.addEventListener("click", (event) => {
|
|
8697
|
+
event.stopPropagation();
|
|
8698
|
+
this.openDialog({ kind: "menu", channelId: button.dataset.chMenu });
|
|
8699
|
+
});
|
|
8700
|
+
button.addEventListener("keydown", (event) => {
|
|
8701
|
+
event.stopPropagation();
|
|
8605
8702
|
});
|
|
8606
8703
|
});
|
|
8607
8704
|
rail.querySelectorAll("[data-ch-rotate]").forEach((button) => {
|
|
@@ -8640,6 +8737,12 @@ var ChannelsMode = class {
|
|
|
8640
8737
|
button.addEventListener("click", () => this.railAction(button.dataset.chAct));
|
|
8641
8738
|
});
|
|
8642
8739
|
}
|
|
8740
|
+
/** Open a channel's detail panel and start its buyer-link read. */
|
|
8741
|
+
openChannel(channelId) {
|
|
8742
|
+
this.detailChannelId = channelId;
|
|
8743
|
+
this.paintRail();
|
|
8744
|
+
void this.loadLinks(channelId).then(() => this.paintRail());
|
|
8745
|
+
}
|
|
8643
8746
|
railAction(action) {
|
|
8644
8747
|
switch (action) {
|
|
8645
8748
|
case "sections":
|
|
@@ -8713,6 +8816,22 @@ var ChannelsMode = class {
|
|
|
8713
8816
|
case "pick-category":
|
|
8714
8817
|
this.pickCategory();
|
|
8715
8818
|
break;
|
|
8819
|
+
case "assign-open":
|
|
8820
|
+
this.assignOpen = true;
|
|
8821
|
+
this.paintRail();
|
|
8822
|
+
break;
|
|
8823
|
+
// Collapsing the tools must also disarm the marquee. Leaving it armed
|
|
8824
|
+
// would hide a mode the map is still in — the organizer would drag to pan
|
|
8825
|
+
// and select seats instead, with no control on screen saying why.
|
|
8826
|
+
case "assign-close":
|
|
8827
|
+
this.assignOpen = false;
|
|
8828
|
+
this.mapIntent = "pan";
|
|
8829
|
+
this.paintRail();
|
|
8830
|
+
this.onInteractionChange?.();
|
|
8831
|
+
break;
|
|
8832
|
+
case "preview-retry":
|
|
8833
|
+
void this.loadPreview();
|
|
8834
|
+
break;
|
|
8716
8835
|
default:
|
|
8717
8836
|
break;
|
|
8718
8837
|
}
|
|
@@ -8862,7 +8981,8 @@ var ChannelsMode = class {
|
|
|
8862
8981
|
blocks.push(`<p class="slm-ch-scopehint">${(matches.length - visible.length).toLocaleString()} more row${matches.length - visible.length === 1 ? "" : "s"}. Search to narrow the list.</p>`);
|
|
8863
8982
|
}
|
|
8864
8983
|
});
|
|
8865
|
-
list.innerHTML = blocks.join("") || `<div class="slm-ch-scope-empty">No sections or rows match \u201C${esc(query)}\u201D.</div
|
|
8984
|
+
list.innerHTML = blocks.join("") || (query ? `<div class="slm-ch-scope-empty" data-ch-state="scope-nomatch">No sections or rows match \u201C${esc(query)}\u201D.</div>` : `<div class="slm-ch-scope-empty" data-ch-state="scope-empty">This chart has no rows with selectable seats.
|
|
8985
|
+
Use Drag box or the seat list instead.</div>`);
|
|
8866
8986
|
filterHint.textContent = query ? `Showing ${rendered.toLocaleString()} of ${totalMatches.toLocaleString()} matching rows. Section checkboxes still select every row in that section.` : "Showing section groups. Expand one or search to see rows.";
|
|
8867
8987
|
list.querySelectorAll("[data-ch-scope-group]").forEach((button) => button.addEventListener("click", () => {
|
|
8868
8988
|
const items = groups[Number(button.dataset.chScopeGroup)]?.[1] ?? [];
|
|
@@ -8912,7 +9032,8 @@ var ChannelsMode = class {
|
|
|
8912
9032
|
this.renderScrim(`
|
|
8913
9033
|
<h3 id="slm-ch-dlg-title">Add whole sections</h3>
|
|
8914
9034
|
<p class="sub">Choose one or more. They are added to the seats already selected on the map.</p>
|
|
8915
|
-
<div class="slm-ch-seatlist">${body || `<div class="slm-empty">
|
|
9035
|
+
<div class="slm-ch-seatlist">${body || `<div class="slm-ch-scope-empty" data-ch-state="scope-empty">This chart has no sections with selectable seats.
|
|
9036
|
+
Use Drag box or the seat list instead.</div>`}</div>
|
|
8916
9037
|
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8917
9038
|
<div class="foot">
|
|
8918
9039
|
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
@@ -8969,6 +9090,7 @@ var ChannelsMode = class {
|
|
|
8969
9090
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
8970
9091
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
8971
9092
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
9093
|
+
else if (state.kind === "menu") this.renderMenuDialog(state);
|
|
8972
9094
|
else if (state.kind === "linkCreate") this.renderLinkCreateDialog(state);
|
|
8973
9095
|
else if (state.kind === "linkRotate") this.renderLinkRotateDialog(state);
|
|
8974
9096
|
else if (state.kind === "linkRevoke") this.renderLinkRevokeDialog(state);
|
|
@@ -9224,6 +9346,59 @@ var ChannelsMode = class {
|
|
|
9224
9346
|
void bar.offsetWidth;
|
|
9225
9347
|
bar.classList.add("shake");
|
|
9226
9348
|
}
|
|
9349
|
+
/**
|
|
9350
|
+
* The ⋯ menu.
|
|
9351
|
+
*
|
|
9352
|
+
* Opening a channel is the ROW's job now, so ⋯ carries what is left: the
|
|
9353
|
+
* secondary and destructive lifecycle actions. It is rendered through the same
|
|
9354
|
+
* scrim primitive as every other sheet, which is what gives it a focus trap,
|
|
9355
|
+
* Escape, and a name — a bare absolutely-positioned popup would have had none
|
|
9356
|
+
* of those. Archive keeps its own confirmation dialog; this menu never
|
|
9357
|
+
* destroys anything by itself.
|
|
9358
|
+
*/
|
|
9359
|
+
renderMenuDialog(state) {
|
|
9360
|
+
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
9361
|
+
if (!channel || !this.caps.manage) {
|
|
9362
|
+
this.closeDialog();
|
|
9363
|
+
return;
|
|
9364
|
+
}
|
|
9365
|
+
const paused = channel.state === "paused";
|
|
9366
|
+
this.renderScrim(`
|
|
9367
|
+
<h3 id="slm-ch-dlg-title">${esc(channel.name)}</h3>
|
|
9368
|
+
<p class="sub">Open the channel to allocate seats and hand out buyer access.
|
|
9369
|
+
These are the rest of its actions.</p>
|
|
9370
|
+
<div class="slm-ch-menu">
|
|
9371
|
+
<button type="button" class="slm-btn" data-ch-menu-act="open">Open channel</button>
|
|
9372
|
+
<button type="button" class="slm-btn ghost" data-ch-menu-act="rename">Rename</button>
|
|
9373
|
+
<button type="button" class="slm-btn ghost" data-ch-menu-act="pause">${paused ? "Resume selling" : "Pause selling"}</button>
|
|
9374
|
+
<button type="button" class="slm-btn ghost" data-ch-menu-act="archive">Archive\u2026</button>
|
|
9375
|
+
</div>
|
|
9376
|
+
<p class="slm-note">Pausing stops new buyer access; checkouts already running can finish.
|
|
9377
|
+
Archiving closes the channel for good and returns its free seats to a destination you choose.
|
|
9378
|
+
Nothing is ever deleted silently.</p>
|
|
9379
|
+
<div class="foot"><button type="button" class="quiet" data-ch-close>Cancel</button></div>`, (dialog) => {
|
|
9380
|
+
dialog.querySelectorAll("[data-ch-menu-act]").forEach((button) => {
|
|
9381
|
+
button.addEventListener("click", () => {
|
|
9382
|
+
const act = button.dataset.chMenuAct;
|
|
9383
|
+
if (act === "open") {
|
|
9384
|
+
this.closeDialog();
|
|
9385
|
+
this.openChannel(channel.id);
|
|
9386
|
+
return;
|
|
9387
|
+
}
|
|
9388
|
+
if (act === "rename") {
|
|
9389
|
+
this.openDialog({ kind: "rename", channelId: channel.id });
|
|
9390
|
+
return;
|
|
9391
|
+
}
|
|
9392
|
+
if (act === "archive") {
|
|
9393
|
+
this.openDialog({ kind: "archive", channelId: channel.id });
|
|
9394
|
+
return;
|
|
9395
|
+
}
|
|
9396
|
+
this.closeDialog();
|
|
9397
|
+
void this.togglePause(channel.id);
|
|
9398
|
+
});
|
|
9399
|
+
});
|
|
9400
|
+
});
|
|
9401
|
+
}
|
|
9227
9402
|
renderRenameDialog(state) {
|
|
9228
9403
|
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
9229
9404
|
if (!channel) {
|
|
@@ -9284,9 +9459,11 @@ var ChannelsMode = class {
|
|
|
9284
9459
|
this.host.onError(err);
|
|
9285
9460
|
}
|
|
9286
9461
|
}
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9462
|
+
/** `channelId` is explicit because this is reachable from the ⋯ menu on a row
|
|
9463
|
+
* that is NOT the open channel, as well as from the detail panel itself. */
|
|
9464
|
+
async togglePause(channelId = this.detailChannelId) {
|
|
9465
|
+
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
9466
|
+
if (!channel || !this.caps.manage) return;
|
|
9290
9467
|
const paused = channel.state !== "paused";
|
|
9291
9468
|
try {
|
|
9292
9469
|
await this.host.api.setChannelPaused(this.host.eventKey, channel.id, paused);
|