@seatlayer/js 0.43.1 → 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 +222 -44
- 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 +222 -44
- 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
|
-
|
|
8315
|
-
<p class="slm-eyebrow"${this.caps.manage ? ' style="margin-top:18px"' : ""}>Sales channels</p>
|
|
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>
|
|
@@ -8352,7 +8425,8 @@ var ChannelsMode = class {
|
|
|
8352
8425
|
<button type="button" class="slm-btn ghost" data-ch-act="pick-category">Category</button>
|
|
8353
8426
|
<button type="button" class="slm-btn ghost" data-ch-act="seatlist">Seat list \u2328</button>
|
|
8354
8427
|
</div>
|
|
8355
|
-
<p class="slm-note">Choose a destination, then add whole sections,
|
|
8428
|
+
<p class="slm-note" style="margin-bottom:18px">Choose a destination, then add whole sections,
|
|
8429
|
+
multiple rows, a dragged area, or individual seats.</p>`;
|
|
8356
8430
|
}
|
|
8357
8431
|
selectionRailHtml(selection) {
|
|
8358
8432
|
const sources = selectionSources(selection, this.allocation, this.list);
|
|
@@ -8387,8 +8461,16 @@ var ChannelsMode = class {
|
|
|
8387
8461
|
<p class="slm-note">The seat list offers the same selection with checkboxes for keyboard and screen-reader use.</p>`;
|
|
8388
8462
|
}
|
|
8389
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>`;
|
|
8390
8467
|
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
8391
|
-
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
|
+
}
|
|
8392
8474
|
const lifecycle = this.caps.manage ? `
|
|
8393
8475
|
<p class="slm-eyebrow" style="margin-top:18px">Lifecycle</p>
|
|
8394
8476
|
<div class="slm-ch-row2" style="margin-top:2px">
|
|
@@ -8399,9 +8481,7 @@ var ChannelsMode = class {
|
|
|
8399
8481
|
<p class="slm-note">Archive returns the allocation to a destination you choose. Nothing is ever deleted silently.</p>` : "";
|
|
8400
8482
|
const access = this.caps.manage ? this.distributeHtml(channel) : "";
|
|
8401
8483
|
return `
|
|
8402
|
-
|
|
8403
|
-
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
8404
|
-
</p>
|
|
8484
|
+
${back}
|
|
8405
8485
|
<p class="slm-eyebrow">Channel \xB7 ${esc(channel.name)}</p>
|
|
8406
8486
|
<div class="slm-ch-list">${this.channelRowHtml(channel)}</div>
|
|
8407
8487
|
${access}
|
|
@@ -8498,16 +8578,17 @@ var ChannelsMode = class {
|
|
|
8498
8578
|
hostedLinksHtml() {
|
|
8499
8579
|
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Buyer links</p>`;
|
|
8500
8580
|
if (this.linksState === "unsupported") {
|
|
8501
|
-
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>
|
|
8502
8582
|
<span><b>Buyer links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
8503
8583
|
}
|
|
8504
8584
|
if (this.linksState === "error") {
|
|
8505
|
-
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8506
|
-
<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.
|
|
8507
8588
|
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
8508
8589
|
}
|
|
8509
|
-
if (this.linksState === "loading" && !this.links.length) {
|
|
8510
|
-
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>`;
|
|
8511
8592
|
}
|
|
8512
8593
|
if (!this.links.length) return "";
|
|
8513
8594
|
return `${eyebrow}
|
|
@@ -8549,9 +8630,14 @@ var ChannelsMode = class {
|
|
|
8549
8630
|
];
|
|
8550
8631
|
const current = this.previewAudience[0] ?? PUBLIC_CHANNEL_ID;
|
|
8551
8632
|
const options = audienceOptions.map((entry) => `<option value="${esc(entry.id)}"${entry.id === current ? " selected" : ""}>${esc(entry.name)}</option>`).join("");
|
|
8552
|
-
const unsupported = this.
|
|
8633
|
+
const unsupported = this.previewState === "unsupported" ? `<div class="slm-ch-alert warn" data-ch-state="preview-unsupported"><span>\u2139</span>
|
|
8553
8634
|
<span><b>Preview needs a newer server.</b> Allocation management works normally;
|
|
8554
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>` : "";
|
|
8555
8641
|
const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
|
|
8556
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")}.
|
|
8557
8643
|
A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
|
|
@@ -8572,7 +8658,7 @@ var ChannelsMode = class {
|
|
|
8572
8658
|
${includePublic}
|
|
8573
8659
|
<p class="slm-hint">This is the same projection the buyer SDK receives for this audience \u2014 not a local
|
|
8574
8660
|
approximation. It is read-only: clicks open seat details, and no holds are created.</p>
|
|
8575
|
-
${unsupported}${unavailable}
|
|
8661
|
+
${busy}${failed}${unsupported}${unavailable}
|
|
8576
8662
|
<div class="slm-ch-legend">
|
|
8577
8663
|
<div class="r"><span class="sw" style="background:#6e7bff"></span> Eligible & free \u2014 buyable by this audience</div>
|
|
8578
8664
|
<div class="r"><span class="sw" style="background:#3a4051"></span> Unavailable to this audience (one neutral state)</div>
|
|
@@ -8581,6 +8667,7 @@ var ChannelsMode = class {
|
|
|
8581
8667
|
${summary}`;
|
|
8582
8668
|
}
|
|
8583
8669
|
paintSelection() {
|
|
8670
|
+
if (this.caps.manage && this.host.selectionLabels().length) this.assignOpen = true;
|
|
8584
8671
|
if (this.view === "inspect" && !this.detailChannelId) this.paintRail();
|
|
8585
8672
|
}
|
|
8586
8673
|
wireRail() {
|
|
@@ -8591,16 +8678,27 @@ var ChannelsMode = class {
|
|
|
8591
8678
|
rail.querySelectorAll("[data-ch-map]").forEach((button) => {
|
|
8592
8679
|
button.addEventListener("click", () => {
|
|
8593
8680
|
this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
|
|
8681
|
+
if (this.mapIntent === "assign") this.assignOpen = true;
|
|
8594
8682
|
this.paintRail();
|
|
8595
8683
|
this.onInteractionChange?.();
|
|
8596
8684
|
});
|
|
8597
8685
|
});
|
|
8598
|
-
rail.querySelectorAll("[data-ch-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
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();
|
|
8604
8702
|
});
|
|
8605
8703
|
});
|
|
8606
8704
|
rail.querySelectorAll("[data-ch-rotate]").forEach((button) => {
|
|
@@ -8639,6 +8737,12 @@ var ChannelsMode = class {
|
|
|
8639
8737
|
button.addEventListener("click", () => this.railAction(button.dataset.chAct));
|
|
8640
8738
|
});
|
|
8641
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
|
+
}
|
|
8642
8746
|
railAction(action) {
|
|
8643
8747
|
switch (action) {
|
|
8644
8748
|
case "sections":
|
|
@@ -8712,6 +8816,22 @@ var ChannelsMode = class {
|
|
|
8712
8816
|
case "pick-category":
|
|
8713
8817
|
this.pickCategory();
|
|
8714
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;
|
|
8715
8835
|
default:
|
|
8716
8836
|
break;
|
|
8717
8837
|
}
|
|
@@ -8861,7 +8981,8 @@ var ChannelsMode = class {
|
|
|
8861
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>`);
|
|
8862
8982
|
}
|
|
8863
8983
|
});
|
|
8864
|
-
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>`);
|
|
8865
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.";
|
|
8866
8987
|
list.querySelectorAll("[data-ch-scope-group]").forEach((button) => button.addEventListener("click", () => {
|
|
8867
8988
|
const items = groups[Number(button.dataset.chScopeGroup)]?.[1] ?? [];
|
|
@@ -8911,7 +9032,8 @@ var ChannelsMode = class {
|
|
|
8911
9032
|
this.renderScrim(`
|
|
8912
9033
|
<h3 id="slm-ch-dlg-title">Add whole sections</h3>
|
|
8913
9034
|
<p class="sub">Choose one or more. They are added to the seats already selected on the map.</p>
|
|
8914
|
-
<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>
|
|
8915
9037
|
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8916
9038
|
<div class="foot">
|
|
8917
9039
|
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
@@ -8968,6 +9090,7 @@ var ChannelsMode = class {
|
|
|
8968
9090
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
8969
9091
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
8970
9092
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
9093
|
+
else if (state.kind === "menu") this.renderMenuDialog(state);
|
|
8971
9094
|
else if (state.kind === "linkCreate") this.renderLinkCreateDialog(state);
|
|
8972
9095
|
else if (state.kind === "linkRotate") this.renderLinkRotateDialog(state);
|
|
8973
9096
|
else if (state.kind === "linkRevoke") this.renderLinkRevokeDialog(state);
|
|
@@ -9223,6 +9346,59 @@ var ChannelsMode = class {
|
|
|
9223
9346
|
void bar.offsetWidth;
|
|
9224
9347
|
bar.classList.add("shake");
|
|
9225
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
|
+
}
|
|
9226
9402
|
renderRenameDialog(state) {
|
|
9227
9403
|
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
9228
9404
|
if (!channel) {
|
|
@@ -9283,9 +9459,11 @@ var ChannelsMode = class {
|
|
|
9283
9459
|
this.host.onError(err);
|
|
9284
9460
|
}
|
|
9285
9461
|
}
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
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;
|
|
9289
9467
|
const paused = channel.state !== "paused";
|
|
9290
9468
|
try {
|
|
9291
9469
|
await this.host.api.setChannelPaused(this.host.eventKey, channel.id, paused);
|