@seatlayer/js 0.36.3 → 0.37.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 +343 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +94 -8
- package/dist/index.d.ts +94 -8
- package/dist/index.js +343 -46
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6547,6 +6547,11 @@ var ManageApi = class {
|
|
|
6547
6547
|
var POLL_MS = 1e4;
|
|
6548
6548
|
var MAX_FLAGS = 8;
|
|
6549
6549
|
var SEAT_LIST_PAGE = 300;
|
|
6550
|
+
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
6551
|
+
var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
6552
|
+
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
6553
|
+
var PREVIEW_UNAVAILABLE_STROKE = "#4b5669";
|
|
6554
|
+
var ALLOCATION_STROKE = "#101723";
|
|
6550
6555
|
var CHANNELS_CSS = `
|
|
6551
6556
|
.slm{--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
6552
6557
|
--slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
|
|
@@ -6560,6 +6565,8 @@ var CHANNELS_CSS = `
|
|
|
6560
6565
|
background:rgba(14,16,23,.88);border:1px solid var(--slm-line);font-size:10px;font-weight:800;letter-spacing:.04em;
|
|
6561
6566
|
transform:translate(-50%,-50%);white-space:nowrap}
|
|
6562
6567
|
.slm-ch-flag .mk{width:14px;height:14px;border-radius:4px;display:grid;place-items:center;font-size:8.5px;font-weight:800;color:#0e1017}
|
|
6568
|
+
.slm-ch-section-target{position:absolute;pointer-events:auto;padding:0;border:0;border-radius:8px;background:transparent;cursor:zoom-in}
|
|
6569
|
+
.slm-ch-section-target:focus-visible{outline:2px solid var(--slm-accent);outline-offset:-3px;background:color-mix(in srgb,var(--slm-accent) 12%,transparent)}
|
|
6563
6570
|
|
|
6564
6571
|
/* preview banner \u2014 raised with the organizer chrome dim, as one transition */
|
|
6565
6572
|
.slm-ch-banner{position:absolute;left:50%;top:14px;z-index:6;display:flex;align-items:center;gap:9px;padding:8px 14px;
|
|
@@ -6579,7 +6586,7 @@ var CHANNELS_CSS = `
|
|
|
6579
6586
|
transition:transform var(--slm-mo-slow) var(--slm-mo-out),opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
6580
6587
|
.slm-ch-staged.on{transform:none;opacity:1}
|
|
6581
6588
|
.slm-ch-staged.done{background:rgba(31,122,77,.96);border-color:#1f7a4d}
|
|
6582
|
-
.slm-ch-staged.shake{animation:slm-ch-shake
|
|
6589
|
+
.slm-ch-staged.shake{animation:slm-ch-shake var(--slm-mo-slow) var(--slm-mo-in-out) 2}
|
|
6583
6590
|
.slm-ch-staged b{font-variant-numeric:tabular-nums}
|
|
6584
6591
|
.slm-ch-staged .grow{flex:1}
|
|
6585
6592
|
.slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
|
|
@@ -6596,6 +6603,11 @@ var CHANNELS_CSS = `
|
|
|
6596
6603
|
.slm-ch-viewseg button{flex:1;padding:6px 8px;min-height:34px;border-radius:7px;font-size:11px;font-weight:800;color:var(--slm-muted)}
|
|
6597
6604
|
.slm-ch-viewseg button.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
6598
6605
|
.slm-ch-viewseg button:disabled{opacity:.5;cursor:not-allowed}
|
|
6606
|
+
.slm-ch-mapnav{margin:-2px 0 12px;padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
6607
|
+
.slm-ch-mapnav-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--slm-muted)}
|
|
6608
|
+
.slm-ch-mapnav-head button{color:var(--slm-accent);font-size:11px;font-weight:800;letter-spacing:0;text-transform:none;min-height:30px}
|
|
6609
|
+
.slm-ch-mapnav .slm-ch-viewseg{margin:8px 0 5px}
|
|
6610
|
+
.slm-ch-mapnav p{margin:0;font-size:11px;line-height:1.45;color:var(--slm-muted)}
|
|
6599
6611
|
.slm-ch-list{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
|
6600
6612
|
.slm-ch-row{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
6601
6613
|
text-align:left;width:100%;display:block;transition:border-color var(--slm-mo-quick) var(--slm-mo-out)}
|
|
@@ -6616,7 +6628,7 @@ var CHANNELS_CSS = `
|
|
|
6616
6628
|
font-variant-numeric:tabular-nums}
|
|
6617
6629
|
.slm-ch-counts b{color:var(--slm-text);font-weight:800}
|
|
6618
6630
|
.slm-ch-counts .free b{color:#5bd39b}
|
|
6619
|
-
.slm-ch-counts b.bump{animation:slm-ch-bump
|
|
6631
|
+
.slm-ch-counts b.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6620
6632
|
@keyframes slm-ch-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08)}}
|
|
6621
6633
|
.slm-ch-access{margin-top:6px;font-size:10.5px;color:var(--slm-muted)}
|
|
6622
6634
|
.slm-ch-more{flex:none;color:var(--slm-muted);font-weight:800;padding:0 4px;min-height:28px}
|
|
@@ -6626,12 +6638,13 @@ var CHANNELS_CSS = `
|
|
|
6626
6638
|
font-weight:800;color:#0e1017}
|
|
6627
6639
|
.slm-ch-selsrc-row b{min-width:30px;text-align:right;font-weight:800}
|
|
6628
6640
|
.slm-ch-selsrc-row span{color:var(--slm-muted)}
|
|
6629
|
-
.slm-ch-selnum.bump{animation:slm-ch-bump
|
|
6641
|
+
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6630
6642
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
6631
6643
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
6632
6644
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
6633
6645
|
line-height:1.5;margin-bottom:12px}
|
|
6634
6646
|
.slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
|
|
6647
|
+
.slm-ch-alert.info{background:rgba(110,123,255,.12);border:1px solid rgba(110,123,255,.44);color:#c5cbff}
|
|
6635
6648
|
.slm-ch-alert.err{background:rgba(229,72,77,.1);border:1px solid rgba(229,72,77,.45);color:#f1a4a6}
|
|
6636
6649
|
.slm-ch-alert b{color:#fff}
|
|
6637
6650
|
.slm-ch-alert button{display:block;margin-top:6px;color:#fff;font-weight:800;min-height:36px}
|
|
@@ -6778,6 +6791,10 @@ var ChannelsMode = class {
|
|
|
6778
6791
|
this.loadError = null;
|
|
6779
6792
|
this.loading = true;
|
|
6780
6793
|
this.view = "inspect";
|
|
6794
|
+
/** Pan is intentionally the initial desktop interaction. Assignment's
|
|
6795
|
+
* marquee is powerful, but must never make an organizer lose map navigation. */
|
|
6796
|
+
this.mapIntent = "pan";
|
|
6797
|
+
this.focusedSectionId = null;
|
|
6781
6798
|
this.showArchived = false;
|
|
6782
6799
|
this.detailChannelId = null;
|
|
6783
6800
|
this.targetChannelId = "";
|
|
@@ -6794,6 +6811,15 @@ var ChannelsMode = class {
|
|
|
6794
6811
|
this.links = [];
|
|
6795
6812
|
this.linksChannelId = null;
|
|
6796
6813
|
this.linksState = "idle";
|
|
6814
|
+
/**
|
|
6815
|
+
* Monotonic read generations — one for the channel list + allocation, one for
|
|
6816
|
+
* the open channel's links. Reads are concurrent (a 10s poll versus a
|
|
6817
|
+
* mutation's own reload), and the network does not promise to answer them in
|
|
6818
|
+
* order. Only the NEWEST read of each kind may write to state; an older
|
|
6819
|
+
* answer that arrives late is dropped, never painted.
|
|
6820
|
+
*/
|
|
6821
|
+
this.listSeq = 0;
|
|
6822
|
+
this.linksSeq = 0;
|
|
6797
6823
|
this.previewAudience = [];
|
|
6798
6824
|
this.previewIncludePublic = false;
|
|
6799
6825
|
this.previewProjection = null;
|
|
@@ -6820,10 +6846,14 @@ var ChannelsMode = class {
|
|
|
6820
6846
|
enter() {
|
|
6821
6847
|
if (this.active) return;
|
|
6822
6848
|
this.active = true;
|
|
6849
|
+
this.mapIntent = "pan";
|
|
6850
|
+
this.focusedSectionId = null;
|
|
6823
6851
|
this.ensureLayer();
|
|
6824
6852
|
this.host.root.classList.add("ch-mode");
|
|
6825
6853
|
this.applySheetClasses();
|
|
6854
|
+
if (this.host.sections().length > 1) this.host.showSectionOverview();
|
|
6826
6855
|
this.paintRail();
|
|
6856
|
+
this.onInteractionChange?.();
|
|
6827
6857
|
void this.refresh();
|
|
6828
6858
|
this.pollTimer = setInterval(() => {
|
|
6829
6859
|
void this.refresh({ quiet: true });
|
|
@@ -6875,6 +6905,17 @@ var ChannelsMode = class {
|
|
|
6875
6905
|
canSelect() {
|
|
6876
6906
|
return this.caps.manage && this.view === "inspect";
|
|
6877
6907
|
}
|
|
6908
|
+
/** Bulk seat assignment is explicit. In Pan map, clicks can still inspect a
|
|
6909
|
+
* single seat, while a primary-button drag always moves the camera. */
|
|
6910
|
+
usesMarqueeSelection() {
|
|
6911
|
+
return this.canSelect() && this.mapIntent === "assign";
|
|
6912
|
+
}
|
|
6913
|
+
/** The renderer calls this when the organizer opens a section from overview. */
|
|
6914
|
+
handleSectionFocus(sectionId) {
|
|
6915
|
+
if (!this.active) return;
|
|
6916
|
+
this.focusedSectionId = sectionId;
|
|
6917
|
+
this.paintRail();
|
|
6918
|
+
}
|
|
6878
6919
|
/**
|
|
6879
6920
|
* Organizer realtime integration point. M5 ships a per-scope socket for
|
|
6880
6921
|
* buyers; the organizer channel-count stream is a later milestone. When it
|
|
@@ -6902,22 +6943,28 @@ var ChannelsMode = class {
|
|
|
6902
6943
|
// ---- data -----------------------------------------------------------------
|
|
6903
6944
|
async refresh(opts = {}) {
|
|
6904
6945
|
if (!this.caps.view) return;
|
|
6946
|
+
const seq = ++this.listSeq;
|
|
6947
|
+
const superseded = () => seq !== this.listSeq;
|
|
6905
6948
|
try {
|
|
6906
6949
|
const list = await this.host.api.channels(this.host.eventKey, { includeArchived: this.showArchived });
|
|
6950
|
+
if (superseded()) return;
|
|
6907
6951
|
this.list = list;
|
|
6908
6952
|
this.assignmentVersion = list.assignmentVersion;
|
|
6909
6953
|
this.loadError = null;
|
|
6910
6954
|
if (!this.targetChannelId) {
|
|
6911
6955
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
6912
6956
|
}
|
|
6913
|
-
await this.loadAllocation();
|
|
6957
|
+
await this.loadAllocation(seq);
|
|
6958
|
+
if (superseded()) return;
|
|
6914
6959
|
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
6960
|
+
if (superseded()) return;
|
|
6915
6961
|
this.loading = false;
|
|
6916
6962
|
if (this.active) {
|
|
6917
6963
|
this.paintRail();
|
|
6918
6964
|
this.paintOverlay();
|
|
6919
6965
|
}
|
|
6920
6966
|
} catch (err) {
|
|
6967
|
+
if (superseded()) return;
|
|
6921
6968
|
this.loading = false;
|
|
6922
6969
|
if (err instanceof ManageApiError && err.status === 403) {
|
|
6923
6970
|
this.caps = { view: false, manage: false };
|
|
@@ -6929,10 +6976,11 @@ var ChannelsMode = class {
|
|
|
6929
6976
|
}
|
|
6930
6977
|
/** Walk every allocation page. Bounded by the event's seat count, and the
|
|
6931
6978
|
* server caps each page, so an arena is a handful of round trips. */
|
|
6932
|
-
async loadAllocation() {
|
|
6979
|
+
async loadAllocation(seq) {
|
|
6933
6980
|
const next = /* @__PURE__ */ new Map();
|
|
6934
6981
|
let afterLabel;
|
|
6935
6982
|
for (let page = 0; page < 200; page += 1) {
|
|
6983
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6936
6984
|
const res = await this.host.api.channelAllocation(this.host.eventKey, {
|
|
6937
6985
|
afterLabel,
|
|
6938
6986
|
limit: 1e3
|
|
@@ -6944,6 +6992,7 @@ var ChannelsMode = class {
|
|
|
6944
6992
|
if (!res.nextAfterLabel) break;
|
|
6945
6993
|
afterLabel = res.nextAfterLabel;
|
|
6946
6994
|
}
|
|
6995
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6947
6996
|
this.allocation = next;
|
|
6948
6997
|
}
|
|
6949
6998
|
// ---- lookups --------------------------------------------------------------
|
|
@@ -7010,9 +7059,10 @@ var ChannelsMode = class {
|
|
|
7010
7059
|
* Repaint the allocation (or preview) overlay in ONE canvas pass.
|
|
7011
7060
|
*
|
|
7012
7061
|
* Channel identity on the map is a fill in the administrative color PLUS the
|
|
7013
|
-
* letter flags below — never color alone.
|
|
7014
|
-
*
|
|
7015
|
-
*
|
|
7062
|
+
* letter flags below — never color alone. In buyer preview the map instead
|
|
7063
|
+
* uses two explicit, channel-neutral access states. Physical status keeps its
|
|
7064
|
+
* own cue: only FREE units are repainted, so sold/held/blocked seats still
|
|
7065
|
+
* read exactly as they do in every other tool.
|
|
7016
7066
|
*/
|
|
7017
7067
|
paintOverlay() {
|
|
7018
7068
|
const canvas = this.canvas;
|
|
@@ -7037,11 +7087,14 @@ var ChannelsMode = class {
|
|
|
7037
7087
|
if (!ctx) return;
|
|
7038
7088
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
7039
7089
|
ctx.clearRect(0, 0, width, height);
|
|
7090
|
+
const seatDetail = this.host.isSeatDetail();
|
|
7040
7091
|
const size = Math.max(3, this.host.seatPixelSize());
|
|
7041
7092
|
const half = size / 2;
|
|
7042
7093
|
const projection = this.view === "preview" ? this.previewProjection : null;
|
|
7043
7094
|
const eligible = projection ? new Set(projection.available === false ? [] : projection.eligible ?? []) : null;
|
|
7044
7095
|
const clusters = /* @__PURE__ */ new Map();
|
|
7096
|
+
const sectionTargets = !seatDetail && this.host.sections().length > 1 ? /* @__PURE__ */ new Map() : null;
|
|
7097
|
+
const previewSections = this.view === "preview" && seatDetail && size <= 15 ? /* @__PURE__ */ new Map() : null;
|
|
7045
7098
|
for (const seat of this.host.seats()) {
|
|
7046
7099
|
const status = this.host.statusOf(seat.label) ?? "free";
|
|
7047
7100
|
const channelId = this.allocation.get(seat.label) ?? PUBLIC_CHANNEL_ID;
|
|
@@ -7052,24 +7105,150 @@ var ChannelsMode = class {
|
|
|
7052
7105
|
cluster.n += 1;
|
|
7053
7106
|
clusters.set(channelId, cluster);
|
|
7054
7107
|
}
|
|
7108
|
+
if (!seatDetail) {
|
|
7109
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7110
|
+
const point2 = section ? this.host.worldToScreen({ x: seat.x, y: seat.y }) : null;
|
|
7111
|
+
if (sectionTargets && section && point2) {
|
|
7112
|
+
const bounds = sectionTargets.get(section.id) ?? {
|
|
7113
|
+
label: section.label,
|
|
7114
|
+
minX: point2.x,
|
|
7115
|
+
minY: point2.y,
|
|
7116
|
+
maxX: point2.x,
|
|
7117
|
+
maxY: point2.y
|
|
7118
|
+
};
|
|
7119
|
+
bounds.minX = Math.min(bounds.minX, point2.x);
|
|
7120
|
+
bounds.minY = Math.min(bounds.minY, point2.y);
|
|
7121
|
+
bounds.maxX = Math.max(bounds.maxX, point2.x);
|
|
7122
|
+
bounds.maxY = Math.max(bounds.maxY, point2.y);
|
|
7123
|
+
sectionTargets.set(section.id, bounds);
|
|
7124
|
+
}
|
|
7125
|
+
continue;
|
|
7126
|
+
}
|
|
7055
7127
|
if (status !== "free") continue;
|
|
7056
7128
|
let fill = null;
|
|
7129
|
+
let stroke = null;
|
|
7057
7130
|
if (this.view === "preview") {
|
|
7058
|
-
|
|
7131
|
+
if (eligible?.has(seat.label)) {
|
|
7132
|
+
fill = PREVIEW_ELIGIBLE_FILL;
|
|
7133
|
+
stroke = PREVIEW_ELIGIBLE_STROKE;
|
|
7134
|
+
} else {
|
|
7135
|
+
fill = PREVIEW_UNAVAILABLE_FILL;
|
|
7136
|
+
stroke = PREVIEW_UNAVAILABLE_STROKE;
|
|
7137
|
+
}
|
|
7059
7138
|
} else if (channelId !== PUBLIC_CHANNEL_ID) {
|
|
7060
7139
|
fill = this.markerFor(channelId).color;
|
|
7140
|
+
stroke = ALLOCATION_STROKE;
|
|
7061
7141
|
}
|
|
7062
7142
|
if (!fill) continue;
|
|
7063
7143
|
const point = this.host.worldToScreen({ x: seat.x, y: seat.y });
|
|
7064
7144
|
if (!point) continue;
|
|
7065
7145
|
if (point.x < -size || point.y < -size || point.x > width + size || point.y > height + size) continue;
|
|
7146
|
+
if (previewSections) {
|
|
7147
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7148
|
+
if (section) {
|
|
7149
|
+
const bounds = previewSections.get(section.id) ?? {
|
|
7150
|
+
label: section.label,
|
|
7151
|
+
minX: point.x,
|
|
7152
|
+
minY: point.y,
|
|
7153
|
+
maxX: point.x,
|
|
7154
|
+
maxY: point.y
|
|
7155
|
+
};
|
|
7156
|
+
bounds.minX = Math.min(bounds.minX, point.x);
|
|
7157
|
+
bounds.minY = Math.min(bounds.minY, point.y);
|
|
7158
|
+
bounds.maxX = Math.max(bounds.maxX, point.x);
|
|
7159
|
+
bounds.maxY = Math.max(bounds.maxY, point.y);
|
|
7160
|
+
previewSections.set(section.id, bounds);
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7066
7163
|
ctx.fillStyle = fill;
|
|
7067
|
-
|
|
7068
|
-
|
|
7164
|
+
if (this.view === "preview" || channelId !== PUBLIC_CHANNEL_ID) {
|
|
7165
|
+
const radius = Math.max(2, half + Math.min(1.5, half * 0.06));
|
|
7166
|
+
ctx.globalAlpha = 1;
|
|
7167
|
+
ctx.beginPath();
|
|
7168
|
+
ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
|
|
7169
|
+
ctx.fill();
|
|
7170
|
+
ctx.strokeStyle = stroke ?? fill;
|
|
7171
|
+
ctx.lineWidth = this.view === "preview" ? Math.max(1, Math.min(1.75, size * 0.13)) : Math.max(1, Math.min(1.5, size * 0.1));
|
|
7172
|
+
ctx.stroke();
|
|
7173
|
+
if (this.view === "preview" && eligible?.has(seat.label) && size >= 22) {
|
|
7174
|
+
this.paintPreviewSeatLabel(ctx, seat.label, point.x, point.y, radius);
|
|
7175
|
+
}
|
|
7176
|
+
} else {
|
|
7177
|
+
ctx.globalAlpha = 0.85;
|
|
7178
|
+
ctx.fillRect(point.x - half, point.y - half, size, size);
|
|
7179
|
+
}
|
|
7069
7180
|
}
|
|
7070
7181
|
ctx.globalAlpha = 1;
|
|
7182
|
+
if (previewSections) this.paintPreviewSectionLabels(ctx, previewSections);
|
|
7183
|
+
this.paintSectionTargets(sectionTargets);
|
|
7071
7184
|
this.paintFlags(clusters);
|
|
7072
7185
|
}
|
|
7186
|
+
/** Draw an eligible seat's actual chart label without inventing a new buyer
|
|
7187
|
+
* identifier. Long labels scale down and are omitted rather than overflowing
|
|
7188
|
+
* into an adjacent seat. */
|
|
7189
|
+
paintPreviewSeatLabel(ctx, label, x, y, radius) {
|
|
7190
|
+
const maxWidth = radius * 1.55;
|
|
7191
|
+
let fontSize = Math.min(13, Math.max(7, radius * 0.55));
|
|
7192
|
+
const minFontSize = 6;
|
|
7193
|
+
while (fontSize >= minFontSize) {
|
|
7194
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7195
|
+
if (ctx.measureText(label).width <= maxWidth) break;
|
|
7196
|
+
fontSize -= 0.5;
|
|
7197
|
+
}
|
|
7198
|
+
if (fontSize < minFontSize) return;
|
|
7199
|
+
ctx.fillStyle = "#ffffff";
|
|
7200
|
+
ctx.textAlign = "center";
|
|
7201
|
+
ctx.textBaseline = "middle";
|
|
7202
|
+
ctx.fillText(label, x, y);
|
|
7203
|
+
}
|
|
7204
|
+
/** A section overview is a navigation map. These transparent, keyboardable
|
|
7205
|
+
* hit areas sit over the renderer's section shells so both mouse and keyboard
|
|
7206
|
+
* always take the organizer into the real focused-section camera state. */
|
|
7207
|
+
paintSectionTargets(sections) {
|
|
7208
|
+
const layer = this.layer;
|
|
7209
|
+
if (!layer) return;
|
|
7210
|
+
layer.querySelectorAll(".slm-ch-section-target").forEach((el) => el.remove());
|
|
7211
|
+
if (!sections) return;
|
|
7212
|
+
for (const [id, section] of sections) {
|
|
7213
|
+
const width = section.maxX - section.minX;
|
|
7214
|
+
const height = section.maxY - section.minY;
|
|
7215
|
+
if (width < 20 || height < 20) continue;
|
|
7216
|
+
const target = document.createElement("button");
|
|
7217
|
+
target.type = "button";
|
|
7218
|
+
target.className = "slm-ch-section-target";
|
|
7219
|
+
target.style.left = `${section.minX - 8}px`;
|
|
7220
|
+
target.style.top = `${section.minY - 8}px`;
|
|
7221
|
+
target.style.width = `${width + 16}px`;
|
|
7222
|
+
target.style.height = `${height + 16}px`;
|
|
7223
|
+
target.setAttribute("aria-label", `Open ${section.label} seats`);
|
|
7224
|
+
target.addEventListener("click", () => {
|
|
7225
|
+
this.focusedSectionId = id;
|
|
7226
|
+
this.host.focusSection(id);
|
|
7227
|
+
this.paintRail();
|
|
7228
|
+
});
|
|
7229
|
+
layer.appendChild(target);
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
/** Keep renderer section names legible over a dense, zoomed-out preview. */
|
|
7233
|
+
paintPreviewSectionLabels(ctx, sections) {
|
|
7234
|
+
for (const section of sections.values()) {
|
|
7235
|
+
const width = section.maxX - section.minX;
|
|
7236
|
+
const height = section.maxY - section.minY;
|
|
7237
|
+
if (width < 52 || height < 26) continue;
|
|
7238
|
+
const centerX = (section.minX + section.maxX) / 2;
|
|
7239
|
+
const centerY = (section.minY + section.maxY) / 2;
|
|
7240
|
+
const fontSize = Math.max(11, Math.min(15, height * 0.16));
|
|
7241
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7242
|
+
const labelWidth = Math.min(width - 8, ctx.measureText(section.label).width + 18);
|
|
7243
|
+
const labelHeight = fontSize + 10;
|
|
7244
|
+
ctx.fillStyle = "rgba(11, 16, 28, .88)";
|
|
7245
|
+
ctx.fillRect(centerX - labelWidth / 2, centerY - labelHeight / 2, labelWidth, labelHeight);
|
|
7246
|
+
ctx.fillStyle = "#f8fafc";
|
|
7247
|
+
ctx.textAlign = "center";
|
|
7248
|
+
ctx.textBaseline = "middle";
|
|
7249
|
+
ctx.fillText(section.label, centerX, centerY);
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
7073
7252
|
/** Letter flags at each channel's centroid — the non-color identity cue. */
|
|
7074
7253
|
paintFlags(clusters) {
|
|
7075
7254
|
const layer = this.layer;
|
|
@@ -7135,7 +7314,7 @@ var ChannelsMode = class {
|
|
|
7135
7314
|
});
|
|
7136
7315
|
});
|
|
7137
7316
|
}
|
|
7138
|
-
setBanner(on, name = "") {
|
|
7317
|
+
setBanner(on, name = "", eligibleSeats) {
|
|
7139
7318
|
const banner = this.bannerEl;
|
|
7140
7319
|
if (!banner) return;
|
|
7141
7320
|
this.host.root.classList.toggle("ch-preview", on);
|
|
@@ -7145,8 +7324,9 @@ var ChannelsMode = class {
|
|
|
7145
7324
|
return;
|
|
7146
7325
|
}
|
|
7147
7326
|
const marker = this.previewAudience.length === 1 ? this.markerFor(this.previewAudience[0]) : { color: "var(--slm-accent)", letter: "" };
|
|
7327
|
+
const availability = eligibleSeats == null ? "" : ` \xB7 ${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat" : "seats"} available now`;
|
|
7148
7328
|
banner.innerHTML = `<span class="dot" style="background:${esc(marker.color)}"></span>
|
|
7149
|
-
Previewing buyer access \xB7 ${esc(name)} \xB7 read-only
|
|
7329
|
+
Previewing buyer access \xB7 ${esc(name)}${availability} \xB7 read-only
|
|
7150
7330
|
<button type="button" data-ch-act="exit-preview">Exit preview</button>`;
|
|
7151
7331
|
banner.classList.add("on");
|
|
7152
7332
|
banner.querySelector('[data-ch-act="exit-preview"]')?.addEventListener("click", () => this.setView("inspect"));
|
|
@@ -7181,6 +7361,8 @@ var ChannelsMode = class {
|
|
|
7181
7361
|
includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
|
|
7182
7362
|
});
|
|
7183
7363
|
this.previewSupported = true;
|
|
7364
|
+
const eligibleSeats = this.previewProjection.available === false ? void 0 : this.previewProjection.counts?.eligible ?? this.previewProjection.eligible?.length;
|
|
7365
|
+
this.setBanner(true, names, eligibleSeats);
|
|
7184
7366
|
} catch (err) {
|
|
7185
7367
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7186
7368
|
this.previewSupported = !(status === 404 || status === 405 || status === 501);
|
|
@@ -7231,6 +7413,22 @@ var ChannelsMode = class {
|
|
|
7231
7413
|
aria-pressed="${this.view === "inspect"}">Inspect allocation</button>
|
|
7232
7414
|
<button type="button" class="${previewOn.trim()}" data-ch-view="preview"
|
|
7233
7415
|
aria-pressed="${this.view === "preview"}">Preview buyer access</button>
|
|
7416
|
+
</div>${this.mapNavigationHtml()}`;
|
|
7417
|
+
}
|
|
7418
|
+
mapNavigationHtml() {
|
|
7419
|
+
if (this.host.sections().length < 2) return "";
|
|
7420
|
+
const focused = this.focusedSectionId ? this.host.sections().find((section) => section.id === this.focusedSectionId)?.label ?? "section" : null;
|
|
7421
|
+
const panOn = this.mapIntent === "pan" ? " on" : "";
|
|
7422
|
+
const assignOn = this.mapIntent === "assign" ? " on" : "";
|
|
7423
|
+
const intent = this.view === "inspect" && this.caps.manage ? `<div class="slm-ch-viewseg" role="group" aria-label="Map interaction">
|
|
7424
|
+
<button type="button" class="${panOn.trim()}" data-ch-map="pan" aria-pressed="${this.mapIntent === "pan"}">Pan map</button>
|
|
7425
|
+
<button type="button" class="${assignOn.trim()}" data-ch-map="assign" aria-pressed="${this.mapIntent === "assign"}">Assign seats</button>
|
|
7426
|
+
</div>
|
|
7427
|
+
<p>${this.mapIntent === "pan" ? "Drag to explore. Click a section to open its seats." : "Drag across seats to select them for allocation."}</p>` : "<p>Drag to explore. Click a section to open its seats.</p>";
|
|
7428
|
+
return `<div class="slm-ch-mapnav">
|
|
7429
|
+
<div class="slm-ch-mapnav-head"><span>${focused ? `Viewing ${esc(focused)}` : "Section overview"}</span>
|
|
7430
|
+
<button type="button" data-ch-act="sections">All sections</button></div>
|
|
7431
|
+
${intent}
|
|
7234
7432
|
</div>`;
|
|
7235
7433
|
}
|
|
7236
7434
|
countsHtml(counts, key) {
|
|
@@ -7369,6 +7567,8 @@ var ChannelsMode = class {
|
|
|
7369
7567
|
*/
|
|
7370
7568
|
async loadLinks(channelId) {
|
|
7371
7569
|
if (!this.caps.view) return;
|
|
7570
|
+
const seq = ++this.linksSeq;
|
|
7571
|
+
const superseded = () => seq !== this.linksSeq || this.linksChannelId !== channelId;
|
|
7372
7572
|
if (this.linksChannelId !== channelId) {
|
|
7373
7573
|
this.links = [];
|
|
7374
7574
|
this.linksChannelId = channelId;
|
|
@@ -7376,11 +7576,11 @@ var ChannelsMode = class {
|
|
|
7376
7576
|
}
|
|
7377
7577
|
try {
|
|
7378
7578
|
const res = await this.host.api.accessLinks(this.host.eventKey, channelId);
|
|
7379
|
-
if (
|
|
7579
|
+
if (superseded()) return;
|
|
7380
7580
|
this.links = res.links ?? [];
|
|
7381
7581
|
this.linksState = "ready";
|
|
7382
7582
|
} catch (err) {
|
|
7383
|
-
if (
|
|
7583
|
+
if (superseded()) return;
|
|
7384
7584
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7385
7585
|
this.links = [];
|
|
7386
7586
|
this.linksState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
|
|
@@ -7457,9 +7657,9 @@ var ChannelsMode = class {
|
|
|
7457
7657
|
const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
|
|
7458
7658
|
<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")}.
|
|
7459
7659
|
A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
|
|
7460
|
-
const
|
|
7461
|
-
const summary =
|
|
7462
|
-
|
|
7660
|
+
const eligibleSeats = this.previewProjection?.counts?.eligible ?? this.previewProjection?.eligible?.length;
|
|
7661
|
+
const summary = eligibleSeats != null && this.previewProjection?.available !== false ? `<div class="slm-ch-alert info"><span>\u2713</span><span><b>${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat is" : "seats are"} available now.</b>
|
|
7662
|
+
This is the exact buyer-visible allocation.${this.previewProjection?.includePublic === false ? " Public sale seats are <b>not</b> included in this access." : ""}</span></div>` : "";
|
|
7463
7663
|
const includePublic = isPublicChannelId(current) ? "" : `
|
|
7464
7664
|
<label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:10px 0">
|
|
7465
7665
|
<input type="checkbox" data-ch-includepublic ${this.previewIncludePublic ? "checked" : ""} />
|
|
@@ -7490,6 +7690,13 @@ var ChannelsMode = class {
|
|
|
7490
7690
|
rail.querySelectorAll("[data-ch-view]").forEach((button) => {
|
|
7491
7691
|
button.addEventListener("click", () => this.setView(button.dataset.chView));
|
|
7492
7692
|
});
|
|
7693
|
+
rail.querySelectorAll("[data-ch-map]").forEach((button) => {
|
|
7694
|
+
button.addEventListener("click", () => {
|
|
7695
|
+
this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
|
|
7696
|
+
this.paintRail();
|
|
7697
|
+
this.onInteractionChange?.();
|
|
7698
|
+
});
|
|
7699
|
+
});
|
|
7493
7700
|
rail.querySelectorAll("[data-ch-detail]").forEach((button) => {
|
|
7494
7701
|
button.addEventListener("click", () => {
|
|
7495
7702
|
const channelId = button.dataset.chDetail;
|
|
@@ -7540,6 +7747,11 @@ var ChannelsMode = class {
|
|
|
7540
7747
|
}
|
|
7541
7748
|
railAction(action) {
|
|
7542
7749
|
switch (action) {
|
|
7750
|
+
case "sections":
|
|
7751
|
+
this.focusedSectionId = null;
|
|
7752
|
+
this.host.showSectionOverview();
|
|
7753
|
+
this.paintRail();
|
|
7754
|
+
break;
|
|
7543
7755
|
case "create":
|
|
7544
7756
|
this.openDialog({ kind: "create" });
|
|
7545
7757
|
break;
|
|
@@ -7714,7 +7926,7 @@ var ChannelsMode = class {
|
|
|
7714
7926
|
<label>Marker</label>
|
|
7715
7927
|
<div style="display:flex;gap:8px;align-items:center">
|
|
7716
7928
|
<span class="slm-ch-mk" data-ch-marker style="background:${esc(suggestion.color)};width:28px;height:28px;font-size:13px">${esc(suggestion.letter)}</span>
|
|
7717
|
-
<span class="slm-note" style="margin:0">Letter
|
|
7929
|
+
<span class="slm-note" style="margin:0">Letter comes from the name; colour is chosen automatically from the next available palette. Buyers never see either.</span>
|
|
7718
7930
|
</div>
|
|
7719
7931
|
</div>
|
|
7720
7932
|
<div class="slm-field">
|
|
@@ -8065,6 +8277,24 @@ var ChannelsMode = class {
|
|
|
8065
8277
|
await this.loadLinks(channelId);
|
|
8066
8278
|
this.paintRail();
|
|
8067
8279
|
}
|
|
8280
|
+
/**
|
|
8281
|
+
* The reload EVERY link mutation owes the panel.
|
|
8282
|
+
*
|
|
8283
|
+
* A create/rotate/revoke changes two things the detail panel renders: the
|
|
8284
|
+
* channel's access line (the server sets `access.intent` on create, and clears
|
|
8285
|
+
* it when the last live link goes) and the link status list. Both are re-read
|
|
8286
|
+
* here and the rail repainted, so the panel the organizer is already looking
|
|
8287
|
+
* at is current the moment the mutation lands — no reload, and no dependence
|
|
8288
|
+
* on HOW the one-time reveal was dismissed (the button, Escape, or never).
|
|
8289
|
+
*/
|
|
8290
|
+
async reloadAfterLinkChange(channelId) {
|
|
8291
|
+
if (this.detailChannelId === channelId) {
|
|
8292
|
+
await this.refresh({ quiet: true });
|
|
8293
|
+
return;
|
|
8294
|
+
}
|
|
8295
|
+
await this.loadLinks(channelId);
|
|
8296
|
+
if (this.active) this.paintRail();
|
|
8297
|
+
}
|
|
8068
8298
|
linkById(linkId) {
|
|
8069
8299
|
return this.links.find((link) => link.id === linkId) ?? null;
|
|
8070
8300
|
}
|
|
@@ -8158,7 +8388,7 @@ var ChannelsMode = class {
|
|
|
8158
8388
|
try {
|
|
8159
8389
|
const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
|
|
8160
8390
|
this.revealLink(reveal, { channelId });
|
|
8161
|
-
await this.
|
|
8391
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8162
8392
|
} catch (err) {
|
|
8163
8393
|
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8164
8394
|
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
@@ -8216,9 +8446,6 @@ var ChannelsMode = class {
|
|
|
8216
8446
|
}
|
|
8217
8447
|
selectSecret(dialog);
|
|
8218
8448
|
});
|
|
8219
|
-
dialog.querySelector("[data-ch-lk-done]")?.addEventListener("click", () => {
|
|
8220
|
-
void this.loadLinks(opts.channelId).then(() => this.paintRail());
|
|
8221
|
-
});
|
|
8222
8449
|
});
|
|
8223
8450
|
this.announce("Your hosted access link is ready and is shown once.");
|
|
8224
8451
|
}
|
|
@@ -8284,6 +8511,7 @@ var ChannelsMode = class {
|
|
|
8284
8511
|
endActiveSessions
|
|
8285
8512
|
);
|
|
8286
8513
|
this.revealLink(reveal, { channelId, rotated: true });
|
|
8514
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8287
8515
|
} catch (err) {
|
|
8288
8516
|
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8289
8517
|
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
@@ -8326,9 +8554,7 @@ var ChannelsMode = class {
|
|
|
8326
8554
|
endActiveSessions
|
|
8327
8555
|
);
|
|
8328
8556
|
this.closeDialog();
|
|
8329
|
-
await this.
|
|
8330
|
-
await this.refresh({ quiet: true });
|
|
8331
|
-
this.paintRail();
|
|
8557
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8332
8558
|
this.host.toast(res.endedSessions ? `Link revoked. ${res.endedSessions.toLocaleString()} buyer${res.endedSessions === 1 ? "" : "s"} lost access.` : "Link revoked. It no longer opens for anyone.", "ok");
|
|
8333
8559
|
} catch (err) {
|
|
8334
8560
|
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
@@ -8413,9 +8639,17 @@ var LEGEND = [
|
|
|
8413
8639
|
{ key: "booked", label: "Booked", color: "#22a06b" },
|
|
8414
8640
|
{ key: "blocked", label: "Blocked", color: "#8b94ac" }
|
|
8415
8641
|
];
|
|
8416
|
-
var
|
|
8642
|
+
var MANAGER_CSS = `
|
|
8417
8643
|
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
|
|
8418
|
-
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius)
|
|
8644
|
+
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
8645
|
+
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
8646
|
+
borrowed from CHANNELS_CSS. The base cockpit animates whether or not
|
|
8647
|
+
Channels mode is in use, so owning its own tokens is what stops a token
|
|
8648
|
+
edit from silently changing only half the surface. Channels mode declares
|
|
8649
|
+
the identical values so an embed of it stays self-contained. */
|
|
8650
|
+
--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
8651
|
+
--slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
|
|
8652
|
+
--slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
|
|
8419
8653
|
.slm *{box-sizing:border-box;margin:0;padding:0}
|
|
8420
8654
|
.slm button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
|
8421
8655
|
.slm input{font:inherit}
|
|
@@ -8428,7 +8662,8 @@ var CSS2 = `
|
|
|
8428
8662
|
.slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
8429
8663
|
.slm-live{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.12em;font-weight:800;color:var(--slm-muted)}
|
|
8430
8664
|
.slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
|
|
8431
|
-
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
8665
|
+
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
8666
|
+
animation:slm-pulse var(--slm-mo-ambient) infinite}
|
|
8432
8667
|
@keyframes slm-pulse{0%{box-shadow:0 0 0 0 rgba(34,160,107,.5)}70%{box-shadow:0 0 0 7px rgba(34,160,107,0)}100%{box-shadow:0 0 0 0 rgba(34,160,107,0)}}
|
|
8433
8668
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
8434
8669
|
border-top:1px solid var(--slm-line)}
|
|
@@ -8437,7 +8672,11 @@ var CSS2 = `
|
|
|
8437
8672
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
8438
8673
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
8439
8674
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
8440
|
-
|
|
8675
|
+
/* The one playful moment this surface is allowed (\xA72 --mo-spring). It ran at
|
|
8676
|
+
.58s against a 200ms catalog, which read as a different design language from
|
|
8677
|
+
the dashboard tile it mirrors; Channels mode's own count bump is the same
|
|
8678
|
+
pattern and must stay in step with it. */
|
|
8679
|
+
.slm-kpi.changed b{animation:slm-kpi-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
8441
8680
|
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
8442
8681
|
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
8443
8682
|
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
@@ -8456,12 +8695,14 @@ var CSS2 = `
|
|
|
8456
8695
|
.slm-hud-chip{padding:6px 11px;border-radius:999px;font-size:12px;font-weight:700;background:var(--slm-surface);
|
|
8457
8696
|
border:1px solid var(--slm-line);color:var(--slm-text)}
|
|
8458
8697
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
8459
|
-
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
8698
|
+
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
8699
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
8460
8700
|
.slm-zoomhint.on{opacity:1}
|
|
8461
8701
|
.slm-liveevent{position:absolute;left:50%;top:14px;z-index:4;display:flex;align-items:center;gap:8px;max-width:min(560px,calc(100% - 32px));
|
|
8462
8702
|
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
8463
8703
|
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
8464
|
-
transition:opacity
|
|
8704
|
+
transition:opacity var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-base) var(--slm-mo-out);
|
|
8705
|
+
backdrop-filter:blur(10px)}
|
|
8465
8706
|
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
8466
8707
|
.slm.block-mode .slm-liveevent{top:52px}
|
|
8467
8708
|
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
@@ -8481,7 +8722,8 @@ var CSS2 = `
|
|
|
8481
8722
|
/* activity feed */
|
|
8482
8723
|
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
8483
8724
|
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
8484
|
-
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
8725
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
8726
|
+
animation:slm-in var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8485
8727
|
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
8486
8728
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
8487
8729
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
@@ -8548,7 +8790,8 @@ var CSS2 = `
|
|
|
8548
8790
|
|
|
8549
8791
|
/* toast */
|
|
8550
8792
|
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
8551
|
-
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
8793
|
+
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
8794
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out);
|
|
8552
8795
|
background:var(--slm-surface);color:var(--slm-text);border:1px solid var(--slm-line);z-index:5}
|
|
8553
8796
|
.slm-toast.on{opacity:1}
|
|
8554
8797
|
.slm-toast.err{background:#c0392b;color:#fff;border-color:#c0392b}
|
|
@@ -8559,7 +8802,9 @@ var CSS2 = `
|
|
|
8559
8802
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
8560
8803
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
8561
8804
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
8562
|
-
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
8805
|
+
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
8806
|
+
background:var(--slm-surface)!important;text-align:left!important;
|
|
8807
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8563
8808
|
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
8564
8809
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
8565
8810
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
@@ -8578,7 +8823,8 @@ var CSS2 = `
|
|
|
8578
8823
|
.slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
|
|
8579
8824
|
/* sections: availability windows */
|
|
8580
8825
|
.slm-availlist{display:flex;flex-direction:column;gap:8px;margin:2px 0 12px}
|
|
8581
|
-
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
8826
|
+
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
8827
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),opacity var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8582
8828
|
.slm-availrow.zone{background:color-mix(in srgb,var(--slm-surface) 82%,#000)}
|
|
8583
8829
|
.slm-availrow.hidden{opacity:.62}.slm-availrow.closed{opacity:.82}
|
|
8584
8830
|
.slm-availhead{display:flex;align-items:center;gap:8px}
|
|
@@ -8618,16 +8864,29 @@ var CSS2 = `
|
|
|
8618
8864
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
8619
8865
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
8620
8866
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
8867
|
+
/* Reduced motion, as a BLANKET over the cockpit subtree rather than a list of
|
|
8868
|
+
selectors. The list this replaces named four animations and two transitions,
|
|
8869
|
+
and had silently fallen behind the stylesheet: the zoom hint, the toast and
|
|
8870
|
+
the availability rows all still animated for a user who had asked the OS for
|
|
8871
|
+
none. An enumerated list has to be edited every time a rule is added, and
|
|
8872
|
+
nothing fails when it isn't \u2014 so it drifts. This cannot.
|
|
8873
|
+
|
|
8874
|
+
Motion is removed, never the information it carried: Channels mode's own
|
|
8875
|
+
block substitutes static outlines for its shake and success states, and it
|
|
8876
|
+
stays authoritative for those. No JS here waits on animationend or
|
|
8877
|
+
transitionend, so cutting them outright strands no state. */
|
|
8621
8878
|
@media (prefers-reduced-motion:reduce){
|
|
8622
|
-
.slm
|
|
8623
|
-
|
|
8879
|
+
.slm,.slm *,.slm *::before,.slm *::after{
|
|
8880
|
+
animation:none!important;
|
|
8881
|
+
transition:none!important;
|
|
8882
|
+
scroll-behavior:auto!important}
|
|
8624
8883
|
}
|
|
8625
8884
|
${CHANNELS_CSS}`;
|
|
8626
8885
|
function injectStyle() {
|
|
8627
8886
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
8628
8887
|
const el = document.createElement("style");
|
|
8629
8888
|
el.id = STYLE_ID2;
|
|
8630
|
-
el.textContent =
|
|
8889
|
+
el.textContent = MANAGER_CSS;
|
|
8631
8890
|
document.head.appendChild(el);
|
|
8632
8891
|
}
|
|
8633
8892
|
function themeVars(theme) {
|
|
@@ -8687,6 +8946,11 @@ var SeatManager = class {
|
|
|
8687
8946
|
this.reconnectTimer = null;
|
|
8688
8947
|
this.attempt = 0;
|
|
8689
8948
|
this.closed = false;
|
|
8949
|
+
/** Mirrors the `live` root class, so the getter never has to read the DOM. */
|
|
8950
|
+
this.connectionStatus = "reconnecting";
|
|
8951
|
+
/** When the server last told us something. Stamped on accepted traffic only —
|
|
8952
|
+
* a socket that opens and says nothing has not refreshed anything. */
|
|
8953
|
+
this.lastMessageAt = null;
|
|
8690
8954
|
this.ready = false;
|
|
8691
8955
|
this.feed = [];
|
|
8692
8956
|
this.feedTimer = null;
|
|
@@ -8896,6 +9160,12 @@ var SeatManager = class {
|
|
|
8896
9160
|
},
|
|
8897
9161
|
worldToScreen: (point) => this.renderer?.worldToScreen(point) ?? null,
|
|
8898
9162
|
seatPixelSize: () => this.seatPixelSize(),
|
|
9163
|
+
isSeatDetail: () => this.renderer?.getRung?.() === "seats",
|
|
9164
|
+
showSectionOverview: () => {
|
|
9165
|
+
this.renderer?.clearSectionFocus();
|
|
9166
|
+
this.renderer?.setRung?.("sections");
|
|
9167
|
+
},
|
|
9168
|
+
focusSection: (sectionId) => this.renderer?.focusSection(sectionId),
|
|
8899
9169
|
isCompact: () => !!this.root?.classList.contains("compact"),
|
|
8900
9170
|
setMapInert: (inert) => {
|
|
8901
9171
|
this.mapHost.toggleAttribute("inert", inert);
|
|
@@ -8905,13 +9175,15 @@ var SeatManager = class {
|
|
|
8905
9175
|
onError: (err) => this.opts.onError?.(err)
|
|
8906
9176
|
};
|
|
8907
9177
|
}
|
|
8908
|
-
/**
|
|
8909
|
-
*
|
|
9178
|
+
/** Actual on-screen seat diameter, for the channel overlay's marks. The
|
|
9179
|
+
* renderer's base seat radius is 9 chart units; retaining the camera scale
|
|
9180
|
+
* (rather than capping it) keeps every preview paint aligned with the real
|
|
9181
|
+
* chart geometry at deep zoom. */
|
|
8910
9182
|
seatPixelSize() {
|
|
8911
9183
|
const rect = this.renderer?.getVisibleWorldRect?.();
|
|
8912
9184
|
const width = this.mapHost?.clientWidth ?? 0;
|
|
8913
9185
|
if (!rect?.width || !width) return 6;
|
|
8914
|
-
return Math.max(3,
|
|
9186
|
+
return Math.max(3, width / rect.width * 18);
|
|
8915
9187
|
}
|
|
8916
9188
|
/** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
|
|
8917
9189
|
setHeatOverlay(enabled) {
|
|
@@ -9087,6 +9359,16 @@ var SeatManager = class {
|
|
|
9087
9359
|
getControlRoomSnapshot(windowMinutes = this.trendWindowMinutes) {
|
|
9088
9360
|
return this.setTrendWindow(windowMinutes);
|
|
9089
9361
|
}
|
|
9362
|
+
/**
|
|
9363
|
+
* The realtime link's current state and the "as of" behind it.
|
|
9364
|
+
*
|
|
9365
|
+
* Pair with `onConnectionChange` for the edges: a host that mounts after a
|
|
9366
|
+
* drop, or re-reads on tab focus, needs to be able to ASK rather than wait
|
|
9367
|
+
* for the next transition that may never come.
|
|
9368
|
+
*/
|
|
9369
|
+
getConnection() {
|
|
9370
|
+
return { status: this.connectionStatus, lastMessageAt: this.lastMessageAt };
|
|
9371
|
+
}
|
|
9090
9372
|
getLog(opts = {}) {
|
|
9091
9373
|
return this.api.log(this.key, opts);
|
|
9092
9374
|
}
|
|
@@ -9151,6 +9433,10 @@ var SeatManager = class {
|
|
|
9151
9433
|
onSelect: (seat) => this.handleSeatSelect(seat),
|
|
9152
9434
|
onDeselect: () => this.syncSelection(),
|
|
9153
9435
|
onMarquee: () => this.syncSelection(),
|
|
9436
|
+
onSectionTap: (sectionId) => {
|
|
9437
|
+
this.renderer?.focusSection(sectionId);
|
|
9438
|
+
this.channels?.handleSectionFocus(sectionId);
|
|
9439
|
+
},
|
|
9154
9440
|
onViewChange: () => {
|
|
9155
9441
|
this.updateZoomHint();
|
|
9156
9442
|
this.channels?.handleViewChange();
|
|
@@ -9161,10 +9447,11 @@ var SeatManager = class {
|
|
|
9161
9447
|
this.applyHeatOverlay();
|
|
9162
9448
|
this.updateZoomHint();
|
|
9163
9449
|
}
|
|
9164
|
-
/** Block
|
|
9165
|
-
*
|
|
9450
|
+
/** Block always uses a marquee. Channels only enables its marquee after the
|
|
9451
|
+
* organizer deliberately chooses Assign seats; Pan map keeps desktop drag
|
|
9452
|
+
* available for large charts. */
|
|
9166
9453
|
isBulkSelectMode() {
|
|
9167
|
-
return this.mode === "block" || this.mode === "channels" && this.channels?.
|
|
9454
|
+
return this.mode === "block" || this.mode === "channels" && this.channels?.usesMarqueeSelection() === true;
|
|
9168
9455
|
}
|
|
9169
9456
|
/**
|
|
9170
9457
|
* Block never touches held or booked inventory, so it cannot select it.
|
|
@@ -9174,7 +9461,7 @@ var SeatManager = class {
|
|
|
9174
9461
|
*/
|
|
9175
9462
|
selectableStatuses() {
|
|
9176
9463
|
if (this.mode === "block") return ["free", "not_for_sale"];
|
|
9177
|
-
if (this.mode === "inspect" || this.isBulkSelectMode()) {
|
|
9464
|
+
if (this.mode === "inspect" || this.isBulkSelectMode() || this.mode === "channels" && this.channels?.canSelect() === true) {
|
|
9178
9465
|
return ["free", "held", "booked", "not_for_sale"];
|
|
9179
9466
|
}
|
|
9180
9467
|
return [];
|
|
@@ -9277,6 +9564,7 @@ var SeatManager = class {
|
|
|
9277
9564
|
return;
|
|
9278
9565
|
}
|
|
9279
9566
|
if (!msg || typeof msg !== "object") return;
|
|
9567
|
+
this.lastMessageAt = Date.now();
|
|
9280
9568
|
const m = msg;
|
|
9281
9569
|
if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
|
|
9282
9570
|
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
@@ -9333,6 +9621,7 @@ var SeatManager = class {
|
|
|
9333
9621
|
const objs = await this.api.objects(this.key);
|
|
9334
9622
|
this.applySnapshot(objs.seats);
|
|
9335
9623
|
this.updateEffectiveAvailability(objs.hidden, objs.closed);
|
|
9624
|
+
this.lastMessageAt = Date.now();
|
|
9336
9625
|
} catch {
|
|
9337
9626
|
}
|
|
9338
9627
|
}
|
|
@@ -9807,6 +10096,14 @@ var SeatManager = class {
|
|
|
9807
10096
|
this.root?.classList.toggle("live", on);
|
|
9808
10097
|
if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
|
|
9809
10098
|
this.paintMonitorInsights();
|
|
10099
|
+
const next = on ? "live" : "reconnecting";
|
|
10100
|
+
if (next === this.connectionStatus) return;
|
|
10101
|
+
this.connectionStatus = next;
|
|
10102
|
+
try {
|
|
10103
|
+
this.opts.onConnectionChange?.(this.getConnection());
|
|
10104
|
+
} catch (err) {
|
|
10105
|
+
this.opts.onError?.(err);
|
|
10106
|
+
}
|
|
9810
10107
|
}
|
|
9811
10108
|
updateZoomHint() {
|
|
9812
10109
|
const hint = this.els.zoomhint;
|