@seatlayer/core 0.22.0 → 0.24.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/{de-KPTY7UOT.js → de-EIG65UFU.js} +5 -35
- package/dist/de-EIG65UFU.js.map +1 -0
- package/dist/{es-ALPR5FBB.js → es-47HFAWS6.js} +5 -35
- package/dist/es-47HFAWS6.js.map +1 -0
- package/dist/{fr-HVQLQD2N.js → fr-7IXNNFBS.js} +5 -35
- package/dist/fr-7IXNNFBS.js.map +1 -0
- package/dist/index.cjs +386 -174
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +119 -5
- package/dist/index.d.ts +119 -5
- package/dist/index.js +375 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/de-KPTY7UOT.js.map +0 -1
- package/dist/es-ALPR5FBB.js.map +0 -1
- package/dist/fr-HVQLQD2N.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -111,6 +111,53 @@ function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected
|
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// src/core/labeling.ts
|
|
115
|
+
var FULL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
116
|
+
var LOWER_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
|
|
117
|
+
function toBijectiveBase(index, alphabet) {
|
|
118
|
+
const base = alphabet.length;
|
|
119
|
+
let n = index + 1;
|
|
120
|
+
let out = "";
|
|
121
|
+
while (n > 0) {
|
|
122
|
+
n -= 1;
|
|
123
|
+
const rem = n % base;
|
|
124
|
+
out = alphabet[rem] + out;
|
|
125
|
+
n = Math.floor(n / base);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
function toLetters(value, lower = false) {
|
|
130
|
+
const alphabet = lower ? LOWER_ALPHABET : FULL_ALPHABET;
|
|
131
|
+
return toBijectiveBase(Math.max(0, Math.floor(value) - 1), alphabet);
|
|
132
|
+
}
|
|
133
|
+
var ROMAN_TABLE = [
|
|
134
|
+
[1e3, "M"],
|
|
135
|
+
[900, "CM"],
|
|
136
|
+
[500, "D"],
|
|
137
|
+
[400, "CD"],
|
|
138
|
+
[100, "C"],
|
|
139
|
+
[90, "XC"],
|
|
140
|
+
[50, "L"],
|
|
141
|
+
[40, "XL"],
|
|
142
|
+
[10, "X"],
|
|
143
|
+
[9, "IX"],
|
|
144
|
+
[5, "V"],
|
|
145
|
+
[4, "IV"],
|
|
146
|
+
[1, "I"]
|
|
147
|
+
];
|
|
148
|
+
function toRoman(value) {
|
|
149
|
+
if (!Number.isFinite(value) || value <= 0) return String(value);
|
|
150
|
+
let remaining = Math.floor(value);
|
|
151
|
+
let out = "";
|
|
152
|
+
for (const [n, sym] of ROMAN_TABLE) {
|
|
153
|
+
while (remaining >= n) {
|
|
154
|
+
out += sym;
|
|
155
|
+
remaining -= n;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
114
161
|
// src/core/layout.ts
|
|
115
162
|
function overrideAccessibility(o) {
|
|
116
163
|
if (!o) return [];
|
|
@@ -156,24 +203,62 @@ function overrideMap(row) {
|
|
|
156
203
|
if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
|
|
157
204
|
return m;
|
|
158
205
|
}
|
|
159
|
-
function
|
|
160
|
-
const
|
|
206
|
+
function centerRank(n) {
|
|
207
|
+
const rank = new Array(n);
|
|
208
|
+
Array.from({ length: n }, (_, i) => i).sort((a, b) => Math.abs(2 * a - (n - 1)) - Math.abs(2 * b - (n - 1)) || a - b).forEach((idx, k) => rank[idx] = k);
|
|
209
|
+
return rank;
|
|
210
|
+
}
|
|
211
|
+
function seatLabelPart(row, i) {
|
|
212
|
+
const rawStart = row.seatLabelStart ?? 1;
|
|
161
213
|
const dir = row.seatNumbering?.direction ?? "ltr";
|
|
162
214
|
const step = row.seatNumbering?.step ?? 1;
|
|
215
|
+
const scheme = row.seatNumbering?.scheme ?? "decimal";
|
|
216
|
+
const prefix = row.seatNumbering?.prefix ?? "";
|
|
217
|
+
const endAt = row.seatNumbering?.endAt;
|
|
163
218
|
const n = row.seatCount;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
219
|
+
if (scheme === "updown") {
|
|
220
|
+
const half = Math.ceil(n / 2);
|
|
221
|
+
const core2 = i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i);
|
|
222
|
+
return `${prefix}${core2}`;
|
|
223
|
+
}
|
|
224
|
+
const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
|
|
225
|
+
const start = endAt != null && Number.isFinite(endAt) ? endAt - (n - 1) * effStep : rawStart;
|
|
226
|
+
const p = dir === "center" ? centerRank(n)[i] : dir === "rtl" ? n - 1 - i : i;
|
|
227
|
+
let core;
|
|
228
|
+
switch (scheme) {
|
|
229
|
+
case "odd": {
|
|
230
|
+
const firstOdd = start % 2 === 1 ? start : start + 1;
|
|
231
|
+
core = String(firstOdd + p * 2);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "even": {
|
|
235
|
+
const firstEven = start % 2 === 0 ? start : start + 1;
|
|
236
|
+
core = String(firstEven + p * 2);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "roman":
|
|
240
|
+
core = toRoman(start + p * step);
|
|
241
|
+
break;
|
|
242
|
+
case "letters-upper":
|
|
243
|
+
core = toLetters(start + p * step, false);
|
|
244
|
+
break;
|
|
245
|
+
case "letters-lower":
|
|
246
|
+
core = toLetters(start + p * step, true);
|
|
247
|
+
break;
|
|
248
|
+
case "decimal":
|
|
249
|
+
default:
|
|
250
|
+
core = String(start + p * step);
|
|
251
|
+
break;
|
|
171
252
|
}
|
|
253
|
+
return `${prefix}${core}`;
|
|
254
|
+
}
|
|
255
|
+
function expandRowSlots(row) {
|
|
172
256
|
const ov = overrideMap(row);
|
|
173
257
|
return rowSeatPositions(row).map((p, i) => {
|
|
174
258
|
const o = ov.get(i);
|
|
175
259
|
const accessibility = overrideAccessibility(o);
|
|
176
|
-
const
|
|
260
|
+
const part = seatLabelPart(row, i);
|
|
261
|
+
const inventoryLabel = o?.label ?? `${row.label}-${part}`;
|
|
177
262
|
const displayPrefix = row.displayLabel ?? row.label;
|
|
178
263
|
const commercial = { ...row.commercial, ...o?.commercial };
|
|
179
264
|
return {
|
|
@@ -181,7 +266,7 @@ function expandRowSlots(row) {
|
|
|
181
266
|
x: p.x + (o?.dx ?? 0),
|
|
182
267
|
y: p.y + (o?.dy ?? 0),
|
|
183
268
|
label: inventoryLabel,
|
|
184
|
-
displayLabel: o?.displayLabel ?? `${displayPrefix}-${
|
|
269
|
+
displayLabel: o?.displayLabel ?? `${displayPrefix}-${part}`,
|
|
185
270
|
categoryKey: o?.categoryKey ?? row.categoryKey,
|
|
186
271
|
skipped: !!o?.skip,
|
|
187
272
|
accessible: accessibility.length > 0,
|
|
@@ -323,8 +408,8 @@ function polygonLabelPoint(outer, holes) {
|
|
|
323
408
|
const xs = outer.map((point) => point.x);
|
|
324
409
|
const ys = outer.map((point) => point.y);
|
|
325
410
|
const bounds = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
|
|
326
|
-
const
|
|
327
|
-
if (pointInPolygonWithHoles(
|
|
411
|
+
const centroid2 = polygonCentroid(outer);
|
|
412
|
+
if (pointInPolygonWithHoles(centroid2, outer, holes)) return centroid2;
|
|
328
413
|
let best = outer[0];
|
|
329
414
|
let bestScore = -Infinity;
|
|
330
415
|
const rings = [outer, ...holes ?? []];
|
|
@@ -648,6 +733,7 @@ import { Rect } from "konva/lib/shapes/Rect";
|
|
|
648
733
|
import { Ellipse } from "konva/lib/shapes/Ellipse";
|
|
649
734
|
import { Line } from "konva/lib/shapes/Line";
|
|
650
735
|
import { Text } from "konva/lib/shapes/Text";
|
|
736
|
+
import { Path } from "konva/lib/shapes/Path";
|
|
651
737
|
import { Image as KImage } from "konva/lib/shapes/Image";
|
|
652
738
|
import { Shape } from "konva/lib/Shape";
|
|
653
739
|
|
|
@@ -664,6 +750,8 @@ var LIGHT_BOOKABLE_LABEL_INK = "#ffffff";
|
|
|
664
750
|
function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
|
|
665
751
|
return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
|
|
666
752
|
}
|
|
753
|
+
var ACCESS_GLYPH_VIEWBOX = 24;
|
|
754
|
+
var ACCESS_GLYPH_PATH = "M12 2c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 7h-6v13h-2v-6h-2v6H9V9H3V7h18v2z";
|
|
667
755
|
function bookableMarkerLabel(publicLabel) {
|
|
668
756
|
return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
|
|
669
757
|
}
|
|
@@ -743,29 +831,17 @@ var en = {
|
|
|
743
831
|
"common.done": "Done",
|
|
744
832
|
"common.copied": "\u2713 Copied",
|
|
745
833
|
// buyer picker
|
|
746
|
-
"picker.holdSeats": "Hold seats & checkout",
|
|
747
|
-
"picker.completeBooking": "Complete booking",
|
|
748
|
-
"picker.seatsHeld": "Seats held \u2014 {time}",
|
|
749
834
|
"picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
|
|
750
|
-
"picker.seatTaken": "Seat {label} was just taken by another buyer.",
|
|
751
835
|
"picker.poweredBy": "Powered by SeatLayer",
|
|
752
836
|
"picker.testMode": "TEST MODE",
|
|
753
|
-
"picker.colorblind": "Colorblind-friendly colors",
|
|
754
837
|
"picker.orphanHint": "This leaves a single seat stranded \u2014 consider shifting one seat over.",
|
|
755
|
-
"picker.seats.one": "{count} seat",
|
|
756
|
-
"picker.seats.other": "{count} seats",
|
|
757
838
|
// renderer (drawn on the Konva map — shared by the embed SDK)
|
|
758
839
|
"map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
|
|
759
840
|
"map.seatsLeft": "{count} LEFT",
|
|
760
841
|
"map.fromPrice": "FROM {price}",
|
|
761
842
|
"map.statusHeld": "On hold",
|
|
762
843
|
"map.statusTaken": "Taken",
|
|
763
|
-
// buyer picker
|
|
764
|
-
"picker.language": "Language",
|
|
765
|
-
"picker.zoomToFit": "Zoom to fit",
|
|
766
|
-
"picker.seatCountLabel": "seats",
|
|
767
|
-
"picker.capacity": "capacity",
|
|
768
|
-
"picker.viewMode": "View mode",
|
|
844
|
+
// buyer picker widget (src/picker/widget/SeatPicker.ts)
|
|
769
845
|
"picker.floor": "Floor",
|
|
770
846
|
"picker.zoomLevel": "Zoom level",
|
|
771
847
|
"picker.rungTip.zones": "Venue overview \u2014 groups of sections such as North Stand or VIP",
|
|
@@ -780,37 +856,19 @@ var en = {
|
|
|
780
856
|
"picker.seatsLeftInSection.other": "{count} seats left",
|
|
781
857
|
"picker.overview": "Overview",
|
|
782
858
|
"picker.tapSeatHint": "Tap any seat to check its view",
|
|
783
|
-
"picker.chartSize": "Chart size",
|
|
784
|
-
"picker.custom": "Custom",
|
|
785
|
-
"picker.categories": "Categories",
|
|
786
|
-
"picker.accessibility": "Accessibility",
|
|
787
|
-
"picker.showAnyAccessible": "Show any accessible seat",
|
|
788
|
-
"picker.any": "Any",
|
|
789
|
-
"picker.yourSeats": "Your seats",
|
|
790
|
-
"picker.emptySeats": "Tap seats on the map",
|
|
791
|
-
"picker.emptySeatsWithGa": "Tap seats on the map \xB7 tap a standing area for GA tickets",
|
|
792
|
-
"picker.oneFewer": "One fewer",
|
|
793
|
-
"picker.oneMore": "One more",
|
|
794
|
-
"picker.remove": "Remove {label}",
|
|
795
859
|
"picker.ticketTierFor": "Ticket tier for {label}",
|
|
796
|
-
"picker.total": "Total: {amount}",
|
|
797
860
|
"picker.viewFromSeat": "View from seat {label}",
|
|
798
861
|
"picker.real360": "REAL 360\xB0",
|
|
799
862
|
"picker.preview": "PREVIEW",
|
|
800
|
-
"picker.open360": "Open 360\xB0 view",
|
|
801
863
|
"picker.sightline": "\u2248 {m} m to stage \xB7 clear sightline",
|
|
802
|
-
"picker.bookedDemo": "Booked! (demo)",
|
|
803
|
-
"picker.bookButton.one": "Book {count} ticket \u2014 {amount}",
|
|
804
|
-
"picker.bookButton.other": "Book {count} tickets \u2014 {amount}",
|
|
805
|
-
"picker.simulateCrowd": "Simulate crowd: {state}",
|
|
806
|
-
"picker.on": "ON",
|
|
807
|
-
"picker.off": "OFF",
|
|
808
864
|
"picker.panorama360": "360\xB0 venue photo",
|
|
809
865
|
"picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage",
|
|
810
866
|
"picker.restrictedView": "Restricted view",
|
|
811
867
|
"picker.obstructedView": "Obstructed view",
|
|
812
868
|
"picker.premiumSeat": "Premium seat",
|
|
813
|
-
"picker.hideLimitedView": "Hide limited-view seats"
|
|
869
|
+
"picker.hideLimitedView": "Hide limited-view seats",
|
|
870
|
+
"picker.bestSeatsPremium": "Best seats",
|
|
871
|
+
"picker.premiumFallbackNote": "No premium block of {count} \u2014 showing best overall"
|
|
814
872
|
};
|
|
815
873
|
|
|
816
874
|
// src/i18n/index.ts
|
|
@@ -861,6 +919,7 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
|
|
|
861
919
|
var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
|
|
862
920
|
var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
|
|
863
921
|
var SEAT_TAP_SLOP_PX = 14;
|
|
922
|
+
var SEAT_GLYPH_MIN_PX = 6.5;
|
|
864
923
|
var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
|
|
865
924
|
var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
|
|
866
925
|
var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
|
|
@@ -1150,6 +1209,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1150
1209
|
this.boothLabelById = /* @__PURE__ */ new Map();
|
|
1151
1210
|
/** Viewport seat labels are rebuilt after each settled camera change. */
|
|
1152
1211
|
this.seatLabelById = /* @__PURE__ */ new Map();
|
|
1212
|
+
/** Coloured accommodation ring per accessible seat (few per chart). */
|
|
1213
|
+
this.accessRingById = /* @__PURE__ */ new Map();
|
|
1214
|
+
/** Centred accessibility glyph per accessible seat — shown once the seat is
|
|
1215
|
+
* big enough on-screen (see {@link SEAT_GLYPH_MIN_PX}); the ring is the
|
|
1216
|
+
* smaller-zoom fallback. Kept in a map so zoom toggles touch only the handful
|
|
1217
|
+
* of accessible seats, never all 13k nodes. */
|
|
1218
|
+
this.accessGlyphById = /* @__PURE__ */ new Map();
|
|
1219
|
+
/** Whether the accessibility glyph is legible at the current camera scale. */
|
|
1220
|
+
this.accessGlyphVisible = false;
|
|
1153
1221
|
/** Authored free-text nodes obey the same rendered-size visibility floor. */
|
|
1154
1222
|
this.freeTextById = /* @__PURE__ */ new Map();
|
|
1155
1223
|
/** Stage/rink landmarks retain a readable screen-space caption at overview. */
|
|
@@ -1462,6 +1530,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
1462
1530
|
this.boothDims.clear();
|
|
1463
1531
|
this.boothLabelById.clear();
|
|
1464
1532
|
this.seatLabelById.clear();
|
|
1533
|
+
this.accessRingById.clear();
|
|
1534
|
+
this.accessGlyphById.clear();
|
|
1465
1535
|
this.freeTextById.clear();
|
|
1466
1536
|
this.primaryFocalLabels.clear();
|
|
1467
1537
|
this.gaById.clear();
|
|
@@ -2370,18 +2440,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2370
2440
|
this.paintSeat(c, seat.id);
|
|
2371
2441
|
target.add(c);
|
|
2372
2442
|
if (seat.accessible) {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
);
|
|
2443
|
+
const ring = new Circle({
|
|
2444
|
+
x: seat.x,
|
|
2445
|
+
y: seat.y,
|
|
2446
|
+
radius: this.seatR + 1.5,
|
|
2447
|
+
stroke: accessibilityRingColor(seat.accessibility),
|
|
2448
|
+
strokeWidth: 2.5,
|
|
2449
|
+
listening: false,
|
|
2450
|
+
perfectDrawEnabled: false,
|
|
2451
|
+
shadowForStrokeEnabled: false
|
|
2452
|
+
});
|
|
2453
|
+
this.accessRingById.set(seat.id, ring);
|
|
2454
|
+
target.add(ring);
|
|
2455
|
+
const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
|
|
2456
|
+
this.accessGlyphById.set(seat.id, glyph);
|
|
2457
|
+
target.add(glyph);
|
|
2385
2458
|
}
|
|
2386
2459
|
}
|
|
2387
2460
|
}
|
|
@@ -2425,6 +2498,53 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2425
2498
|
target.add(t2);
|
|
2426
2499
|
this.paintSeat(rect, seat.id);
|
|
2427
2500
|
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Build the centred accessibility glyph for a seat. Sized relative to the seat
|
|
2503
|
+
* radius (so it always fills the marker at any zoom) and given a contrast-aware
|
|
2504
|
+
* fill against the seat's paint — recomputed per state in {@link paintSeat}.
|
|
2505
|
+
*/
|
|
2506
|
+
buildAccessGlyph(seat, seatFill) {
|
|
2507
|
+
const k = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX;
|
|
2508
|
+
return new Path({
|
|
2509
|
+
x: seat.x,
|
|
2510
|
+
y: seat.y,
|
|
2511
|
+
data: ACCESS_GLYPH_PATH,
|
|
2512
|
+
offsetX: ACCESS_GLYPH_VIEWBOX / 2,
|
|
2513
|
+
offsetY: ACCESS_GLYPH_VIEWBOX / 2,
|
|
2514
|
+
scaleX: k,
|
|
2515
|
+
scaleY: k,
|
|
2516
|
+
fill: stateAwareBookableLabelInk(seatFill, "#ffffff"),
|
|
2517
|
+
listening: false,
|
|
2518
|
+
visible: this.accessGlyphVisible,
|
|
2519
|
+
perfectDrawEnabled: false,
|
|
2520
|
+
shadowForStrokeEnabled: false
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* Toggle the accessibility glyphs for the current camera scale: shown once the
|
|
2525
|
+
* effective on-screen seat radius clears {@link SEAT_GLYPH_MIN_PX}, otherwise
|
|
2526
|
+
* hidden so only the ring remains. Iterates the (small) accessible-seat set,
|
|
2527
|
+
* never the full node graph, so it is cheap to call on every view change.
|
|
2528
|
+
*/
|
|
2529
|
+
updateAccessGlyphs(scale) {
|
|
2530
|
+
if (!this.accessGlyphById.size) return;
|
|
2531
|
+
this.accessGlyphVisible = this.seatR * scale >= SEAT_GLYPH_MIN_PX;
|
|
2532
|
+
for (const [id, glyph] of this.accessGlyphById) {
|
|
2533
|
+
glyph.visible(this.accessGlyphVisible && this.accessGlyphEligible(id));
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Whether an accessible seat's glyph should show for its current status. It is
|
|
2538
|
+
* hidden on seats a buyer cannot take (sold, or another buyer's hold) where the
|
|
2539
|
+
* overlay status cue (diagonal mark / lock) carries the state instead — mirrors
|
|
2540
|
+
* the `unavailable` test in {@link updateLabels} so the two never collide.
|
|
2541
|
+
*/
|
|
2542
|
+
accessGlyphEligible(id) {
|
|
2543
|
+
const status = this.statusById.get(id) ?? "free";
|
|
2544
|
+
if (status === "booked") return false;
|
|
2545
|
+
if (status === "held" && !this.ownedHold.has(id) && !this.opts.manageMode) return false;
|
|
2546
|
+
return true;
|
|
2547
|
+
}
|
|
2428
2548
|
/** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
|
|
2429
2549
|
seatBaseColor(categoryKey) {
|
|
2430
2550
|
if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
|
|
@@ -2538,6 +2658,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
2538
2658
|
isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
|
|
2539
2659
|
);
|
|
2540
2660
|
}
|
|
2661
|
+
const accessGlyph = this.accessGlyphById.get(id);
|
|
2662
|
+
if (accessGlyph) {
|
|
2663
|
+
const fill = c.fill();
|
|
2664
|
+
accessGlyph.fill(stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", "#ffffff"));
|
|
2665
|
+
accessGlyph.opacity(c.opacity());
|
|
2666
|
+
accessGlyph.visible(this.accessGlyphVisible && this.accessGlyphEligible(id));
|
|
2667
|
+
}
|
|
2668
|
+
this.accessRingById.get(id)?.opacity(c.opacity());
|
|
2541
2669
|
}
|
|
2542
2670
|
/** True when a seat sits in a section/zone currently marked `closed`. */
|
|
2543
2671
|
seatInClosedSection(id) {
|
|
@@ -3016,7 +3144,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3016
3144
|
* Membership and category mix are still precomputed for the detailed state.
|
|
3017
3145
|
*/
|
|
3018
3146
|
renderSection(obj) {
|
|
3019
|
-
const
|
|
3147
|
+
const centroid2 = polygonLabelPoint(obj.outline, obj.holes);
|
|
3020
3148
|
const palette = overviewPalette(this.canvasBackground);
|
|
3021
3149
|
const memberIds = [];
|
|
3022
3150
|
const catCounts = /* @__PURE__ */ new Map();
|
|
@@ -3078,8 +3206,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3078
3206
|
const preferredInk = labelStyle?.color ?? palette.sectionInk;
|
|
3079
3207
|
const labelScale = (labelStyle?.size ?? 18) / 18;
|
|
3080
3208
|
const nameLabel = new Text({
|
|
3081
|
-
x: obj.labelPresentation?.position?.x ??
|
|
3082
|
-
y: obj.labelPresentation?.position?.y ??
|
|
3209
|
+
x: obj.labelPresentation?.position?.x ?? centroid2.x,
|
|
3210
|
+
y: obj.labelPresentation?.position?.y ?? centroid2.y,
|
|
3083
3211
|
text: obj.displayLabel ?? obj.label,
|
|
3084
3212
|
rotation: obj.labelPresentation?.rotation ?? 0,
|
|
3085
3213
|
visible: obj.labelPresentation?.visible !== false,
|
|
@@ -3094,8 +3222,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3094
3222
|
nameLabel.offsetY(nameLabel.height() / 2);
|
|
3095
3223
|
bgTarget.add(nameLabel);
|
|
3096
3224
|
const subLabel = new Text({
|
|
3097
|
-
x:
|
|
3098
|
-
y:
|
|
3225
|
+
x: centroid2.x,
|
|
3226
|
+
y: centroid2.y,
|
|
3099
3227
|
text: t("map.seatsLeft", { count: free }),
|
|
3100
3228
|
fontSize: 12,
|
|
3101
3229
|
fontStyle: "700",
|
|
@@ -3114,8 +3242,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
3114
3242
|
outline: obj.outline,
|
|
3115
3243
|
...obj.outlinePath ? { outlinePath: obj.outlinePath } : {},
|
|
3116
3244
|
holes: obj.holes ?? [],
|
|
3117
|
-
centroid,
|
|
3118
|
-
labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [],
|
|
3245
|
+
centroid: centroid2,
|
|
3246
|
+
labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid2),
|
|
3119
3247
|
zone: obj.zone,
|
|
3120
3248
|
memberIds,
|
|
3121
3249
|
total: memberIds.length,
|
|
@@ -4263,6 +4391,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4263
4391
|
if (this.hasSections) this.applySectionLod(scale);
|
|
4264
4392
|
else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
|
|
4265
4393
|
this.paintGAStateForView();
|
|
4394
|
+
this.updateAccessGlyphs(scale);
|
|
4266
4395
|
const shouldCache = scale < CACHE_THRESHOLD;
|
|
4267
4396
|
if (shouldCache && !this.cached) {
|
|
4268
4397
|
this.cacheSeatLayer();
|
|
@@ -4345,6 +4474,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
|
|
|
4345
4474
|
for (const seat of this.seats) {
|
|
4346
4475
|
if (seat.kind === "booth") continue;
|
|
4347
4476
|
if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
|
|
4477
|
+
if (seat.accessible && this.accessGlyphVisible) continue;
|
|
4348
4478
|
const shape = this.circleById.get(seat.id);
|
|
4349
4479
|
if ((shape?.opacity() ?? 1) < 0.5) continue;
|
|
4350
4480
|
const status = this.statusById.get(seat.id) ?? "free";
|
|
@@ -4475,6 +4605,103 @@ function strandedSingles(seats, statusOf, selectedIds) {
|
|
|
4475
4605
|
return out;
|
|
4476
4606
|
}
|
|
4477
4607
|
|
|
4608
|
+
// src/core/bestAvailable.ts
|
|
4609
|
+
function seatIndex2(seat, fallback) {
|
|
4610
|
+
const i = seat.id.lastIndexOf(":");
|
|
4611
|
+
if (i < 0) return fallback;
|
|
4612
|
+
const n = Number(seat.id.slice(i + 1));
|
|
4613
|
+
return Number.isFinite(n) ? n : fallback;
|
|
4614
|
+
}
|
|
4615
|
+
function dist2(a, b) {
|
|
4616
|
+
const dx = a.x - b.x;
|
|
4617
|
+
const dy = a.y - b.y;
|
|
4618
|
+
return dx * dx + dy * dy;
|
|
4619
|
+
}
|
|
4620
|
+
function centroid(seats) {
|
|
4621
|
+
let x = 0;
|
|
4622
|
+
let y = 0;
|
|
4623
|
+
for (const s of seats) {
|
|
4624
|
+
x += s.x;
|
|
4625
|
+
y += s.y;
|
|
4626
|
+
}
|
|
4627
|
+
return { x: x / seats.length, y: y / seats.length };
|
|
4628
|
+
}
|
|
4629
|
+
function isPremium(seat) {
|
|
4630
|
+
return seat.commercial?.premium === true;
|
|
4631
|
+
}
|
|
4632
|
+
function pickBestAvailable(seats, available, opts) {
|
|
4633
|
+
const qty = Math.floor(opts.qty);
|
|
4634
|
+
if (!Number.isFinite(qty) || qty <= 0) return { labels: [], reason: "sold_out" };
|
|
4635
|
+
const { categoryKey, focal, preferPremium } = opts;
|
|
4636
|
+
const rows = /* @__PURE__ */ new Map();
|
|
4637
|
+
const eligibleAll = [];
|
|
4638
|
+
seats.forEach((seat, i) => {
|
|
4639
|
+
const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey);
|
|
4640
|
+
let arr = rows.get(seat.rowId);
|
|
4641
|
+
if (!arr) {
|
|
4642
|
+
arr = [];
|
|
4643
|
+
rows.set(seat.rowId, arr);
|
|
4644
|
+
}
|
|
4645
|
+
arr.push({ seat, index: seatIndex2(seat, i), elig });
|
|
4646
|
+
if (elig) eligibleAll.push(seat);
|
|
4647
|
+
});
|
|
4648
|
+
if (eligibleAll.length === 0) return { labels: [], reason: "sold_out" };
|
|
4649
|
+
let best = null;
|
|
4650
|
+
const better = (c) => {
|
|
4651
|
+
if (!best) return true;
|
|
4652
|
+
if (preferPremium && c.nonPremium !== best.nonPremium) return c.nonPremium < best.nonPremium;
|
|
4653
|
+
if (c.orphan !== best.orphan) return c.orphan < best.orphan;
|
|
4654
|
+
if (c.d2 !== best.d2) return c.d2 < best.d2;
|
|
4655
|
+
const r = c.rowId.localeCompare(best.rowId);
|
|
4656
|
+
if (r !== 0) return r < 0;
|
|
4657
|
+
return c.startIndex < best.startIndex;
|
|
4658
|
+
};
|
|
4659
|
+
for (const [rowId, slotsUnsorted] of rows) {
|
|
4660
|
+
const slots = [...slotsUnsorted].sort((a, b) => a.index - b.index);
|
|
4661
|
+
let i = 0;
|
|
4662
|
+
while (i < slots.length) {
|
|
4663
|
+
if (!slots[i].elig) {
|
|
4664
|
+
i++;
|
|
4665
|
+
continue;
|
|
4666
|
+
}
|
|
4667
|
+
let j = i;
|
|
4668
|
+
while (j < slots.length && slots[j].elig) j++;
|
|
4669
|
+
const segLen = j - i;
|
|
4670
|
+
for (let p = 0; p + qty <= segLen; p++) {
|
|
4671
|
+
const leftRem = p;
|
|
4672
|
+
const rightRem = segLen - (p + qty);
|
|
4673
|
+
const orphan = leftRem === 1 || rightRem === 1 ? 1 : 0;
|
|
4674
|
+
const runSeats = slots.slice(i + p, i + p + qty).map((s) => s.seat);
|
|
4675
|
+
const c = {
|
|
4676
|
+
labels: runSeats.map((s) => s.label),
|
|
4677
|
+
seats: runSeats,
|
|
4678
|
+
rowId,
|
|
4679
|
+
startIndex: slots[i + p].index,
|
|
4680
|
+
orphan,
|
|
4681
|
+
nonPremium: preferPremium ? runSeats.reduce((n, s) => n + (isPremium(s) ? 0 : 1), 0) : 0,
|
|
4682
|
+
d2: dist2(centroid(runSeats), focal)
|
|
4683
|
+
};
|
|
4684
|
+
if (better(c)) best = c;
|
|
4685
|
+
}
|
|
4686
|
+
i = j;
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
if (best) return { labels: best.labels };
|
|
4690
|
+
if (eligibleAll.length < qty) return { labels: [], reason: "not_enough_together" };
|
|
4691
|
+
const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, focal) })).sort((a, b) => {
|
|
4692
|
+
if (preferPremium) {
|
|
4693
|
+
const pa = isPremium(a.seat) ? 0 : 1;
|
|
4694
|
+
const pb = isPremium(b.seat) ? 0 : 1;
|
|
4695
|
+
if (pa !== pb) return pa - pb;
|
|
4696
|
+
}
|
|
4697
|
+
if (a.d2 !== b.d2) return a.d2 - b.d2;
|
|
4698
|
+
const r = a.seat.rowId.localeCompare(b.seat.rowId);
|
|
4699
|
+
if (r !== 0) return r;
|
|
4700
|
+
return seatIndex2(a.seat, a.i) - seatIndex2(b.seat, b.i);
|
|
4701
|
+
});
|
|
4702
|
+
return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4478
4705
|
// src/picker/PickerController.ts
|
|
4479
4706
|
var DEFAULT_MAX_SELECTION = 10;
|
|
4480
4707
|
var MAX_BACKOFF_MS = 15e3;
|
|
@@ -4597,9 +4824,11 @@ var PickerController = class {
|
|
|
4597
4824
|
this.allIds.push(s.id);
|
|
4598
4825
|
const source = chartObjects.get(s.rowId);
|
|
4599
4826
|
const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
|
|
4600
|
-
const
|
|
4601
|
-
const
|
|
4602
|
-
const
|
|
4827
|
+
const sourceDisplayLabel = source && "displayLabel" in source && typeof source.displayLabel === "string" && source.displayLabel ? source.displayLabel : sourceLabel;
|
|
4828
|
+
const rowLabel = s.kind === "booth" ? void 0 : sourceDisplayLabel;
|
|
4829
|
+
const visibleSeatLabel = s.displayLabel ?? s.label;
|
|
4830
|
+
const labelParts = visibleSeatLabel.split("-");
|
|
4831
|
+
const seatNumber = sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) : s.kind === "booth" ? visibleSeatLabel : labelParts[labelParts.length - 1] ?? visibleSeatLabel;
|
|
4603
4832
|
this.seatContext.set(s.id, {
|
|
4604
4833
|
sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
|
|
4605
4834
|
rowLabel,
|
|
@@ -4826,10 +5055,77 @@ var PickerController = class {
|
|
|
4826
5055
|
this.setHold({ holdId: result.holdId, labels: combined.map((s) => s.label), expiresAt: result.expiresAt, items: result.items });
|
|
4827
5056
|
return this.hold_;
|
|
4828
5057
|
}
|
|
4829
|
-
/**
|
|
4830
|
-
|
|
5058
|
+
/** True when any seat on the visible chart carries the `premium` commercial
|
|
5059
|
+
* flag — gates the buyer widget's "★ Best seats" premium quick-pick (chip is
|
|
5060
|
+
* present-only, exactly like the accessibility filter chips). */
|
|
5061
|
+
hasPremiumSeats() {
|
|
5062
|
+
for (const seat of this.labelToSeat.values()) {
|
|
5063
|
+
if (seat.commercial?.premium) return true;
|
|
5064
|
+
}
|
|
5065
|
+
return false;
|
|
5066
|
+
}
|
|
5067
|
+
/**
|
|
5068
|
+
* Client-side premium pre-pass: find the best contiguous block of `qty`
|
|
5069
|
+
* PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
|
|
5070
|
+
* using the local seat geometry + live availability, mirroring the server's
|
|
5071
|
+
* held-back exclusion via the visible (hidden-stripped) chart and current
|
|
5072
|
+
* seat status. Returns a FULL premium block of `qty`, or null when none
|
|
5073
|
+
* exists (the caller then falls back to the normal server pick).
|
|
5074
|
+
*/
|
|
5075
|
+
pickPremiumBlock(qty, categoryKey) {
|
|
5076
|
+
const doc = this.visibleDoc();
|
|
5077
|
+
if (!doc?.objects?.length) return null;
|
|
5078
|
+
const focal = doc.focalPoint ?? { x: 0, y: 0 };
|
|
5079
|
+
const seats = expandChart(doc);
|
|
5080
|
+
const held = new Set(this.hold_?.labels ?? []);
|
|
5081
|
+
const available = /* @__PURE__ */ new Set();
|
|
5082
|
+
for (const seat of seats) {
|
|
5083
|
+
if (held.has(seat.label)) continue;
|
|
5084
|
+
const id = this.labelToId.get(seat.label);
|
|
5085
|
+
const status = id ? this.getStatus(id) : void 0;
|
|
5086
|
+
if ((status ?? "free") === "free") available.add(seat.label);
|
|
5087
|
+
}
|
|
5088
|
+
const pick = pickBestAvailable(seats, available, { qty, categoryKey, focal, preferPremium: true });
|
|
5089
|
+
if (pick.labels.length !== qty) return null;
|
|
5090
|
+
const allPremium = pick.labels.every((l) => this.labelToSeat.get(l)?.commercial?.premium);
|
|
5091
|
+
return allPremium ? [...pick.labels] : null;
|
|
5092
|
+
}
|
|
5093
|
+
/**
|
|
5094
|
+
* Server-picks `qty` best free seats and holds them atomically. Throws on
|
|
5095
|
+
* failure. With `preferPremium`, first tries to hold the best PREMIUM block
|
|
5096
|
+
* locally; when none matches the requested quantity it falls back to the
|
|
5097
|
+
* normal server pick (the widget surfaces a subtle note on that fallback).
|
|
5098
|
+
*/
|
|
5099
|
+
async bestAvailable(qty, categoryKey, opts = {}) {
|
|
4831
5100
|
const r = this.renderer;
|
|
4832
5101
|
if (!r) return null;
|
|
5102
|
+
if (opts.preferPremium) {
|
|
5103
|
+
const block = this.pickPremiumBlock(qty, categoryKey);
|
|
5104
|
+
if (block) {
|
|
5105
|
+
if (this.hold_ && !await this.release()) return null;
|
|
5106
|
+
try {
|
|
5107
|
+
const selection = block.map((label) => {
|
|
5108
|
+
const seat = this.labelToSeat.get(label);
|
|
5109
|
+
const resolved = seat ? this.toSeat(seat) : null;
|
|
5110
|
+
return { label, ...resolved?.tierId ? { tierId: resolved.tierId } : {} };
|
|
5111
|
+
});
|
|
5112
|
+
const result = await this.api.hold(this.key, selection);
|
|
5113
|
+
r.clearSelection();
|
|
5114
|
+
const ids = block.map((l) => this.labelToId.get(l)).filter((v) => !!v);
|
|
5115
|
+
if (ids.length) r.setStatus(ids, "held");
|
|
5116
|
+
this.setHold({ holdId: result.holdId, labels: [...block], expiresAt: result.expiresAt, items: result.items });
|
|
5117
|
+
const seats = block.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
|
|
5118
|
+
this.opts.onSelectionChange?.(seats);
|
|
5119
|
+
return this.hold_;
|
|
5120
|
+
} catch (err) {
|
|
5121
|
+
const { status, reason } = errInfo(err);
|
|
5122
|
+
if (status === 409 && reason === "event_closed") {
|
|
5123
|
+
this.opts.onSalesClosed?.();
|
|
5124
|
+
throw err;
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
4833
5129
|
if (this.hold_ && !await this.release()) return null;
|
|
4834
5130
|
try {
|
|
4835
5131
|
const result = await this.api.bestAvailable(this.key, qty, categoryKey);
|
|
@@ -5202,14 +5498,16 @@ var PickerController = class {
|
|
|
5202
5498
|
// ---- internals ------------------------------------------------------------
|
|
5203
5499
|
toSeat(s) {
|
|
5204
5500
|
const commercial = s.commercial ? { commercial: s.commercial } : void 0;
|
|
5501
|
+
const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
|
|
5205
5502
|
const tiers = this.tiersFor(s.categoryKey);
|
|
5206
5503
|
if (!tiers) {
|
|
5207
|
-
return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
|
|
5504
|
+
return { id: s.id, label: s.label, ...display, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
|
|
5208
5505
|
}
|
|
5209
5506
|
const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
|
|
5210
5507
|
return {
|
|
5211
5508
|
id: s.id,
|
|
5212
5509
|
label: s.label,
|
|
5510
|
+
...display,
|
|
5213
5511
|
categoryKey: s.categoryKey,
|
|
5214
5512
|
price: chosen.price,
|
|
5215
5513
|
tiers,
|
|
@@ -5702,9 +6000,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
|
|
|
5702
6000
|
|
|
5703
6001
|
// src/i18n/bundles.ts
|
|
5704
6002
|
var LOADERS = {
|
|
5705
|
-
es: () => import("./es-
|
|
5706
|
-
de: () => import("./de-
|
|
5707
|
-
fr: () => import("./fr-
|
|
6003
|
+
es: () => import("./es-47HFAWS6.js").then((m) => ({ default: m.es })),
|
|
6004
|
+
de: () => import("./de-EIG65UFU.js").then((m) => ({ default: m.de })),
|
|
6005
|
+
fr: () => import("./fr-7IXNNFBS.js").then((m) => ({ default: m.fr }))
|
|
5708
6006
|
};
|
|
5709
6007
|
var loaded = /* @__PURE__ */ new Set(["en"]);
|
|
5710
6008
|
async function loadLocale(code) {
|
|
@@ -5769,6 +6067,8 @@ export {
|
|
|
5769
6067
|
pointInPolygonWithHoles,
|
|
5770
6068
|
polygonLabelPoint,
|
|
5771
6069
|
resolveLocale,
|
|
6070
|
+
rowSeatPositions,
|
|
6071
|
+
seatLabelPart,
|
|
5772
6072
|
setLocale,
|
|
5773
6073
|
setMoneyLocale,
|
|
5774
6074
|
setStringOverrides,
|