@seatlayer/core 0.22.0 → 0.23.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.d.cts CHANGED
@@ -2067,8 +2067,28 @@ declare class PickerController {
2067
2067
  tierId?: string | null;
2068
2068
  ttlMs?: number;
2069
2069
  }): Promise<HoldInfo | null>;
2070
- /** Server-picks `qty` best free seats and holds them atomically. Throws on failure. */
2071
- bestAvailable(qty: number, categoryKey?: string): Promise<HoldInfo | null>;
2070
+ /** True when any seat on the visible chart carries the `premium` commercial
2071
+ * flag gates the buyer widget's "★ Best seats" premium quick-pick (chip is
2072
+ * present-only, exactly like the accessibility filter chips). */
2073
+ hasPremiumSeats(): boolean;
2074
+ /**
2075
+ * Client-side premium pre-pass: find the best contiguous block of `qty`
2076
+ * PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
2077
+ * using the local seat geometry + live availability, mirroring the server's
2078
+ * held-back exclusion via the visible (hidden-stripped) chart and current
2079
+ * seat status. Returns a FULL premium block of `qty`, or null when none
2080
+ * exists (the caller then falls back to the normal server pick).
2081
+ */
2082
+ private pickPremiumBlock;
2083
+ /**
2084
+ * Server-picks `qty` best free seats and holds them atomically. Throws on
2085
+ * failure. With `preferPremium`, first tries to hold the best PREMIUM block
2086
+ * locally; when none matches the requested quantity it falls back to the
2087
+ * normal server pick (the widget surfaces a subtle note on that fallback).
2088
+ */
2089
+ bestAvailable(qty: number, categoryKey?: string, opts?: {
2090
+ preferPremium?: boolean;
2091
+ }): Promise<HoldInfo | null>;
2072
2092
  /**
2073
2093
  * Complete a booking. Requires a transport that supports book() (the SDK
2074
2094
  * deliberately does not). `bookingRef` MUST be a real reference from the caller.
package/dist/index.d.ts CHANGED
@@ -2067,8 +2067,28 @@ declare class PickerController {
2067
2067
  tierId?: string | null;
2068
2068
  ttlMs?: number;
2069
2069
  }): Promise<HoldInfo | null>;
2070
- /** Server-picks `qty` best free seats and holds them atomically. Throws on failure. */
2071
- bestAvailable(qty: number, categoryKey?: string): Promise<HoldInfo | null>;
2070
+ /** True when any seat on the visible chart carries the `premium` commercial
2071
+ * flag gates the buyer widget's "★ Best seats" premium quick-pick (chip is
2072
+ * present-only, exactly like the accessibility filter chips). */
2073
+ hasPremiumSeats(): boolean;
2074
+ /**
2075
+ * Client-side premium pre-pass: find the best contiguous block of `qty`
2076
+ * PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
2077
+ * using the local seat geometry + live availability, mirroring the server's
2078
+ * held-back exclusion via the visible (hidden-stripped) chart and current
2079
+ * seat status. Returns a FULL premium block of `qty`, or null when none
2080
+ * exists (the caller then falls back to the normal server pick).
2081
+ */
2082
+ private pickPremiumBlock;
2083
+ /**
2084
+ * Server-picks `qty` best free seats and holds them atomically. Throws on
2085
+ * failure. With `preferPremium`, first tries to hold the best PREMIUM block
2086
+ * locally; when none matches the requested quantity it falls back to the
2087
+ * normal server pick (the widget surfaces a subtle note on that fallback).
2088
+ */
2089
+ bestAvailable(qty: number, categoryKey?: string, opts?: {
2090
+ preferPremium?: boolean;
2091
+ }): Promise<HoldInfo | null>;
2072
2092
  /**
2073
2093
  * Complete a booking. Requires a transport that supports book() (the SDK
2074
2094
  * deliberately does not). `bookingRef` MUST be a real reference from the caller.
package/dist/index.js CHANGED
@@ -323,8 +323,8 @@ function polygonLabelPoint(outer, holes) {
323
323
  const xs = outer.map((point) => point.x);
324
324
  const ys = outer.map((point) => point.y);
325
325
  const bounds = { minX: Math.min(...xs), maxX: Math.max(...xs), minY: Math.min(...ys), maxY: Math.max(...ys) };
326
- const centroid = polygonCentroid(outer);
327
- if (pointInPolygonWithHoles(centroid, outer, holes)) return centroid;
326
+ const centroid2 = polygonCentroid(outer);
327
+ if (pointInPolygonWithHoles(centroid2, outer, holes)) return centroid2;
328
328
  let best = outer[0];
329
329
  let bestScore = -Infinity;
330
330
  const rings = [outer, ...holes ?? []];
@@ -810,7 +810,9 @@ var en = {
810
810
  "picker.restrictedView": "Restricted view",
811
811
  "picker.obstructedView": "Obstructed view",
812
812
  "picker.premiumSeat": "Premium seat",
813
- "picker.hideLimitedView": "Hide limited-view seats"
813
+ "picker.hideLimitedView": "Hide limited-view seats",
814
+ "picker.bestSeatsPremium": "Best seats",
815
+ "picker.premiumFallbackNote": "No premium block of {count} \u2014 showing best overall"
814
816
  };
815
817
 
816
818
  // src/i18n/index.ts
@@ -3016,7 +3018,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3016
3018
  * Membership and category mix are still precomputed for the detailed state.
3017
3019
  */
3018
3020
  renderSection(obj) {
3019
- const centroid = polygonLabelPoint(obj.outline, obj.holes);
3021
+ const centroid2 = polygonLabelPoint(obj.outline, obj.holes);
3020
3022
  const palette = overviewPalette(this.canvasBackground);
3021
3023
  const memberIds = [];
3022
3024
  const catCounts = /* @__PURE__ */ new Map();
@@ -3078,8 +3080,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3078
3080
  const preferredInk = labelStyle?.color ?? palette.sectionInk;
3079
3081
  const labelScale = (labelStyle?.size ?? 18) / 18;
3080
3082
  const nameLabel = new Text({
3081
- x: obj.labelPresentation?.position?.x ?? centroid.x,
3082
- y: obj.labelPresentation?.position?.y ?? centroid.y,
3083
+ x: obj.labelPresentation?.position?.x ?? centroid2.x,
3084
+ y: obj.labelPresentation?.position?.y ?? centroid2.y,
3083
3085
  text: obj.displayLabel ?? obj.label,
3084
3086
  rotation: obj.labelPresentation?.rotation ?? 0,
3085
3087
  visible: obj.labelPresentation?.visible !== false,
@@ -3094,8 +3096,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3094
3096
  nameLabel.offsetY(nameLabel.height() / 2);
3095
3097
  bgTarget.add(nameLabel);
3096
3098
  const subLabel = new Text({
3097
- x: centroid.x,
3098
- y: centroid.y,
3099
+ x: centroid2.x,
3100
+ y: centroid2.y,
3099
3101
  text: t("map.seatsLeft", { count: free }),
3100
3102
  fontSize: 12,
3101
3103
  fontStyle: "700",
@@ -3114,8 +3116,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3114
3116
  outline: obj.outline,
3115
3117
  ...obj.outlinePath ? { outlinePath: obj.outlinePath } : {},
3116
3118
  holes: obj.holes ?? [],
3117
- centroid,
3118
- labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid),
3119
+ centroid: centroid2,
3120
+ labelAnchors: polygonLabelCandidates(obj.outline, obj.holes ?? [], centroid2),
3119
3121
  zone: obj.zone,
3120
3122
  memberIds,
3121
3123
  total: memberIds.length,
@@ -4475,6 +4477,103 @@ function strandedSingles(seats, statusOf, selectedIds) {
4475
4477
  return out;
4476
4478
  }
4477
4479
 
4480
+ // src/core/bestAvailable.ts
4481
+ function seatIndex2(seat, fallback) {
4482
+ const i = seat.id.lastIndexOf(":");
4483
+ if (i < 0) return fallback;
4484
+ const n = Number(seat.id.slice(i + 1));
4485
+ return Number.isFinite(n) ? n : fallback;
4486
+ }
4487
+ function dist2(a, b) {
4488
+ const dx = a.x - b.x;
4489
+ const dy = a.y - b.y;
4490
+ return dx * dx + dy * dy;
4491
+ }
4492
+ function centroid(seats) {
4493
+ let x = 0;
4494
+ let y = 0;
4495
+ for (const s of seats) {
4496
+ x += s.x;
4497
+ y += s.y;
4498
+ }
4499
+ return { x: x / seats.length, y: y / seats.length };
4500
+ }
4501
+ function isPremium(seat) {
4502
+ return seat.commercial?.premium === true;
4503
+ }
4504
+ function pickBestAvailable(seats, available, opts) {
4505
+ const qty = Math.floor(opts.qty);
4506
+ if (!Number.isFinite(qty) || qty <= 0) return { labels: [], reason: "sold_out" };
4507
+ const { categoryKey, focal, preferPremium } = opts;
4508
+ const rows = /* @__PURE__ */ new Map();
4509
+ const eligibleAll = [];
4510
+ seats.forEach((seat, i) => {
4511
+ const elig = available.has(seat.label) && (!categoryKey || seat.categoryKey === categoryKey);
4512
+ let arr = rows.get(seat.rowId);
4513
+ if (!arr) {
4514
+ arr = [];
4515
+ rows.set(seat.rowId, arr);
4516
+ }
4517
+ arr.push({ seat, index: seatIndex2(seat, i), elig });
4518
+ if (elig) eligibleAll.push(seat);
4519
+ });
4520
+ if (eligibleAll.length === 0) return { labels: [], reason: "sold_out" };
4521
+ let best = null;
4522
+ const better = (c) => {
4523
+ if (!best) return true;
4524
+ if (preferPremium && c.nonPremium !== best.nonPremium) return c.nonPremium < best.nonPremium;
4525
+ if (c.orphan !== best.orphan) return c.orphan < best.orphan;
4526
+ if (c.d2 !== best.d2) return c.d2 < best.d2;
4527
+ const r = c.rowId.localeCompare(best.rowId);
4528
+ if (r !== 0) return r < 0;
4529
+ return c.startIndex < best.startIndex;
4530
+ };
4531
+ for (const [rowId, slotsUnsorted] of rows) {
4532
+ const slots = [...slotsUnsorted].sort((a, b) => a.index - b.index);
4533
+ let i = 0;
4534
+ while (i < slots.length) {
4535
+ if (!slots[i].elig) {
4536
+ i++;
4537
+ continue;
4538
+ }
4539
+ let j = i;
4540
+ while (j < slots.length && slots[j].elig) j++;
4541
+ const segLen = j - i;
4542
+ for (let p = 0; p + qty <= segLen; p++) {
4543
+ const leftRem = p;
4544
+ const rightRem = segLen - (p + qty);
4545
+ const orphan = leftRem === 1 || rightRem === 1 ? 1 : 0;
4546
+ const runSeats = slots.slice(i + p, i + p + qty).map((s) => s.seat);
4547
+ const c = {
4548
+ labels: runSeats.map((s) => s.label),
4549
+ seats: runSeats,
4550
+ rowId,
4551
+ startIndex: slots[i + p].index,
4552
+ orphan,
4553
+ nonPremium: preferPremium ? runSeats.reduce((n, s) => n + (isPremium(s) ? 0 : 1), 0) : 0,
4554
+ d2: dist2(centroid(runSeats), focal)
4555
+ };
4556
+ if (better(c)) best = c;
4557
+ }
4558
+ i = j;
4559
+ }
4560
+ }
4561
+ if (best) return { labels: best.labels };
4562
+ if (eligibleAll.length < qty) return { labels: [], reason: "not_enough_together" };
4563
+ const ranked = eligibleAll.map((seat, i) => ({ seat, i, d2: dist2(seat, focal) })).sort((a, b) => {
4564
+ if (preferPremium) {
4565
+ const pa = isPremium(a.seat) ? 0 : 1;
4566
+ const pb = isPremium(b.seat) ? 0 : 1;
4567
+ if (pa !== pb) return pa - pb;
4568
+ }
4569
+ if (a.d2 !== b.d2) return a.d2 - b.d2;
4570
+ const r = a.seat.rowId.localeCompare(b.seat.rowId);
4571
+ if (r !== 0) return r;
4572
+ return seatIndex2(a.seat, a.i) - seatIndex2(b.seat, b.i);
4573
+ });
4574
+ return { labels: ranked.slice(0, qty).map((r) => r.seat.label) };
4575
+ }
4576
+
4478
4577
  // src/picker/PickerController.ts
4479
4578
  var DEFAULT_MAX_SELECTION = 10;
4480
4579
  var MAX_BACKOFF_MS = 15e3;
@@ -4826,10 +4925,77 @@ var PickerController = class {
4826
4925
  this.setHold({ holdId: result.holdId, labels: combined.map((s) => s.label), expiresAt: result.expiresAt, items: result.items });
4827
4926
  return this.hold_;
4828
4927
  }
4829
- /** Server-picks `qty` best free seats and holds them atomically. Throws on failure. */
4830
- async bestAvailable(qty, categoryKey) {
4928
+ /** True when any seat on the visible chart carries the `premium` commercial
4929
+ * flag — gates the buyer widget's "★ Best seats" premium quick-pick (chip is
4930
+ * present-only, exactly like the accessibility filter chips). */
4931
+ hasPremiumSeats() {
4932
+ for (const seat of this.labelToSeat.values()) {
4933
+ if (seat.commercial?.premium) return true;
4934
+ }
4935
+ return false;
4936
+ }
4937
+ /**
4938
+ * Client-side premium pre-pass: find the best contiguous block of `qty`
4939
+ * PREMIUM-flagged free seats (orphan-avoiding, closest to the focal point)
4940
+ * using the local seat geometry + live availability, mirroring the server's
4941
+ * held-back exclusion via the visible (hidden-stripped) chart and current
4942
+ * seat status. Returns a FULL premium block of `qty`, or null when none
4943
+ * exists (the caller then falls back to the normal server pick).
4944
+ */
4945
+ pickPremiumBlock(qty, categoryKey) {
4946
+ const doc = this.visibleDoc();
4947
+ if (!doc?.objects?.length) return null;
4948
+ const focal = doc.focalPoint ?? { x: 0, y: 0 };
4949
+ const seats = expandChart(doc);
4950
+ const held = new Set(this.hold_?.labels ?? []);
4951
+ const available = /* @__PURE__ */ new Set();
4952
+ for (const seat of seats) {
4953
+ if (held.has(seat.label)) continue;
4954
+ const id = this.labelToId.get(seat.label);
4955
+ const status = id ? this.getStatus(id) : void 0;
4956
+ if ((status ?? "free") === "free") available.add(seat.label);
4957
+ }
4958
+ const pick = pickBestAvailable(seats, available, { qty, categoryKey, focal, preferPremium: true });
4959
+ if (pick.labels.length !== qty) return null;
4960
+ const allPremium = pick.labels.every((l) => this.labelToSeat.get(l)?.commercial?.premium);
4961
+ return allPremium ? [...pick.labels] : null;
4962
+ }
4963
+ /**
4964
+ * Server-picks `qty` best free seats and holds them atomically. Throws on
4965
+ * failure. With `preferPremium`, first tries to hold the best PREMIUM block
4966
+ * locally; when none matches the requested quantity it falls back to the
4967
+ * normal server pick (the widget surfaces a subtle note on that fallback).
4968
+ */
4969
+ async bestAvailable(qty, categoryKey, opts = {}) {
4831
4970
  const r = this.renderer;
4832
4971
  if (!r) return null;
4972
+ if (opts.preferPremium) {
4973
+ const block = this.pickPremiumBlock(qty, categoryKey);
4974
+ if (block) {
4975
+ if (this.hold_ && !await this.release()) return null;
4976
+ try {
4977
+ const selection = block.map((label) => {
4978
+ const seat = this.labelToSeat.get(label);
4979
+ const resolved = seat ? this.toSeat(seat) : null;
4980
+ return { label, ...resolved?.tierId ? { tierId: resolved.tierId } : {} };
4981
+ });
4982
+ const result = await this.api.hold(this.key, selection);
4983
+ r.clearSelection();
4984
+ const ids = block.map((l) => this.labelToId.get(l)).filter((v) => !!v);
4985
+ if (ids.length) r.setStatus(ids, "held");
4986
+ this.setHold({ holdId: result.holdId, labels: [...block], expiresAt: result.expiresAt, items: result.items });
4987
+ const seats = block.map((l) => this.labelToSeat.get(l)).filter((s) => !!s).map((s) => this.toSeat(s));
4988
+ this.opts.onSelectionChange?.(seats);
4989
+ return this.hold_;
4990
+ } catch (err) {
4991
+ const { status, reason } = errInfo(err);
4992
+ if (status === 409 && reason === "event_closed") {
4993
+ this.opts.onSalesClosed?.();
4994
+ throw err;
4995
+ }
4996
+ }
4997
+ }
4998
+ }
4833
4999
  if (this.hold_ && !await this.release()) return null;
4834
5000
  try {
4835
5001
  const result = await this.api.bestAvailable(this.key, qty, categoryKey);
@@ -5702,9 +5868,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
5702
5868
 
5703
5869
  // src/i18n/bundles.ts
5704
5870
  var LOADERS = {
5705
- es: () => import("./es-ALPR5FBB.js").then((m) => ({ default: m.es })),
5706
- de: () => import("./de-KPTY7UOT.js").then((m) => ({ default: m.de })),
5707
- fr: () => import("./fr-HVQLQD2N.js").then((m) => ({ default: m.fr }))
5871
+ es: () => import("./es-N73CIVTC.js").then((m) => ({ default: m.es })),
5872
+ de: () => import("./de-DRWMZ2FV.js").then((m) => ({ default: m.de })),
5873
+ fr: () => import("./fr-SOY2OB4P.js").then((m) => ({ default: m.fr }))
5708
5874
  };
5709
5875
  var loaded = /* @__PURE__ */ new Set(["en"]);
5710
5876
  async function loadLocale(code) {