@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.js
CHANGED
|
@@ -6976,7 +6976,23 @@ var CHANNELS_CSS = (
|
|
|
6976
6976
|
.slm-ch-counts b.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6977
6977
|
@keyframes slm-ch-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08)}}
|
|
6978
6978
|
.slm-ch-access{margin-top:6px;font-size:10.5px;color:var(--slm-muted)}
|
|
6979
|
-
|
|
6979
|
+
/* A row that opens its channel must READ as pressable, and be one for a keyboard
|
|
6980
|
+
too. It is a role="button" div rather than a <button> because the \u22EF control
|
|
6981
|
+
lives inside it, and a button inside a button is not valid HTML. */
|
|
6982
|
+
.slm-ch-row.open{cursor:pointer}
|
|
6983
|
+
.slm-ch-row.open:hover{border-color:var(--slm-accent);background:color-mix(in srgb,var(--slm-accent) 6%,var(--slm-surface))}
|
|
6984
|
+
.slm-ch-row.open:focus-visible{outline:2px solid var(--slm-accent);outline-offset:2px}
|
|
6985
|
+
.slm-ch-row.open:active{border-color:var(--slm-accent)}
|
|
6986
|
+
.slm-ch-more{flex:none;color:var(--slm-muted);font-weight:800;padding:0 4px;min-height:28px;border-radius:6px}
|
|
6987
|
+
.slm-ch-more:hover{color:var(--slm-text)}
|
|
6988
|
+
.slm-ch-more:focus-visible{outline:2px solid var(--slm-accent);outline-offset:1px;color:var(--slm-text)}
|
|
6989
|
+
.slm-ch-menu{display:flex;flex-direction:column;gap:8px}
|
|
6990
|
+
.slm-ch-menu .slm-btn{width:100%}
|
|
6991
|
+
/* loading is a state, never a flash of empty: same slot, visibly working */
|
|
6992
|
+
.slm-ch-busy{display:flex;align-items:center;gap:9px;font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
6993
|
+
.slm-ch-busy::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--slm-accent);flex:none;
|
|
6994
|
+
animation:slm-ch-pulse var(--slm-mo-ambient) var(--slm-mo-in-out) infinite}
|
|
6995
|
+
@keyframes slm-ch-pulse{0%,100%{opacity:.25;transform:scale(.7)}50%{opacity:1;transform:scale(1)}}
|
|
6980
6996
|
.slm-ch-selsrc{display:flex;flex-direction:column;gap:5px;margin:8px 0 12px}
|
|
6981
6997
|
.slm-ch-selsrc-row{display:flex;align-items:center;gap:8px;font-size:12px;font-variant-numeric:tabular-nums}
|
|
6982
6998
|
.slm-ch-selsrc-row .mk{width:15px;height:15px;border-radius:4px;display:grid;place-items:center;font-size:8px;
|
|
@@ -7108,6 +7124,7 @@ var CHANNELS_CSS = (
|
|
|
7108
7124
|
.slm-ch-meter i,.slm-ch-radio{transition:none!important}
|
|
7109
7125
|
.slm-ch-staged.shake,.slm-ch-tick,.slm-ch-bucket,.slm-ch-scrim,.slm-ch-dialog,
|
|
7110
7126
|
.slm-ch-counts b.bump,.slm-ch-selnum.bump{animation:none!important}
|
|
7127
|
+
.slm-ch-busy::before{animation:none!important;opacity:1}
|
|
7111
7128
|
.slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
|
|
7112
7129
|
.slm-ch-staged.done{outline:2px solid #5bd39b;outline-offset:2px}
|
|
7113
7130
|
}
|
|
@@ -7168,6 +7185,18 @@ var ChannelsMode = class {
|
|
|
7168
7185
|
this.focusedSectionId = null;
|
|
7169
7186
|
this.showArchived = false;
|
|
7170
7187
|
this.detailChannelId = null;
|
|
7188
|
+
/**
|
|
7189
|
+
* Whether the organizer has asked to assign seats.
|
|
7190
|
+
*
|
|
7191
|
+
* The list rail leads with the CHANNELS. The assignment tooling — destination
|
|
7192
|
+
* picker plus five select-by routes — used to paint unconditionally above that
|
|
7193
|
+
* list, so a first-time organizer met a workbench for a job they had not asked
|
|
7194
|
+
* to do, with the thing they came for pushed below the fold. It is now
|
|
7195
|
+
* disclosed on intent: the "Assign seats to a channel" action under the list,
|
|
7196
|
+
* the map's own "Assign seats" segment, or simply having a selection. The
|
|
7197
|
+
* selection rail always shows the tools, because there the intent is proven.
|
|
7198
|
+
*/
|
|
7199
|
+
this.assignOpen = false;
|
|
7171
7200
|
this.targetChannelId = "";
|
|
7172
7201
|
this.conflict = false;
|
|
7173
7202
|
this.dialog = null;
|
|
@@ -7194,8 +7223,16 @@ var ChannelsMode = class {
|
|
|
7194
7223
|
this.previewAudience = [];
|
|
7195
7224
|
this.previewIncludePublic = false;
|
|
7196
7225
|
this.previewProjection = null;
|
|
7197
|
-
|
|
7198
|
-
|
|
7226
|
+
/**
|
|
7227
|
+
* The buyer-preview read, as one honest state rather than a boolean.
|
|
7228
|
+
*
|
|
7229
|
+
* `previewSupported: boolean | null` could say "this worker has no projection
|
|
7230
|
+
* route", but it could not say "the read is in flight" or "the read failed" —
|
|
7231
|
+
* both of those painted an audience picker with no result underneath, which
|
|
7232
|
+
* reads as "this audience can buy nothing". Loading and error are now states
|
|
7233
|
+
* of their own, and the error one offers a retry.
|
|
7234
|
+
*/
|
|
7235
|
+
this.previewState = "idle";
|
|
7199
7236
|
this.pollTimer = null;
|
|
7200
7237
|
this.layer = null;
|
|
7201
7238
|
this.canvas = null;
|
|
@@ -7233,6 +7270,7 @@ var ChannelsMode = class {
|
|
|
7233
7270
|
if (this.active) return;
|
|
7234
7271
|
this.active = true;
|
|
7235
7272
|
this.mapIntent = "pan";
|
|
7273
|
+
this.assignOpen = false;
|
|
7236
7274
|
this.focusedSectionId = null;
|
|
7237
7275
|
this.assignmentRowsCache = null;
|
|
7238
7276
|
this.railHtml = null;
|
|
@@ -7742,6 +7780,7 @@ var ChannelsMode = class {
|
|
|
7742
7780
|
this.view = view;
|
|
7743
7781
|
if (view === "inspect") {
|
|
7744
7782
|
this.previewProjection = null;
|
|
7783
|
+
this.previewState = "idle";
|
|
7745
7784
|
this.setBanner(false);
|
|
7746
7785
|
} else {
|
|
7747
7786
|
if (!this.previewAudience.length) {
|
|
@@ -7759,6 +7798,9 @@ var ChannelsMode = class {
|
|
|
7759
7798
|
const audience = [...this.previewAudience];
|
|
7760
7799
|
const names = audience.map((id) => this.nameOf(id) ?? PUBLIC_CHANNEL_NAME).join(" + ");
|
|
7761
7800
|
this.setBanner(true, names);
|
|
7801
|
+
this.previewProjection = null;
|
|
7802
|
+
this.previewState = "loading";
|
|
7803
|
+
if (this.active) this.paintRail();
|
|
7762
7804
|
try {
|
|
7763
7805
|
this.previewProjection = await this.host.api.channelPreview(this.host.eventKey, audience, {
|
|
7764
7806
|
// Naming Public sale as the audience IS asking for public inventory; the
|
|
@@ -7766,14 +7808,14 @@ var ChannelsMode = class {
|
|
|
7766
7808
|
// this the request would resolve to an empty scope and 422.
|
|
7767
7809
|
includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
|
|
7768
7810
|
});
|
|
7769
|
-
this.
|
|
7811
|
+
this.previewState = "ready";
|
|
7770
7812
|
const eligibleSeats = this.previewProjection.available === false ? void 0 : this.previewProjection.counts?.eligible ?? this.previewProjection.eligible?.length;
|
|
7771
7813
|
this.setBanner(true, names, eligibleSeats);
|
|
7772
7814
|
} catch (err) {
|
|
7773
7815
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7774
|
-
this.
|
|
7816
|
+
this.previewState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
|
|
7775
7817
|
this.previewProjection = null;
|
|
7776
|
-
if (this.
|
|
7818
|
+
if (this.previewState === "error") this.host.onError(err);
|
|
7777
7819
|
}
|
|
7778
7820
|
if (this.active) {
|
|
7779
7821
|
this.paintRail();
|
|
@@ -7814,13 +7856,14 @@ var ChannelsMode = class {
|
|
|
7814
7856
|
}
|
|
7815
7857
|
if (this.loading && !this.list) {
|
|
7816
7858
|
this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
7817
|
-
<div class="slm-
|
|
7859
|
+
<div class="slm-ch-busy" role="status" data-ch-state="list-loading">Loading channels and allocations\u2026</div>`);
|
|
7818
7860
|
return;
|
|
7819
7861
|
}
|
|
7820
7862
|
if (!this.list && this.loadError) {
|
|
7821
7863
|
if (this.setRailHtml(`<p class="slm-eyebrow">Sales channels</p>
|
|
7822
|
-
<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
7823
|
-
<span><b>Couldn't load sales channels.</b>
|
|
7864
|
+
<div class="slm-ch-alert err" role="alert" data-ch-state="list-error"><span>\u26A0</span>
|
|
7865
|
+
<span><b>Couldn't load sales channels.</b> This event may well have channels \u2014 we could not read them.
|
|
7866
|
+
Everything else on this event still works.
|
|
7824
7867
|
<button type="button" data-ch-act="retry">Try again</button></span></div>`)) {
|
|
7825
7868
|
rail.querySelector('[data-ch-act="retry"]')?.addEventListener("click", () => {
|
|
7826
7869
|
void this.refresh();
|
|
@@ -7879,10 +7922,13 @@ var ChannelsMode = class {
|
|
|
7879
7922
|
const marker = this.markerFor(channel.id);
|
|
7880
7923
|
const badgeKind = opts.builtin ? "builtin" : channel.state;
|
|
7881
7924
|
const dim = channel.state === "paused" || channel.state === "archived" ? " dim" : "";
|
|
7882
|
-
const
|
|
7883
|
-
const
|
|
7884
|
-
|
|
7885
|
-
|
|
7925
|
+
const opens = !opts.builtin && this.caps.manage && this.detailChannelId !== channel.id;
|
|
7926
|
+
const cls = `slm-ch-row${opts.builtin ? " public" : ""}${channel.state === "archived" ? " archived" : ""}${this.detailChannelId === channel.id ? " on" : ""}${opens ? " open" : ""}`;
|
|
7927
|
+
const more = !opts.builtin && this.caps.manage ? `<button type="button" class="slm-ch-more" data-ch-menu="${esc(channel.id)}"
|
|
7928
|
+
aria-label="More actions for ${esc(channel.name)}" aria-haspopup="dialog">\u22EF</button>` : "";
|
|
7929
|
+
const rowAttrs = opens ? ` role="button" tabindex="0" data-ch-open="${esc(channel.id)}"
|
|
7930
|
+
aria-label="Open ${esc(channel.name)}"` : "";
|
|
7931
|
+
return `<div class="${cls}"${rowAttrs}>
|
|
7886
7932
|
<span class="slm-ch-head">
|
|
7887
7933
|
<span class="slm-ch-mk${dim}" style="background:${esc(marker.color)}" aria-hidden="true">${esc(marker.letter)}</span>
|
|
7888
7934
|
<span class="slm-ch-name">${esc(channel.name)}</span>
|
|
@@ -7896,41 +7942,68 @@ var ChannelsMode = class {
|
|
|
7896
7942
|
listRailHtml() {
|
|
7897
7943
|
const list = this.list;
|
|
7898
7944
|
const archivedCount = list.channels.filter((channel) => channel.state === "archived").length;
|
|
7945
|
+
const visible = list.channels.filter((channel) => this.showArchived || channel.state !== "archived");
|
|
7899
7946
|
const rows = [
|
|
7900
7947
|
this.channelRowHtml(list.publicSale, { builtin: true }),
|
|
7901
|
-
...
|
|
7948
|
+
...visible.map((channel, index) => this.channelRowHtml(channel, { index }))
|
|
7902
7949
|
].join("");
|
|
7950
|
+
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>`;
|
|
7903
7951
|
const create = this.caps.manage ? `<button type="button" class="slm-btn ghost" style="width:100%" data-ch-act="create">+ Create channel</button>` : "";
|
|
7904
7952
|
const readOnly = this.caps.manage ? "" : `<p class="slm-note">You can see how inventory is allocated. Changing it needs channel-management permission.</p>`;
|
|
7905
7953
|
return `
|
|
7906
|
-
${this.caps.manage ? this.assignmentToolsHtml() : ""}
|
|
7907
7954
|
<p class="slm-eyebrow">Sales channels</p>
|
|
7908
7955
|
<p class="slm-hint">Channel colours and names are only visible to organizers, never to buyers.</p>
|
|
7909
7956
|
<div class="slm-ch-list">${rows}</div>
|
|
7957
|
+
${empty}
|
|
7910
7958
|
${create}
|
|
7911
7959
|
${readOnly}
|
|
7960
|
+
${this.assignEntryHtml()}
|
|
7912
7961
|
<p class="slm-note" style="margin-top:10px">
|
|
7913
7962
|
<button type="button" class="slm-linkbtn" data-ch-act="toggle-archived" aria-pressed="${this.showArchived}"
|
|
7914
7963
|
style="text-align:left">${this.showArchived ? "Hide" : "Show"} archived${archivedCount ? ` (${archivedCount})` : ""}</button>
|
|
7915
7964
|
</p>`;
|
|
7916
7965
|
}
|
|
7966
|
+
/**
|
|
7967
|
+
* The assignment entry point on the list rail.
|
|
7968
|
+
*
|
|
7969
|
+
* Collapsed it is ONE plain-language action, so the channels the organizer came
|
|
7970
|
+
* for stay at the top of the panel. Opened it is the same tool set that has
|
|
7971
|
+
* always worked, in the same order, plus the way back out — and opening it is
|
|
7972
|
+
* also what the map's "Assign seats" segment does, so the two routes into the
|
|
7973
|
+
* job cannot disagree about whether it is running.
|
|
7974
|
+
*/
|
|
7975
|
+
assignEntryHtml() {
|
|
7976
|
+
if (!this.caps.manage) return "";
|
|
7977
|
+
if (!this.assignOpen) {
|
|
7978
|
+
return `
|
|
7979
|
+
<button type="button" class="slm-btn ghost" style="width:100%;margin-top:8px"
|
|
7980
|
+
data-ch-act="assign-open" aria-expanded="false">Assign seats to a channel</button>
|
|
7981
|
+
<p class="slm-note">Move whole sections, rows, a dragged area or single seats out of public sale
|
|
7982
|
+
and into a channel. Nothing moves until you review and apply.</p>`;
|
|
7983
|
+
}
|
|
7984
|
+
return `<div style="margin-top:8px">${this.assignmentToolsHtml({ collapsible: true })}</div>`;
|
|
7985
|
+
}
|
|
7917
7986
|
/**
|
|
7918
7987
|
* Destination-first assignment controls.
|
|
7919
7988
|
*
|
|
7920
7989
|
* These used to live only inside the selection rail, which meant every route
|
|
7921
7990
|
* into them was gated behind "select a seat on the map first" — the section,
|
|
7922
7991
|
* row and category choosers were invisible until the organizer had already
|
|
7923
|
-
* done the work by hand. They
|
|
7924
|
-
*
|
|
7925
|
-
*
|
|
7992
|
+
* done the work by hand. They are still reachable before a seat is selected,
|
|
7993
|
+
* but they are no longer the first thing in the panel: on the list rail they
|
|
7994
|
+
* are disclosed by `assignEntryHtml`, and there they carry a way back out
|
|
7995
|
+
* (`collapsible`). In the selection rail there is nothing to disclose — a
|
|
7996
|
+
* selection IS the intent — so they paint unconditionally and without Done.
|
|
7926
7997
|
*/
|
|
7927
|
-
assignmentToolsHtml() {
|
|
7998
|
+
assignmentToolsHtml(opts = {}) {
|
|
7928
7999
|
const options = this.assignableChannels().map((channel) => `<option value="${esc(channel.id)}"${channel.id === this.targetChannelId ? " selected" : ""}>${esc(channel.name)}</option>`).join("");
|
|
7929
8000
|
const sections = this.host.sections().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-sections">Sections</button>' : "";
|
|
7930
8001
|
const rows = this.assignmentRows().length ? '<button type="button" class="slm-btn ghost" data-ch-act="pick-rows">Rows</button>' : "";
|
|
7931
8002
|
const dragClass = this.mapIntent === "assign" ? "slm-btn" : "slm-btn ghost";
|
|
8003
|
+
const done = opts.collapsible ? `<button type="button" class="slm-linkbtn" data-ch-act="assign-close"
|
|
8004
|
+
aria-expanded="true" style="float:right;font-weight:800">Done</button>` : "";
|
|
7932
8005
|
return `
|
|
7933
|
-
<p class="slm-eyebrow"
|
|
8006
|
+
<p class="slm-eyebrow">${done}Assign inventory</p>
|
|
7934
8007
|
<div class="slm-field">
|
|
7935
8008
|
<label for="slm-ch-target">Assign to</label>
|
|
7936
8009
|
<select class="slm-select" id="slm-ch-target" data-ch-target>${options}</select>
|
|
@@ -7980,8 +8053,16 @@ var ChannelsMode = class {
|
|
|
7980
8053
|
<p class="slm-note">The seat list offers the same selection with checkboxes for keyboard and screen-reader use.</p>`;
|
|
7981
8054
|
}
|
|
7982
8055
|
detailRailHtml(channelId) {
|
|
8056
|
+
const back = `<p class="slm-eyebrow">
|
|
8057
|
+
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
8058
|
+
</p>`;
|
|
7983
8059
|
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
7984
|
-
if (!channel)
|
|
8060
|
+
if (!channel) {
|
|
8061
|
+
return `${back}
|
|
8062
|
+
<div class="slm-ch-alert warn" role="status" data-ch-state="detail-gone"><span>\u2139</span>
|
|
8063
|
+
<span><b>This channel is no longer on this event.</b> It was archived or removed while you had it open.
|
|
8064
|
+
${this.showArchived ? "" : "Archived channels are hidden \u2014 use \u201CShow archived\u201D on the list to see it."}</span></div>`;
|
|
8065
|
+
}
|
|
7985
8066
|
const lifecycle = this.caps.manage ? `
|
|
7986
8067
|
<p class="slm-eyebrow" style="margin-top:18px">Lifecycle</p>
|
|
7987
8068
|
<div class="slm-ch-row2" style="margin-top:2px">
|
|
@@ -7992,9 +8073,7 @@ var ChannelsMode = class {
|
|
|
7992
8073
|
<p class="slm-note">Archive returns the allocation to a destination you choose. Nothing is ever deleted silently.</p>` : "";
|
|
7993
8074
|
const access = this.caps.manage ? this.distributeHtml(channel) : "";
|
|
7994
8075
|
return `
|
|
7995
|
-
|
|
7996
|
-
<button type="button" class="slm-linkbtn" data-ch-act="back" style="text-align:left">\u2039 All channels</button>
|
|
7997
|
-
</p>
|
|
8076
|
+
${back}
|
|
7998
8077
|
<p class="slm-eyebrow">Channel \xB7 ${esc(channel.name)}</p>
|
|
7999
8078
|
<div class="slm-ch-list">${this.channelRowHtml(channel)}</div>
|
|
8000
8079
|
${access}
|
|
@@ -8091,16 +8170,17 @@ var ChannelsMode = class {
|
|
|
8091
8170
|
hostedLinksHtml() {
|
|
8092
8171
|
const eyebrow = `<p class="slm-eyebrow" style="margin-top:18px">Buyer links</p>`;
|
|
8093
8172
|
if (this.linksState === "unsupported") {
|
|
8094
|
-
return `${eyebrow}<div class="slm-ch-alert warn"><span>\u2139</span>
|
|
8173
|
+
return `${eyebrow}<div class="slm-ch-alert warn" data-ch-state="links-unsupported"><span>\u2139</span>
|
|
8095
8174
|
<span><b>Buyer links need a newer server.</b> Everything else on this channel works normally.</span></div>`;
|
|
8096
8175
|
}
|
|
8097
8176
|
if (this.linksState === "error") {
|
|
8098
|
-
return `${eyebrow}<div class="slm-ch-alert err" role="alert"><span>\u26A0</span>
|
|
8099
|
-
<span><b>Couldn't load this channel's links.</b>
|
|
8177
|
+
return `${eyebrow}<div class="slm-ch-alert err" role="alert" data-ch-state="links-error"><span>\u26A0</span>
|
|
8178
|
+
<span><b>Couldn't load this channel's links.</b> This is a failed read, not an empty list \u2014
|
|
8179
|
+
any live link is still working.
|
|
8100
8180
|
<button type="button" data-ch-act="link-reload">Try again</button></span></div>`;
|
|
8101
8181
|
}
|
|
8102
|
-
if (this.linksState === "loading" && !this.links.length) {
|
|
8103
|
-
return `${eyebrow}<div class="slm-
|
|
8182
|
+
if (this.linksState === "idle" || this.linksState === "loading" && !this.links.length) {
|
|
8183
|
+
return `${eyebrow}<div class="slm-ch-busy" role="status" data-ch-state="links-loading">Loading buyer links\u2026</div>`;
|
|
8104
8184
|
}
|
|
8105
8185
|
if (!this.links.length) return "";
|
|
8106
8186
|
return `${eyebrow}
|
|
@@ -8142,9 +8222,14 @@ var ChannelsMode = class {
|
|
|
8142
8222
|
];
|
|
8143
8223
|
const current = this.previewAudience[0] ?? PUBLIC_CHANNEL_ID;
|
|
8144
8224
|
const options = audienceOptions.map((entry) => `<option value="${esc(entry.id)}"${entry.id === current ? " selected" : ""}>${esc(entry.name)}</option>`).join("");
|
|
8145
|
-
const unsupported = this.
|
|
8225
|
+
const unsupported = this.previewState === "unsupported" ? `<div class="slm-ch-alert warn" data-ch-state="preview-unsupported"><span>\u2139</span>
|
|
8146
8226
|
<span><b>Preview needs a newer server.</b> Allocation management works normally;
|
|
8147
8227
|
the buyer-view simulation will appear once this event's API is updated.</span></div>` : "";
|
|
8228
|
+
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>` : "";
|
|
8229
|
+
const failed = this.previewState === "error" ? `<div class="slm-ch-alert err" role="alert" data-ch-state="preview-error"><span>\u26A0</span>
|
|
8230
|
+
<span><b>Couldn't load the buyer preview.</b> Nothing is wrong with this audience's seats \u2014
|
|
8231
|
+
the simulation itself failed to load.
|
|
8232
|
+
<button type="button" data-ch-act="preview-retry">Try again</button></span></div>` : "";
|
|
8148
8233
|
const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
|
|
8149
8234
|
<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")}.
|
|
8150
8235
|
A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
|
|
@@ -8165,7 +8250,7 @@ var ChannelsMode = class {
|
|
|
8165
8250
|
${includePublic}
|
|
8166
8251
|
<p class="slm-hint">This is the same projection the buyer SDK receives for this audience \u2014 not a local
|
|
8167
8252
|
approximation. It is read-only: clicks open seat details, and no holds are created.</p>
|
|
8168
|
-
${unsupported}${unavailable}
|
|
8253
|
+
${busy}${failed}${unsupported}${unavailable}
|
|
8169
8254
|
<div class="slm-ch-legend">
|
|
8170
8255
|
<div class="r"><span class="sw" style="background:#6e7bff"></span> Eligible & free \u2014 buyable by this audience</div>
|
|
8171
8256
|
<div class="r"><span class="sw" style="background:#3a4051"></span> Unavailable to this audience (one neutral state)</div>
|
|
@@ -8174,6 +8259,7 @@ var ChannelsMode = class {
|
|
|
8174
8259
|
${summary}`;
|
|
8175
8260
|
}
|
|
8176
8261
|
paintSelection() {
|
|
8262
|
+
if (this.caps.manage && this.host.selectionLabels().length) this.assignOpen = true;
|
|
8177
8263
|
if (this.view === "inspect" && !this.detailChannelId) this.paintRail();
|
|
8178
8264
|
}
|
|
8179
8265
|
wireRail() {
|
|
@@ -8184,16 +8270,27 @@ var ChannelsMode = class {
|
|
|
8184
8270
|
rail.querySelectorAll("[data-ch-map]").forEach((button) => {
|
|
8185
8271
|
button.addEventListener("click", () => {
|
|
8186
8272
|
this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
|
|
8273
|
+
if (this.mapIntent === "assign") this.assignOpen = true;
|
|
8187
8274
|
this.paintRail();
|
|
8188
8275
|
this.onInteractionChange?.();
|
|
8189
8276
|
});
|
|
8190
8277
|
});
|
|
8191
|
-
rail.querySelectorAll("[data-ch-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8278
|
+
rail.querySelectorAll("[data-ch-open]").forEach((row) => {
|
|
8279
|
+
const open = () => this.openChannel(row.dataset.chOpen);
|
|
8280
|
+
row.addEventListener("click", open);
|
|
8281
|
+
row.addEventListener("keydown", (event) => {
|
|
8282
|
+
if (event.key !== "Enter" && event.key !== " " && event.key !== "Spacebar") return;
|
|
8283
|
+
event.preventDefault();
|
|
8284
|
+
open();
|
|
8285
|
+
});
|
|
8286
|
+
});
|
|
8287
|
+
rail.querySelectorAll("[data-ch-menu]").forEach((button) => {
|
|
8288
|
+
button.addEventListener("click", (event) => {
|
|
8289
|
+
event.stopPropagation();
|
|
8290
|
+
this.openDialog({ kind: "menu", channelId: button.dataset.chMenu });
|
|
8291
|
+
});
|
|
8292
|
+
button.addEventListener("keydown", (event) => {
|
|
8293
|
+
event.stopPropagation();
|
|
8197
8294
|
});
|
|
8198
8295
|
});
|
|
8199
8296
|
rail.querySelectorAll("[data-ch-rotate]").forEach((button) => {
|
|
@@ -8232,6 +8329,12 @@ var ChannelsMode = class {
|
|
|
8232
8329
|
button.addEventListener("click", () => this.railAction(button.dataset.chAct));
|
|
8233
8330
|
});
|
|
8234
8331
|
}
|
|
8332
|
+
/** Open a channel's detail panel and start its buyer-link read. */
|
|
8333
|
+
openChannel(channelId) {
|
|
8334
|
+
this.detailChannelId = channelId;
|
|
8335
|
+
this.paintRail();
|
|
8336
|
+
void this.loadLinks(channelId).then(() => this.paintRail());
|
|
8337
|
+
}
|
|
8235
8338
|
railAction(action) {
|
|
8236
8339
|
switch (action) {
|
|
8237
8340
|
case "sections":
|
|
@@ -8305,6 +8408,22 @@ var ChannelsMode = class {
|
|
|
8305
8408
|
case "pick-category":
|
|
8306
8409
|
this.pickCategory();
|
|
8307
8410
|
break;
|
|
8411
|
+
case "assign-open":
|
|
8412
|
+
this.assignOpen = true;
|
|
8413
|
+
this.paintRail();
|
|
8414
|
+
break;
|
|
8415
|
+
// Collapsing the tools must also disarm the marquee. Leaving it armed
|
|
8416
|
+
// would hide a mode the map is still in — the organizer would drag to pan
|
|
8417
|
+
// and select seats instead, with no control on screen saying why.
|
|
8418
|
+
case "assign-close":
|
|
8419
|
+
this.assignOpen = false;
|
|
8420
|
+
this.mapIntent = "pan";
|
|
8421
|
+
this.paintRail();
|
|
8422
|
+
this.onInteractionChange?.();
|
|
8423
|
+
break;
|
|
8424
|
+
case "preview-retry":
|
|
8425
|
+
void this.loadPreview();
|
|
8426
|
+
break;
|
|
8308
8427
|
default:
|
|
8309
8428
|
break;
|
|
8310
8429
|
}
|
|
@@ -8454,7 +8573,8 @@ var ChannelsMode = class {
|
|
|
8454
8573
|
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>`);
|
|
8455
8574
|
}
|
|
8456
8575
|
});
|
|
8457
|
-
list.innerHTML = blocks.join("") || `<div class="slm-ch-scope-empty">No sections or rows match \u201C${esc(query)}\u201D.</div
|
|
8576
|
+
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.
|
|
8577
|
+
Use Drag box or the seat list instead.</div>`);
|
|
8458
8578
|
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.";
|
|
8459
8579
|
list.querySelectorAll("[data-ch-scope-group]").forEach((button) => button.addEventListener("click", () => {
|
|
8460
8580
|
const items = groups[Number(button.dataset.chScopeGroup)]?.[1] ?? [];
|
|
@@ -8504,7 +8624,8 @@ var ChannelsMode = class {
|
|
|
8504
8624
|
this.renderScrim(`
|
|
8505
8625
|
<h3 id="slm-ch-dlg-title">Add whole sections</h3>
|
|
8506
8626
|
<p class="sub">Choose one or more. They are added to the seats already selected on the map.</p>
|
|
8507
|
-
<div class="slm-ch-seatlist">${body || `<div class="slm-empty">
|
|
8627
|
+
<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.
|
|
8628
|
+
Use Drag box or the seat list instead.</div>`}</div>
|
|
8508
8629
|
<p class="slm-ch-err" data-ch-error hidden></p>
|
|
8509
8630
|
<div class="foot">
|
|
8510
8631
|
<button type="button" class="quiet" data-ch-close>Cancel</button>
|
|
@@ -8561,6 +8682,7 @@ var ChannelsMode = class {
|
|
|
8561
8682
|
else if (state.kind === "archive") this.renderArchiveDialog(state);
|
|
8562
8683
|
else if (state.kind === "rename") this.renderRenameDialog(state);
|
|
8563
8684
|
else if (state.kind === "seatlist") this.renderSeatListDialog();
|
|
8685
|
+
else if (state.kind === "menu") this.renderMenuDialog(state);
|
|
8564
8686
|
else if (state.kind === "linkCreate") this.renderLinkCreateDialog(state);
|
|
8565
8687
|
else if (state.kind === "linkRotate") this.renderLinkRotateDialog(state);
|
|
8566
8688
|
else if (state.kind === "linkRevoke") this.renderLinkRevokeDialog(state);
|
|
@@ -8816,6 +8938,59 @@ var ChannelsMode = class {
|
|
|
8816
8938
|
void bar.offsetWidth;
|
|
8817
8939
|
bar.classList.add("shake");
|
|
8818
8940
|
}
|
|
8941
|
+
/**
|
|
8942
|
+
* The ⋯ menu.
|
|
8943
|
+
*
|
|
8944
|
+
* Opening a channel is the ROW's job now, so ⋯ carries what is left: the
|
|
8945
|
+
* secondary and destructive lifecycle actions. It is rendered through the same
|
|
8946
|
+
* scrim primitive as every other sheet, which is what gives it a focus trap,
|
|
8947
|
+
* Escape, and a name — a bare absolutely-positioned popup would have had none
|
|
8948
|
+
* of those. Archive keeps its own confirmation dialog; this menu never
|
|
8949
|
+
* destroys anything by itself.
|
|
8950
|
+
*/
|
|
8951
|
+
renderMenuDialog(state) {
|
|
8952
|
+
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
8953
|
+
if (!channel || !this.caps.manage) {
|
|
8954
|
+
this.closeDialog();
|
|
8955
|
+
return;
|
|
8956
|
+
}
|
|
8957
|
+
const paused = channel.state === "paused";
|
|
8958
|
+
this.renderScrim(`
|
|
8959
|
+
<h3 id="slm-ch-dlg-title">${esc(channel.name)}</h3>
|
|
8960
|
+
<p class="sub">Open the channel to allocate seats and hand out buyer access.
|
|
8961
|
+
These are the rest of its actions.</p>
|
|
8962
|
+
<div class="slm-ch-menu">
|
|
8963
|
+
<button type="button" class="slm-btn" data-ch-menu-act="open">Open channel</button>
|
|
8964
|
+
<button type="button" class="slm-btn ghost" data-ch-menu-act="rename">Rename</button>
|
|
8965
|
+
<button type="button" class="slm-btn ghost" data-ch-menu-act="pause">${paused ? "Resume selling" : "Pause selling"}</button>
|
|
8966
|
+
<button type="button" class="slm-btn ghost" data-ch-menu-act="archive">Archive\u2026</button>
|
|
8967
|
+
</div>
|
|
8968
|
+
<p class="slm-note">Pausing stops new buyer access; checkouts already running can finish.
|
|
8969
|
+
Archiving closes the channel for good and returns its free seats to a destination you choose.
|
|
8970
|
+
Nothing is ever deleted silently.</p>
|
|
8971
|
+
<div class="foot"><button type="button" class="quiet" data-ch-close>Cancel</button></div>`, (dialog) => {
|
|
8972
|
+
dialog.querySelectorAll("[data-ch-menu-act]").forEach((button) => {
|
|
8973
|
+
button.addEventListener("click", () => {
|
|
8974
|
+
const act = button.dataset.chMenuAct;
|
|
8975
|
+
if (act === "open") {
|
|
8976
|
+
this.closeDialog();
|
|
8977
|
+
this.openChannel(channel.id);
|
|
8978
|
+
return;
|
|
8979
|
+
}
|
|
8980
|
+
if (act === "rename") {
|
|
8981
|
+
this.openDialog({ kind: "rename", channelId: channel.id });
|
|
8982
|
+
return;
|
|
8983
|
+
}
|
|
8984
|
+
if (act === "archive") {
|
|
8985
|
+
this.openDialog({ kind: "archive", channelId: channel.id });
|
|
8986
|
+
return;
|
|
8987
|
+
}
|
|
8988
|
+
this.closeDialog();
|
|
8989
|
+
void this.togglePause(channel.id);
|
|
8990
|
+
});
|
|
8991
|
+
});
|
|
8992
|
+
});
|
|
8993
|
+
}
|
|
8819
8994
|
renderRenameDialog(state) {
|
|
8820
8995
|
const channel = this.list?.channels.find((item) => item.id === state.channelId);
|
|
8821
8996
|
if (!channel) {
|
|
@@ -8876,9 +9051,11 @@ var ChannelsMode = class {
|
|
|
8876
9051
|
this.host.onError(err);
|
|
8877
9052
|
}
|
|
8878
9053
|
}
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
9054
|
+
/** `channelId` is explicit because this is reachable from the ⋯ menu on a row
|
|
9055
|
+
* that is NOT the open channel, as well as from the detail panel itself. */
|
|
9056
|
+
async togglePause(channelId = this.detailChannelId) {
|
|
9057
|
+
const channel = this.list?.channels.find((item) => item.id === channelId);
|
|
9058
|
+
if (!channel || !this.caps.manage) return;
|
|
8882
9059
|
const paused = channel.state !== "paused";
|
|
8883
9060
|
try {
|
|
8884
9061
|
await this.host.api.setChannelPaused(this.host.eventKey, channel.id, paused);
|