@seatlayer/core 0.23.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/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 expandRowSlots(row) {
160
- const start = row.seatLabelStart ?? 1;
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
- let seatNumber;
165
- if (dir === "center") {
166
- const rank = new Array(n);
167
- 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);
168
- seatNumber = (i) => start + rank[i] * step;
169
- } else {
170
- seatNumber = (i) => start + (dir === "rtl" ? n - 1 - i : i) * step;
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 inventoryLabel = o?.label ?? `${row.label}-${seatNumber(i)}`;
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}-${seatNumber(i)}`,
269
+ displayLabel: o?.displayLabel ?? `${displayPrefix}-${part}`,
185
270
  categoryKey: o?.categoryKey ?? row.categoryKey,
186
271
  skipped: !!o?.skip,
187
272
  accessible: accessibility.length > 0,
@@ -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 page (src/pages/PickerPage.tsx)
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,31 +856,11 @@ 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",
@@ -863,6 +919,7 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
863
919
  var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
864
920
  var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
865
921
  var SEAT_TAP_SLOP_PX = 14;
922
+ var SEAT_GLYPH_MIN_PX = 6.5;
866
923
  var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
867
924
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
868
925
  var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
@@ -1152,6 +1209,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1152
1209
  this.boothLabelById = /* @__PURE__ */ new Map();
1153
1210
  /** Viewport seat labels are rebuilt after each settled camera change. */
1154
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;
1155
1221
  /** Authored free-text nodes obey the same rendered-size visibility floor. */
1156
1222
  this.freeTextById = /* @__PURE__ */ new Map();
1157
1223
  /** Stage/rink landmarks retain a readable screen-space caption at overview. */
@@ -1464,6 +1530,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1464
1530
  this.boothDims.clear();
1465
1531
  this.boothLabelById.clear();
1466
1532
  this.seatLabelById.clear();
1533
+ this.accessRingById.clear();
1534
+ this.accessGlyphById.clear();
1467
1535
  this.freeTextById.clear();
1468
1536
  this.primaryFocalLabels.clear();
1469
1537
  this.gaById.clear();
@@ -2372,18 +2440,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2372
2440
  this.paintSeat(c, seat.id);
2373
2441
  target.add(c);
2374
2442
  if (seat.accessible) {
2375
- target.add(
2376
- new Circle({
2377
- x: seat.x,
2378
- y: seat.y,
2379
- radius: this.seatR + 1,
2380
- stroke: accessibilityRingColor(seat.accessibility),
2381
- strokeWidth: 2,
2382
- listening: false,
2383
- perfectDrawEnabled: false,
2384
- shadowForStrokeEnabled: false
2385
- })
2386
- );
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);
2387
2458
  }
2388
2459
  }
2389
2460
  }
@@ -2427,6 +2498,53 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2427
2498
  target.add(t2);
2428
2499
  this.paintSeat(rect, seat.id);
2429
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
+ }
2430
2548
  /** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
2431
2549
  seatBaseColor(categoryKey) {
2432
2550
  if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
@@ -2540,6 +2658,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2540
2658
  isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
2541
2659
  );
2542
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());
2543
2669
  }
2544
2670
  /** True when a seat sits in a section/zone currently marked `closed`. */
2545
2671
  seatInClosedSection(id) {
@@ -4265,6 +4391,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4265
4391
  if (this.hasSections) this.applySectionLod(scale);
4266
4392
  else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
4267
4393
  this.paintGAStateForView();
4394
+ this.updateAccessGlyphs(scale);
4268
4395
  const shouldCache = scale < CACHE_THRESHOLD;
4269
4396
  if (shouldCache && !this.cached) {
4270
4397
  this.cacheSeatLayer();
@@ -4347,6 +4474,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4347
4474
  for (const seat of this.seats) {
4348
4475
  if (seat.kind === "booth") continue;
4349
4476
  if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
4477
+ if (seat.accessible && this.accessGlyphVisible) continue;
4350
4478
  const shape = this.circleById.get(seat.id);
4351
4479
  if ((shape?.opacity() ?? 1) < 0.5) continue;
4352
4480
  const status = this.statusById.get(seat.id) ?? "free";
@@ -4696,9 +4824,11 @@ var PickerController = class {
4696
4824
  this.allIds.push(s.id);
4697
4825
  const source = chartObjects.get(s.rowId);
4698
4826
  const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
4699
- const rowLabel = s.kind === "booth" ? void 0 : sourceLabel;
4700
- const labelParts = s.label.split("-");
4701
- const seatNumber = rowLabel && s.label.startsWith(`${rowLabel}-`) ? s.label.slice(rowLabel.length + 1) : s.kind === "booth" ? s.label : labelParts[labelParts.length - 1] ?? s.label;
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;
4702
4832
  this.seatContext.set(s.id, {
4703
4833
  sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
4704
4834
  rowLabel,
@@ -5368,14 +5498,16 @@ var PickerController = class {
5368
5498
  // ---- internals ------------------------------------------------------------
5369
5499
  toSeat(s) {
5370
5500
  const commercial = s.commercial ? { commercial: s.commercial } : void 0;
5501
+ const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
5371
5502
  const tiers = this.tiersFor(s.categoryKey);
5372
5503
  if (!tiers) {
5373
- 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 };
5374
5505
  }
5375
5506
  const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
5376
5507
  return {
5377
5508
  id: s.id,
5378
5509
  label: s.label,
5510
+ ...display,
5379
5511
  categoryKey: s.categoryKey,
5380
5512
  price: chosen.price,
5381
5513
  tiers,
@@ -5868,9 +6000,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
5868
6000
 
5869
6001
  // src/i18n/bundles.ts
5870
6002
  var LOADERS = {
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 }))
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 }))
5874
6006
  };
5875
6007
  var loaded = /* @__PURE__ */ new Set(["en"]);
5876
6008
  async function loadLocale(code) {
@@ -5935,6 +6067,8 @@ export {
5935
6067
  pointInPolygonWithHoles,
5936
6068
  polygonLabelPoint,
5937
6069
  resolveLocale,
6070
+ rowSeatPositions,
6071
+ seatLabelPart,
5938
6072
  setLocale,
5939
6073
  setMoneyLocale,
5940
6074
  setStringOverrides,