@seatlayer/core 0.23.0 → 0.25.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 CHANGED
@@ -41,29 +41,17 @@ var init_es = __esm({
41
41
  "common.done": "Listo",
42
42
  "common.copied": "\u2713 Copiado",
43
43
  // buyer picker (extraction in progress — P4 of the port plan)
44
- "picker.holdSeats": "Retener asientos y comprar",
45
- "picker.completeBooking": "Completar reserva",
46
- "picker.seatsHeld": "Asientos retenidos \u2014 {time}",
47
44
  "picker.holdExpired": "Tu retenci\xF3n expir\xF3 \u2014 los asientos fueron liberados. Elige de nuevo.",
48
- "picker.seatTaken": "El asiento {label} fue tomado por otro comprador.",
49
45
  "picker.poweredBy": "Con la tecnolog\xEDa de SeatLayer",
50
46
  "picker.testMode": "MODO DE PRUEBA",
51
- "picker.colorblind": "Colores aptos para daltonismo",
52
47
  "picker.orphanHint": "Esto deja un asiento aislado \u2014 considera desplazarte un asiento.",
53
- "picker.seats.one": "{count} asiento",
54
- "picker.seats.other": "{count} asientos",
55
48
  // renderer (drawn on the Konva map — shared by the embed SDK)
56
49
  "map.aria": "Mapa de asientos. Usa las flechas para moverte entre asientos, Intro para seleccionar.",
57
50
  "map.seatsLeft": "{count} LIBRES",
58
51
  "map.statusHeld": "En espera",
59
52
  "map.statusTaken": "Ocupado",
60
53
  "map.fromPrice": "DESDE {price}",
61
- // buyer picker page (src/pages/PickerPage.tsx)
62
- "picker.language": "Idioma",
63
- "picker.zoomToFit": "Ajustar zoom",
64
- "picker.seatCountLabel": "asientos",
65
- "picker.capacity": "capacidad",
66
- "picker.viewMode": "Modo de vista",
54
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
67
55
  "picker.floor": "Piso",
68
56
  "picker.zoomLevel": "Nivel de zoom",
69
57
  "picker.rungTip.zones": "Vista general \u2014 grupos de secciones como Grada norte o VIP",
@@ -77,32 +65,13 @@ var init_es = __esm({
77
65
  "picker.seatsLeftInSection.one": "{count} asiento disponible",
78
66
  "picker.seatsLeftInSection.other": "{count} asientos disponibles",
79
67
  "picker.overview": "Descripci\xF3n general",
68
+ "picker.entrance": "Entrada",
80
69
  "picker.tapSeatHint": "Toca cualquier asiento para ver su vista",
81
- "picker.chartSize": "Tama\xF1o del gr\xE1fico",
82
- "picker.custom": "Personalizado",
83
- "picker.categories": "Categor\xEDas",
84
- "picker.accessibility": "Accesibilidad",
85
- "picker.showAnyAccessible": "Mostrar cualquier asiento accesible",
86
- "picker.any": "Cualquiera",
87
- "picker.yourSeats": "Tus asientos",
88
- "picker.emptySeats": "Toca asientos en el mapa",
89
- "picker.emptySeatsWithGa": "Toca asientos en el mapa \xB7 toca un \xE1rea de pie para entradas de entrada general",
90
- "picker.oneFewer": "Uno menos",
91
- "picker.oneMore": "Uno m\xE1s",
92
- "picker.remove": "Eliminar {label}",
93
70
  "picker.ticketTierFor": "Categor\xEDa de entrada para {label}",
94
- "picker.total": "Total: {amount}",
95
71
  "picker.viewFromSeat": "Vista desde el asiento {label}",
96
72
  "picker.real360": "REAL 360\xB0",
97
73
  "picker.preview": "VISTA PREVIA",
98
- "picker.open360": "Abrir vista de 360\xB0",
99
74
  "picker.sightline": "\u2248 {m} m al escenario \xB7 visi\xF3n despejada",
100
- "picker.bookedDemo": "\xA1Reservado! (demostraci\xF3n)",
101
- "picker.bookButton.one": "Reservar {count} entrada \u2014 {amount}",
102
- "picker.bookButton.other": "Reservar {count} entradas \u2014 {amount}",
103
- "picker.simulateCrowd": "Simular multitud: {state}",
104
- "picker.on": "ACTIVADO",
105
- "picker.off": "DESACTIVADO",
106
75
  "picker.panorama360": "Foto de 360\xB0 del lugar",
107
76
  "picker.illustrationCaption": "ilustraci\xF3n \xB7 \u2248 {m} m del escenario",
108
77
  "picker.restrictedView": "Visibilidad restringida",
@@ -131,29 +100,17 @@ var init_de = __esm({
131
100
  "common.done": "Fertig",
132
101
  "common.copied": "\u2713 Kopiert",
133
102
  // buyer picker (extraction in progress — P4 of the port plan)
134
- "picker.holdSeats": "Pl\xE4tze reservieren & Kasse",
135
- "picker.completeBooking": "Buchung abschlie\xDFen",
136
- "picker.seatsHeld": "Pl\xE4tze reserviert \u2014 {time}",
137
103
  "picker.holdExpired": "Ihre Reservierung ist abgelaufen \u2014 die Pl\xE4tze wurden freigegeben. Bitte erneut ausw\xE4hlen.",
138
- "picker.seatTaken": "Platz {label} wurde gerade von einem anderen K\xE4ufer gebucht.",
139
104
  "picker.poweredBy": "Bereitgestellt von SeatLayer",
140
105
  "picker.testMode": "TESTMODUS",
141
- "picker.colorblind": "Farbenblind-freundliche Farben",
142
106
  "picker.orphanHint": "Dadurch bleibt ein einzelner Platz \xFCbrig \u2014 r\xFCcken Sie ggf. einen Platz weiter.",
143
- "picker.seats.one": "{count} Platz",
144
- "picker.seats.other": "{count} Pl\xE4tze",
145
107
  // renderer (drawn on the Konva map — shared by the embed SDK)
146
108
  "map.aria": "Sitzplan. Mit Pfeiltasten zwischen Pl\xE4tzen navigieren, Eingabe zum Ausw\xE4hlen.",
147
109
  "map.seatsLeft": "{count} FREI",
148
110
  "map.statusHeld": "Reserviert",
149
111
  "map.statusTaken": "Vergeben",
150
112
  "map.fromPrice": "AB {price}",
151
- // buyer picker page (src/pages/PickerPage.tsx)
152
- "picker.language": "Sprache",
153
- "picker.zoomToFit": "Auf Gr\xF6\xDFe anpassen",
154
- "picker.seatCountLabel": "Pl\xE4tze",
155
- "picker.capacity": "Kapazit\xE4t",
156
- "picker.viewMode": "Ansichtsmodus",
113
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
157
114
  "picker.floor": "Etage",
158
115
  "picker.zoomLevel": "Zoomstufe",
159
116
  "picker.rungTip.zones": "\xDCbersicht \u2014 Bereichsgruppen wie Nordtrib\xFCne oder VIP",
@@ -167,32 +124,13 @@ var init_de = __esm({
167
124
  "picker.seatsLeftInSection.one": "{count} Platz verf\xFCgbar",
168
125
  "picker.seatsLeftInSection.other": "{count} Pl\xE4tze verf\xFCgbar",
169
126
  "picker.overview": "\xDCbersicht",
127
+ "picker.entrance": "Eingang",
170
128
  "picker.tapSeatHint": "Tippen Sie auf einen Platz, um die Ansicht zu pr\xFCfen",
171
- "picker.chartSize": "Kartengr\xF6\xDFe",
172
- "picker.custom": "Benutzerdefiniert",
173
- "picker.categories": "Kategorien",
174
- "picker.accessibility": "Barrierefreiheit",
175
- "picker.showAnyAccessible": "Jeden barrierefreien Platz anzeigen",
176
- "picker.any": "Beliebig",
177
- "picker.yourSeats": "Ihre Pl\xE4tze",
178
- "picker.emptySeats": "Tippen Sie auf die Pl\xE4tze auf der Karte",
179
- "picker.emptySeatsWithGa": "Tippen Sie auf die Pl\xE4tze auf der Karte \xB7 tippen Sie auf einen Stehbereich f\xFCr Stehplatztickets",
180
- "picker.oneFewer": "Einen weniger",
181
- "picker.oneMore": "Einen mehr",
182
- "picker.remove": "{label} entfernen",
183
129
  "picker.ticketTierFor": "Ticketklasse f\xFCr {label}",
184
- "picker.total": "Gesamt: {amount}",
185
130
  "picker.viewFromSeat": "Ansicht von Platz {label}",
186
131
  "picker.real360": "REAL 360\xB0",
187
132
  "picker.preview": "VORSCHAU",
188
- "picker.open360": "\xD6ffnen Sie die 360\xB0-Ansicht",
189
133
  "picker.sightline": "\u2248 {m} m zur B\xFChne \xB7 freie Sicht",
190
- "picker.bookedDemo": "Gebucht! (Demo)",
191
- "picker.bookButton.one": "{count} Ticket buchen \u2014 {amount}",
192
- "picker.bookButton.other": "{count} Tickets buchen \u2014 {amount}",
193
- "picker.simulateCrowd": "Menschenmenge simulieren: {state}",
194
- "picker.on": "AN",
195
- "picker.off": "AUS",
196
134
  "picker.panorama360": "360\xB0-Veranstaltungsfotos",
197
135
  "picker.illustrationCaption": "Illustration \xB7 \u2248 {m} m von der B\xFChne",
198
136
  "picker.restrictedView": "Eingeschr\xE4nkte Sicht",
@@ -221,29 +159,17 @@ var init_fr = __esm({
221
159
  "common.done": "Termin\xE9",
222
160
  "common.copied": "\u2713 Copi\xE9",
223
161
  // buyer picker (extraction in progress — P4 of the port plan)
224
- "picker.holdSeats": "R\xE9server les si\xE8ges et valider",
225
- "picker.completeBooking": "Terminer la r\xE9servation",
226
- "picker.seatsHeld": "Si\xE8ges r\xE9serv\xE9s \u2014 {time}",
227
162
  "picker.holdExpired": "Votre r\xE9servation a expir\xE9 \u2014 les si\xE8ges ont \xE9t\xE9 lib\xE9r\xE9s. S\xE9lectionnez \xE0 nouveau.",
228
- "picker.seatTaken": "Le si\xE8ge {label} vient d'\xEAtre r\xE9serv\xE9 par un autre acheteur.",
229
163
  "picker.poweredBy": "Propuls\xE9 par SeatLayer",
230
164
  "picker.testMode": "MODE TEST",
231
- "picker.colorblind": "Couleurs adapt\xE9es au daltonisme",
232
165
  "picker.orphanHint": "Cela laisse un si\xE8ge isol\xE9 \u2014 pensez \xE0 vous d\xE9caler d'un si\xE8ge.",
233
- "picker.seats.one": "{count} si\xE8ge",
234
- "picker.seats.other": "{count} si\xE8ges",
235
166
  // renderer (drawn on the Konva map — shared by the embed SDK)
236
167
  "map.aria": "Plan de salle. Utilisez les fl\xE8ches pour naviguer entre les si\xE8ges, Entr\xE9e pour s\xE9lectionner.",
237
168
  "map.seatsLeft": "{count} LIBRES",
238
169
  "map.statusHeld": "En attente",
239
170
  "map.statusTaken": "Pris",
240
171
  "map.fromPrice": "D\xC8S {price}",
241
- // buyer picker page (src/pages/PickerPage.tsx)
242
- "picker.language": "Langue",
243
- "picker.zoomToFit": "Ajuster le zoom",
244
- "picker.seatCountLabel": "si\xE8ges",
245
- "picker.capacity": "capacit\xE9",
246
- "picker.viewMode": "Mode d'affichage",
172
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
247
173
  "picker.floor": "\xC9tage",
248
174
  "picker.zoomLevel": "Niveau de zoom",
249
175
  "picker.rungTip.zones": "Vue g\xE9n\xE9rale \u2014 groupes de sections comme Tribune nord ou VIP",
@@ -257,32 +183,13 @@ var init_fr = __esm({
257
183
  "picker.seatsLeftInSection.one": "{count} si\xE8ge restant",
258
184
  "picker.seatsLeftInSection.other": "{count} si\xE8ges restants",
259
185
  "picker.overview": "Vue d'ensemble",
186
+ "picker.entrance": "Entr\xE9e",
260
187
  "picker.tapSeatHint": "Appuyez sur un si\xE8ge pour v\xE9rifier sa vue",
261
- "picker.chartSize": "Taille du plan",
262
- "picker.custom": "Personnalis\xE9",
263
- "picker.categories": "Cat\xE9gories",
264
- "picker.accessibility": "Accessibilit\xE9",
265
- "picker.showAnyAccessible": "Afficher un si\xE8ge accessible",
266
- "picker.any": "N'importe quel",
267
- "picker.yourSeats": "Vos si\xE8ges",
268
- "picker.emptySeats": "Appuyez sur les si\xE8ges sur le plan",
269
- "picker.emptySeatsWithGa": "Appuyez sur les si\xE8ges sur le plan \xB7 appuyez sur une zone debout pour les billets GA",
270
- "picker.oneFewer": "Un de moins",
271
- "picker.oneMore": "Un de plus",
272
- "picker.remove": "Supprimer {label}",
273
188
  "picker.ticketTierFor": "Cat\xE9gorie de billet pour {label}",
274
- "picker.total": "Total : {amount}",
275
189
  "picker.viewFromSeat": "Vue depuis le si\xE8ge {label}",
276
190
  "picker.real360": "VRAI 360\xB0",
277
191
  "picker.preview": "APER\xC7U",
278
- "picker.open360": "Ouvrir la vue \xE0 360\xB0",
279
192
  "picker.sightline": "\u2248 {m} m de la sc\xE8ne \xB7 vue d\xE9gag\xE9e",
280
- "picker.bookedDemo": "R\xE9serv\xE9 ! (d\xE9mo)",
281
- "picker.bookButton.one": "R\xE9server {count} billet \u2014 {amount}",
282
- "picker.bookButton.other": "R\xE9server {count} billets \u2014 {amount}",
283
- "picker.simulateCrowd": "Simuler la foule : {state}",
284
- "picker.on": "ACTIV\xC9",
285
- "picker.off": "D\xC9SACTIV\xC9",
286
193
  "picker.panorama360": "Photo panoramique 360\xB0 du lieu",
287
194
  "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m de la sc\xE8ne",
288
195
  "picker.restrictedView": "Visibilit\xE9 r\xE9duite",
@@ -307,6 +214,7 @@ __export(index_exports, {
307
214
  MAX_EVENT_INVENTORY: () => MAX_EVENT_INVENTORY,
308
215
  MAX_GA_CAPACITY: () => MAX_GA_CAPACITY,
309
216
  PickerController: () => PickerController,
217
+ RENDERED_QUALITY_REPORT_VERSION: () => RENDERED_QUALITY_REPORT_VERSION,
310
218
  SUPPORTED_LOCALES: () => SUPPORTED_LOCALES,
311
219
  SeatmapRenderer: () => SeatmapRenderer,
312
220
  UNGROUPED_ID: () => UNGROUPED_ID,
@@ -334,6 +242,7 @@ __export(index_exports, {
334
242
  generateSeatThumb: () => generateSeatThumb,
335
243
  getLocale: () => getLocale,
336
244
  hiddenObjectIds: () => hiddenObjectIds,
245
+ inspectRenderedQualityEvidence: () => inspectRenderedQualityEvidence,
337
246
  isGaUnitLabel: () => isGaUnitLabel,
338
247
  isSectionHidden: () => isSectionHidden,
339
248
  layerOf: () => layerOf,
@@ -343,6 +252,8 @@ __export(index_exports, {
343
252
  pointInPolygonWithHoles: () => pointInPolygonWithHoles,
344
253
  polygonLabelPoint: () => polygonLabelPoint,
345
254
  resolveLocale: () => resolveLocale,
255
+ rowSeatPositions: () => rowSeatPositions,
256
+ seatLabelPart: () => seatLabelPart,
346
257
  setLocale: () => setLocale,
347
258
  setMoneyLocale: () => setMoneyLocale,
348
259
  setStringOverrides: () => setStringOverrides,
@@ -465,6 +376,53 @@ function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected
465
376
  };
466
377
  }
467
378
 
379
+ // src/core/labeling.ts
380
+ var FULL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
381
+ var LOWER_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
382
+ function toBijectiveBase(index, alphabet) {
383
+ const base = alphabet.length;
384
+ let n = index + 1;
385
+ let out = "";
386
+ while (n > 0) {
387
+ n -= 1;
388
+ const rem = n % base;
389
+ out = alphabet[rem] + out;
390
+ n = Math.floor(n / base);
391
+ }
392
+ return out;
393
+ }
394
+ function toLetters(value, lower = false) {
395
+ const alphabet = lower ? LOWER_ALPHABET : FULL_ALPHABET;
396
+ return toBijectiveBase(Math.max(0, Math.floor(value) - 1), alphabet);
397
+ }
398
+ var ROMAN_TABLE = [
399
+ [1e3, "M"],
400
+ [900, "CM"],
401
+ [500, "D"],
402
+ [400, "CD"],
403
+ [100, "C"],
404
+ [90, "XC"],
405
+ [50, "L"],
406
+ [40, "XL"],
407
+ [10, "X"],
408
+ [9, "IX"],
409
+ [5, "V"],
410
+ [4, "IV"],
411
+ [1, "I"]
412
+ ];
413
+ function toRoman(value) {
414
+ if (!Number.isFinite(value) || value <= 0) return String(value);
415
+ let remaining = Math.floor(value);
416
+ let out = "";
417
+ for (const [n, sym] of ROMAN_TABLE) {
418
+ while (remaining >= n) {
419
+ out += sym;
420
+ remaining -= n;
421
+ }
422
+ }
423
+ return out;
424
+ }
425
+
468
426
  // src/core/layout.ts
469
427
  function overrideAccessibility(o) {
470
428
  if (!o) return [];
@@ -510,24 +468,62 @@ function overrideMap(row) {
510
468
  if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
511
469
  return m;
512
470
  }
513
- function expandRowSlots(row) {
514
- const start = row.seatLabelStart ?? 1;
471
+ function centerRank(n) {
472
+ const rank = new Array(n);
473
+ 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);
474
+ return rank;
475
+ }
476
+ function seatLabelPart(row, i) {
477
+ const rawStart = row.seatLabelStart ?? 1;
515
478
  const dir = row.seatNumbering?.direction ?? "ltr";
516
479
  const step = row.seatNumbering?.step ?? 1;
480
+ const scheme = row.seatNumbering?.scheme ?? "decimal";
481
+ const prefix = row.seatNumbering?.prefix ?? "";
482
+ const endAt = row.seatNumbering?.endAt;
517
483
  const n = row.seatCount;
518
- let seatNumber;
519
- if (dir === "center") {
520
- const rank = new Array(n);
521
- 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);
522
- seatNumber = (i) => start + rank[i] * step;
523
- } else {
524
- seatNumber = (i) => start + (dir === "rtl" ? n - 1 - i : i) * step;
484
+ if (scheme === "updown") {
485
+ const half = Math.ceil(n / 2);
486
+ const core2 = i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i);
487
+ return `${prefix}${core2}`;
488
+ }
489
+ const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
490
+ const start = endAt != null && Number.isFinite(endAt) ? endAt - (n - 1) * effStep : rawStart;
491
+ const p = dir === "center" ? centerRank(n)[i] : dir === "rtl" ? n - 1 - i : i;
492
+ let core;
493
+ switch (scheme) {
494
+ case "odd": {
495
+ const firstOdd = start % 2 === 1 ? start : start + 1;
496
+ core = String(firstOdd + p * 2);
497
+ break;
498
+ }
499
+ case "even": {
500
+ const firstEven = start % 2 === 0 ? start : start + 1;
501
+ core = String(firstEven + p * 2);
502
+ break;
503
+ }
504
+ case "roman":
505
+ core = toRoman(start + p * step);
506
+ break;
507
+ case "letters-upper":
508
+ core = toLetters(start + p * step, false);
509
+ break;
510
+ case "letters-lower":
511
+ core = toLetters(start + p * step, true);
512
+ break;
513
+ case "decimal":
514
+ default:
515
+ core = String(start + p * step);
516
+ break;
525
517
  }
518
+ return `${prefix}${core}`;
519
+ }
520
+ function expandRowSlots(row) {
526
521
  const ov = overrideMap(row);
527
522
  return rowSeatPositions(row).map((p, i) => {
528
523
  const o = ov.get(i);
529
524
  const accessibility = overrideAccessibility(o);
530
- const inventoryLabel = o?.label ?? `${row.label}-${seatNumber(i)}`;
525
+ const part = seatLabelPart(row, i);
526
+ const inventoryLabel = o?.label ?? `${row.label}-${part}`;
531
527
  const displayPrefix = row.displayLabel ?? row.label;
532
528
  const commercial = { ...row.commercial, ...o?.commercial };
533
529
  return {
@@ -535,7 +531,7 @@ function expandRowSlots(row) {
535
531
  x: p.x + (o?.dx ?? 0),
536
532
  y: p.y + (o?.dy ?? 0),
537
533
  label: inventoryLabel,
538
- displayLabel: o?.displayLabel ?? `${displayPrefix}-${seatNumber(i)}`,
534
+ displayLabel: o?.displayLabel ?? `${displayPrefix}-${part}`,
539
535
  categoryKey: o?.categoryKey ?? row.categoryKey,
540
536
  skipped: !!o?.skip,
541
537
  accessible: accessibility.length > 0,
@@ -992,19 +988,6 @@ function applyHidden(doc, hidden) {
992
988
  return { ...doc, objects };
993
989
  }
994
990
 
995
- // src/engine/SeatmapRenderer.ts
996
- var import_Core = require("konva/lib/Core");
997
- var import_Stage = require("konva/lib/Stage");
998
- var import_Layer = require("konva/lib/Layer");
999
- var import_Group = require("konva/lib/Group");
1000
- var import_Circle = require("konva/lib/shapes/Circle");
1001
- var import_Rect = require("konva/lib/shapes/Rect");
1002
- var import_Ellipse = require("konva/lib/shapes/Ellipse");
1003
- var import_Line = require("konva/lib/shapes/Line");
1004
- var import_Text = require("konva/lib/shapes/Text");
1005
- var import_Image = require("konva/lib/shapes/Image");
1006
- var import_Shape = require("konva/lib/Shape");
1007
-
1008
991
  // src/core/chartRenderRules.ts
1009
992
  var SEAT_LABEL_FONT_SIZE = 7;
1010
993
  var BOOTH_LABEL_FONT_SIZE = 10;
@@ -1018,6 +1001,8 @@ var LIGHT_BOOKABLE_LABEL_INK = "#ffffff";
1018
1001
  function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
1019
1002
  return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
1020
1003
  }
1004
+ var ACCESS_GLYPH_VIEWBOX = 24;
1005
+ 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";
1021
1006
  function bookableMarkerLabel(publicLabel) {
1022
1007
  return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
1023
1008
  }
@@ -1059,6 +1044,470 @@ function stateAwareBookableLabelInk(fill, preferred) {
1059
1044
  return darkContrast >= lightContrast ? DARK_BOOKABLE_LABEL_INK : LIGHT_BOOKABLE_LABEL_INK;
1060
1045
  }
1061
1046
 
1047
+ // src/core/renderedQuality.ts
1048
+ var RENDERED_QUALITY_REPORT_VERSION = 3;
1049
+ var MIN_TEXT_CONTRAST = 4.5;
1050
+ var MIN_GRAPHICAL_CONTRAST = 3;
1051
+ var MIN_POINTER_TARGET_PX = 24;
1052
+ var MAX_SAMPLES_PER_FINDING = 20;
1053
+ function visibleWithBox(items) {
1054
+ return items.filter((item) => item.visible && Boolean(item.screenBox));
1055
+ }
1056
+ function intersects(left, right) {
1057
+ return left.x < right.x + right.width && left.x + left.width > right.x && left.y < right.y + right.height && left.y + left.height > right.y;
1058
+ }
1059
+ function minimum(values) {
1060
+ return values.length ? Math.round(Math.min(...values) * 100) / 100 : null;
1061
+ }
1062
+ function finding(code, message, samples) {
1063
+ if (!samples.length) return null;
1064
+ return {
1065
+ code,
1066
+ message,
1067
+ count: samples.length,
1068
+ samples: samples.slice(0, MAX_SAMPLES_PER_FINDING)
1069
+ };
1070
+ }
1071
+ function labelSample(label, measured, minimumValue) {
1072
+ return {
1073
+ primaryId: label.seatId,
1074
+ primaryLabel: label.label,
1075
+ ...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
1076
+ ...minimumValue == null ? {} : { minimum: minimumValue }
1077
+ };
1078
+ }
1079
+ function hierarchySample(label, measured, minimumValue) {
1080
+ return {
1081
+ primaryId: label.id,
1082
+ primaryLabel: label.label,
1083
+ ...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
1084
+ ...minimumValue == null ? {} : { minimum: minimumValue }
1085
+ };
1086
+ }
1087
+ function freeTextSample(label, measured, minimumValue) {
1088
+ return {
1089
+ primaryId: label.objectId,
1090
+ primaryLabel: label.text,
1091
+ ...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
1092
+ ...minimumValue == null ? {} : { minimum: minimumValue }
1093
+ };
1094
+ }
1095
+ function collisionSample(primaryId, primaryLabel, secondaryId, secondaryLabel) {
1096
+ return { primaryId, primaryLabel, secondaryId, secondaryLabel };
1097
+ }
1098
+ function inspectRenderedQualityEvidence(evidence, state = "overview", targetCategoryKey = null) {
1099
+ const bookable = visibleWithBox(evidence.labels);
1100
+ const hierarchy = visibleWithBox(evidence.hierarchyLabels);
1101
+ const freeText = visibleWithBox(evidence.freeTextLabels);
1102
+ const visibleGA = evidence.gaAreas.filter((area) => area.visible);
1103
+ const bookableContrast = bookable.map((label) => renderedTextContrast(label.ink, label.fill) ?? 0);
1104
+ const hierarchyContrast = hierarchy.map((label) => renderedTextContrast(label.ink, label.fill) ?? 0);
1105
+ const freeTextContrast = freeText.map((label) => renderedTextContrast(label.ink, label.background) ?? 0);
1106
+ const gaContrast = visibleGA.map((area) => renderedTextContrast(area.effectiveBackground, evidence.canvasBackground) ?? 0);
1107
+ const targetLabels = targetCategoryKey ? evidence.labels.filter((label) => label.categoryKey === targetCategoryKey) : evidence.labels;
1108
+ const targetGAAreas = targetCategoryKey ? evidence.gaAreas.filter((area) => area.categoryKey === targetCategoryKey) : evidence.gaAreas;
1109
+ const selected = targetLabels.filter((label) => label.selected);
1110
+ const held = targetLabels.filter((label) => label.status === "held");
1111
+ const booked = targetLabels.filter((label) => label.status === "booked");
1112
+ const activePointerTargets = targetLabels.filter((label) => label.pointerTarget.active);
1113
+ const activeCategoryKeys = /* @__PURE__ */ new Set([
1114
+ ...evidence.labels.filter((label) => label.visible && (label.opacity > 0.25 || label.selected || label.status !== "free")).map((label) => label.categoryKey),
1115
+ ...evidence.gaAreas.filter((area) => area.visible && area.opacity > 0.1).map((area) => area.categoryKey)
1116
+ ]);
1117
+ const selectedRingContrast = selected.length ? minimum(selected.flatMap((label) => [
1118
+ renderedTextContrast(evidence.selectionRingColor, evidence.canvasBackground) ?? 0,
1119
+ renderedTextContrast(evidence.selectionRingColor, label.fill) ?? 0
1120
+ ])) : null;
1121
+ const findings = [];
1122
+ const overviewCountSamples = (count, label) => Array.from({ length: count }, (_, index) => ({
1123
+ primaryId: `overview:${index + 1}`,
1124
+ primaryLabel: label
1125
+ }));
1126
+ if (state === "overview" && evidence.rung === "sections") {
1127
+ findings.push(finding(
1128
+ "overview-section-category-paint",
1129
+ "Section overview shells must use the neutral hierarchy palette, not category paint",
1130
+ overviewCountSamples(evidence.overviewStyle.categoryPaintedSectionShells, "category-painted section shell")
1131
+ ));
1132
+ findings.push(finding(
1133
+ "overview-category-detail-visible",
1134
+ "Category-tinted section detail must wait until section focus or seat zoom",
1135
+ overviewCountSamples(evidence.overviewStyle.visibleCategoryDetailOutlines, "category detail outline")
1136
+ ));
1137
+ findings.push(finding(
1138
+ "overview-row-hints-visible",
1139
+ "Row and seat patterns must not clutter the section overview",
1140
+ overviewCountSamples(evidence.overviewStyle.visibleSectionRowHints, "section row hint")
1141
+ ));
1142
+ findings.push(finding(
1143
+ "overview-availability-clutter",
1144
+ "Live availability counts belong in focused detail, not on section overview shells",
1145
+ overviewCountSamples(evidence.overviewStyle.visibleSectionAvailabilityLabels, "section availability label")
1146
+ ));
1147
+ findings.push(finding(
1148
+ "overview-ga-detail-visible",
1149
+ "Section-contained standing paint belongs in focused detail, not on section overview shells",
1150
+ overviewCountSamples(evidence.overviewStyle.visibleSectionGADetails, "section-contained GA detail")
1151
+ ));
1152
+ }
1153
+ findings.push(finding(
1154
+ "bookable-label-undersized",
1155
+ "Visible bookable labels must meet the rendered small-text size floor",
1156
+ bookable.filter((label) => label.renderedFontPx < evidence.minimumVisibleLabelPx).map((label) => labelSample(label, label.renderedFontPx, evidence.minimumVisibleLabelPx))
1157
+ ));
1158
+ if (state === "interaction") {
1159
+ const labelledInventory = targetLabels.length > 0;
1160
+ const gaInventory = targetGAAreas.length > 0;
1161
+ const detailInventory = labelledInventory || gaInventory;
1162
+ const visibleTargetGA = targetGAAreas.filter((area) => area.visible);
1163
+ const sections = /* @__PURE__ */ new Set([
1164
+ ...targetLabels.flatMap((label) => label.sectionId ? [label.sectionId] : []),
1165
+ ...targetGAAreas.flatMap((area) => area.sectionId ? [area.sectionId] : [])
1166
+ ]);
1167
+ const categories = /* @__PURE__ */ new Set([
1168
+ ...evidence.labels.map((label) => label.categoryKey),
1169
+ ...evidence.gaAreas.map((area) => area.categoryKey)
1170
+ ]);
1171
+ const inViewport = (label) => label.screenCenter.x >= 0 && label.screenCenter.x <= evidence.viewport.width && label.screenCenter.y >= 0 && label.screenCenter.y <= evidence.viewport.height;
1172
+ findings.push(finding(
1173
+ "detail-rung-missing",
1174
+ "Interaction evidence with bookable inventory must render the seat-detail rung",
1175
+ detailInventory && evidence.rung !== "seats" ? [{ primaryId: "renderer", primaryLabel: evidence.rung }] : []
1176
+ ));
1177
+ findings.push(finding(
1178
+ "detail-inventory-not-visible",
1179
+ "Interaction evidence must frame at least one real bookable unit",
1180
+ detailInventory && !targetLabels.some(inViewport) && !visibleTargetGA.length ? [{ primaryId: "renderer", primaryLabel: "no target inventory in viewport" }] : []
1181
+ ));
1182
+ findings.push(finding(
1183
+ "pointer-target-inactive",
1184
+ "Interaction evidence must expose a live production pointer target",
1185
+ labelledInventory && !activePointerTargets.some(inViewport) || !labelledInventory && gaInventory && !visibleTargetGA.some((area) => area.interactive) ? [{ primaryId: "renderer", primaryLabel: "no active pointer target in viewport" }] : []
1186
+ ));
1187
+ findings.push(finding(
1188
+ "pointer-target-undersized",
1189
+ "Every active production pointer target must reach at least 24 CSS pixels",
1190
+ activePointerTargets.filter((label) => inViewport(label) && label.pointerTarget.effectiveMinimumPx < MIN_POINTER_TARGET_PX).map((label) => labelSample(label, label.pointerTarget.effectiveMinimumPx, MIN_POINTER_TARGET_PX))
1191
+ ));
1192
+ findings.push(finding(
1193
+ "selected-state-missing",
1194
+ "Interaction evidence must paint a selected unit and its renderer-owned ring",
1195
+ labelledInventory && (!selected.length || !selected.some((label) => evidence.selectionRingSeatIds.includes(label.seatId))) ? [{ primaryId: "renderer", primaryLabel: "selected state" }] : []
1196
+ ));
1197
+ findings.push(finding(
1198
+ "selected-state-contrast-low",
1199
+ "The selected-state ring must maintain 3:1 graphical contrast with the canvas",
1200
+ selected.length && (selectedRingContrast ?? 0) < MIN_GRAPHICAL_CONTRAST ? [{
1201
+ primaryId: selected[0].seatId,
1202
+ primaryLabel: selected[0].label,
1203
+ measured: Math.round((selectedRingContrast ?? 0) * 100) / 100,
1204
+ minimum: MIN_GRAPHICAL_CONTRAST
1205
+ }] : []
1206
+ ));
1207
+ findings.push(finding(
1208
+ "held-state-missing",
1209
+ "Interaction evidence must paint a held state when the floor has at least two status-managed units",
1210
+ targetLabels.length >= 2 && !held.length ? [{ primaryId: "renderer", primaryLabel: "held state" }] : []
1211
+ ));
1212
+ findings.push(finding(
1213
+ "booked-state-missing",
1214
+ "Interaction evidence must paint a taken state when the floor has at least three status-managed units",
1215
+ targetLabels.length >= 3 && !booked.length ? [{ primaryId: "renderer", primaryLabel: "booked state" }] : []
1216
+ ));
1217
+ const heldSignatures = new Set(held.map((label) => `${label.fill.toLowerCase()}:${label.opacity}`));
1218
+ const bookedSignatures = new Set(booked.map((label) => `${label.fill.toLowerCase()}:${label.opacity}`));
1219
+ findings.push(finding(
1220
+ "status-state-indistinct",
1221
+ "Held and taken evidence must resolve to distinct renderer paint",
1222
+ held.length && booked.length && [...heldSignatures].some((signature) => bookedSignatures.has(signature)) ? [{ primaryId: held[0].seatId, primaryLabel: held[0].label, secondaryId: booked[0].seatId, secondaryLabel: booked[0].label }] : []
1223
+ ));
1224
+ findings.push(finding(
1225
+ "section-focus-missing",
1226
+ "Interaction evidence must exercise section focus and its backdrop when section membership exists",
1227
+ sections.size && (!evidence.focusedSectionId || !evidence.focusBackdropVisible) ? [{ primaryId: "renderer", primaryLabel: "section focus" }] : []
1228
+ ));
1229
+ findings.push(finding(
1230
+ "category-filter-missing",
1231
+ "Interaction evidence must exercise a category filter when multiple categories exist",
1232
+ categories.size >= 2 && (!evidence.categoryFilterKeys || !evidence.categoryFilterKeys.length) ? [{ primaryId: "renderer", primaryLabel: "category filter" }] : []
1233
+ ));
1234
+ const excludedFree = evidence.labels.filter((label) => label.status === "free" && !label.selected && evidence.categoryFilterKeys != null && !evidence.categoryFilterKeys.includes(label.categoryKey));
1235
+ const excludedGA = evidence.gaAreas.filter((area) => evidence.categoryFilterKeys != null && !evidence.categoryFilterKeys.includes(area.categoryKey));
1236
+ findings.push(finding(
1237
+ "category-filter-ineffective",
1238
+ "The active category filter must visibly dim excluded free inventory",
1239
+ (excludedFree.length || excludedGA.length) && !excludedFree.some((label) => label.opacity <= 0.25) && !excludedGA.some((area) => area.opacity <= 0.1) ? [
1240
+ ...excludedFree.map((label) => labelSample(label, label.opacity)),
1241
+ ...excludedGA.map((area) => ({ primaryId: area.areaId, primaryLabel: area.label, measured: area.opacity }))
1242
+ ] : []
1243
+ ));
1244
+ findings.push(finding(
1245
+ "target-category-not-visible",
1246
+ "A category-specific interaction scene must visibly paint its exact target category",
1247
+ targetCategoryKey && !activeCategoryKeys.has(targetCategoryKey) ? [{ primaryId: targetCategoryKey, primaryLabel: targetCategoryKey }] : []
1248
+ ));
1249
+ findings.push(finding(
1250
+ "target-category-filter-mismatch",
1251
+ "A category-specific interaction scene must bind its filter to only the exact target category",
1252
+ targetCategoryKey && (evidence.categoryFilterKeys?.length !== 1 || evidence.categoryFilterKeys[0] !== targetCategoryKey) ? [{ primaryId: targetCategoryKey, primaryLabel: targetCategoryKey }] : []
1253
+ ));
1254
+ }
1255
+ findings.push(finding(
1256
+ "bookable-label-contrast-low",
1257
+ "Visible bookable labels must meet 4.5:1 contrast against their actual paint",
1258
+ bookable.flatMap((label) => {
1259
+ const ratio = renderedTextContrast(label.ink, label.fill) ?? 0;
1260
+ return ratio < MIN_TEXT_CONTRAST ? [labelSample(label, ratio, MIN_TEXT_CONTRAST)] : [];
1261
+ })
1262
+ ));
1263
+ const bookableCollisions = [];
1264
+ for (let index = 0; index < bookable.length; index += 1) {
1265
+ for (let other = 0; other < index; other += 1) {
1266
+ if (intersects(bookable[index].screenBox, bookable[other].screenBox)) {
1267
+ bookableCollisions.push(collisionSample(
1268
+ bookable[other].seatId,
1269
+ bookable[other].label,
1270
+ bookable[index].seatId,
1271
+ bookable[index].label
1272
+ ));
1273
+ }
1274
+ }
1275
+ }
1276
+ findings.push(finding(
1277
+ "bookable-label-collision",
1278
+ "Visible bookable labels must not overlap",
1279
+ bookableCollisions
1280
+ ));
1281
+ findings.push(finding(
1282
+ "hierarchy-label-undersized",
1283
+ "Visible section and zone labels must meet the rendered small-text size floor",
1284
+ hierarchy.filter((label) => label.renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX).map((label) => hierarchySample(label, label.renderedFontPx, MIN_VISIBLE_BOOKABLE_LABEL_PX))
1285
+ ));
1286
+ findings.push(finding(
1287
+ "hierarchy-label-contrast-low",
1288
+ "Visible section and zone labels must meet 4.5:1 contrast against their backing paint",
1289
+ hierarchy.flatMap((label) => {
1290
+ const ratio = renderedTextContrast(label.ink, label.fill) ?? 0;
1291
+ return ratio < MIN_TEXT_CONTRAST ? [hierarchySample(label, ratio, MIN_TEXT_CONTRAST)] : [];
1292
+ })
1293
+ ));
1294
+ findings.push(finding(
1295
+ "hierarchy-label-outside-section",
1296
+ "Visible section labels must remain inside the filled section surface and outside holes",
1297
+ hierarchy.filter((label) => label.kind === "section" && label.fitsContainer === false).map((label) => hierarchySample(label))
1298
+ ));
1299
+ const hierarchyCollisions = [];
1300
+ for (let index = 0; index < hierarchy.length; index += 1) {
1301
+ for (let other = 0; other < index; other += 1) {
1302
+ if (intersects(hierarchy[index].screenBox, hierarchy[other].screenBox)) {
1303
+ hierarchyCollisions.push(collisionSample(
1304
+ hierarchy[other].id,
1305
+ hierarchy[other].label,
1306
+ hierarchy[index].id,
1307
+ hierarchy[index].label
1308
+ ));
1309
+ }
1310
+ }
1311
+ }
1312
+ findings.push(finding(
1313
+ "hierarchy-label-collision",
1314
+ "Visible hierarchy labels must not overlap each other",
1315
+ hierarchyCollisions
1316
+ ));
1317
+ const hierarchyBookableCollisions = [];
1318
+ for (const upper of hierarchy) {
1319
+ for (const unit of bookable) {
1320
+ if (intersects(upper.screenBox, unit.screenBox)) {
1321
+ hierarchyBookableCollisions.push(collisionSample(upper.id, upper.label, unit.seatId, unit.label));
1322
+ }
1323
+ }
1324
+ }
1325
+ findings.push(finding(
1326
+ "hierarchy-bookable-collision",
1327
+ "Visible hierarchy labels must not overlap bookable labels",
1328
+ hierarchyBookableCollisions
1329
+ ));
1330
+ findings.push(finding(
1331
+ "free-text-undersized",
1332
+ "Visible chart text must meet the rendered small-text size floor",
1333
+ freeText.filter((label) => label.renderedFontPx < evidence.minimumVisibleLabelPx).map((label) => freeTextSample(label, label.renderedFontPx, evidence.minimumVisibleLabelPx))
1334
+ ));
1335
+ findings.push(finding(
1336
+ "free-text-contrast-low",
1337
+ "Visible chart text must meet 4.5:1 contrast against its measured background",
1338
+ freeText.flatMap((label) => {
1339
+ const ratio = renderedTextContrast(label.ink, label.background) ?? 0;
1340
+ return ratio < MIN_TEXT_CONTRAST ? [freeTextSample(label, ratio, MIN_TEXT_CONTRAST)] : [];
1341
+ })
1342
+ ));
1343
+ const freeTextCollisions = [];
1344
+ for (let index = 0; index < freeText.length; index += 1) {
1345
+ for (let other = 0; other < index; other += 1) {
1346
+ if (intersects(freeText[index].screenBox, freeText[other].screenBox)) {
1347
+ freeTextCollisions.push(collisionSample(
1348
+ freeText[other].objectId,
1349
+ freeText[other].text,
1350
+ freeText[index].objectId,
1351
+ freeText[index].text
1352
+ ));
1353
+ }
1354
+ }
1355
+ }
1356
+ findings.push(finding(
1357
+ "free-text-collision",
1358
+ "Visible chart text labels must not overlap each other",
1359
+ freeTextCollisions
1360
+ ));
1361
+ const freeTextBookableCollisions = [];
1362
+ for (const text of freeText) {
1363
+ for (const unit of bookable) {
1364
+ if (intersects(text.screenBox, unit.screenBox)) {
1365
+ freeTextBookableCollisions.push(collisionSample(text.objectId, text.text, unit.seatId, unit.label));
1366
+ }
1367
+ }
1368
+ }
1369
+ findings.push(finding(
1370
+ "free-text-bookable-collision",
1371
+ "Visible chart text must not overlap bookable labels",
1372
+ freeTextBookableCollisions
1373
+ ));
1374
+ const freeTextHierarchyCollisions = [];
1375
+ for (const text of freeText) {
1376
+ for (const upper of hierarchy) {
1377
+ if (intersects(text.screenBox, upper.screenBox)) {
1378
+ freeTextHierarchyCollisions.push(collisionSample(text.objectId, text.text, upper.id, upper.label));
1379
+ }
1380
+ }
1381
+ }
1382
+ findings.push(finding(
1383
+ "free-text-hierarchy-collision",
1384
+ "Visible chart text must not overlap hierarchy labels",
1385
+ freeTextHierarchyCollisions
1386
+ ));
1387
+ findings.push(finding(
1388
+ "ga-contrast-low",
1389
+ "Visible GA surfaces must maintain 3:1 graphical contrast with the canvas",
1390
+ visibleGA.flatMap((area) => {
1391
+ const ratio = renderedTextContrast(area.effectiveBackground, evidence.canvasBackground) ?? 0;
1392
+ return ratio < MIN_GRAPHICAL_CONTRAST ? [{
1393
+ primaryId: area.areaId,
1394
+ primaryLabel: area.label,
1395
+ measured: Math.round(ratio * 100) / 100,
1396
+ minimum: MIN_GRAPHICAL_CONTRAST
1397
+ }] : [];
1398
+ })
1399
+ ));
1400
+ const materialFindings = findings.filter((item) => Boolean(item));
1401
+ return {
1402
+ version: RENDERED_QUALITY_REPORT_VERSION,
1403
+ passed: materialFindings.length === 0,
1404
+ state,
1405
+ targetCategoryKey,
1406
+ resolvedRules: [
1407
+ "rendered-overview-label-size",
1408
+ "rendered-overview-label-contrast",
1409
+ "rendered-overview-label-collision",
1410
+ "rendered-overview-hierarchy-containment",
1411
+ "rendered-overview-section-first-style",
1412
+ "rendered-overview-ga-contrast",
1413
+ ...state === "interaction" ? [
1414
+ "rendered-detail-inventory",
1415
+ "rendered-pointer-target",
1416
+ "rendered-selected-held-taken-states",
1417
+ "rendered-section-focus",
1418
+ "rendered-category-filter"
1419
+ ] : []
1420
+ ],
1421
+ viewport: evidence.viewport,
1422
+ canvasBackground: evidence.canvasBackground,
1423
+ effectiveScale: evidence.effectiveScale,
1424
+ rung: evidence.rung,
1425
+ inventory: {
1426
+ totalBookableUnits: evidence.totalBookableUnits,
1427
+ totalLabelledBookableUnits: evidence.totalLabelledBookableUnits,
1428
+ visibleBookableLabels: bookable.length,
1429
+ hiddenBookableLabels: evidence.hiddenLabels,
1430
+ visibleHierarchyLabels: hierarchy.length,
1431
+ visibleFreeTextLabels: freeText.length,
1432
+ visibleGAAreas: visibleGA.length
1433
+ },
1434
+ overviewStyle: evidence.overviewStyle,
1435
+ composition: {
1436
+ hierarchy: evidence.hierarchyLabels.filter((label) => label.role === "name").map((label) => ({
1437
+ id: label.id,
1438
+ kind: label.kind,
1439
+ label: label.label,
1440
+ visible: label.visible
1441
+ })).sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id)),
1442
+ labelledObjects: evidence.freeTextLabels.map((label) => ({
1443
+ objectId: label.objectId,
1444
+ kind: label.kind,
1445
+ text: label.text,
1446
+ visible: label.visible
1447
+ })).sort((left, right) => left.objectId.localeCompare(right.objectId) || left.kind.localeCompare(right.kind)),
1448
+ gaAreas: evidence.gaAreas.map((area) => ({
1449
+ areaId: area.areaId,
1450
+ label: area.label,
1451
+ categoryKey: area.categoryKey,
1452
+ ...area.sectionId ? { sectionId: area.sectionId } : {},
1453
+ visible: area.visible
1454
+ })).sort((left, right) => left.areaId.localeCompare(right.areaId)),
1455
+ bookableSectionIds: [...new Set(evidence.labels.flatMap((label) => label.sectionId ? [label.sectionId] : []))].sort(),
1456
+ categoryKeys: [.../* @__PURE__ */ new Set([
1457
+ ...evidence.labels.map((label) => label.categoryKey),
1458
+ ...evidence.gaAreas.map((area) => area.categoryKey)
1459
+ ])].sort(),
1460
+ activeCategoryKeys: [...activeCategoryKeys].sort()
1461
+ },
1462
+ metrics: {
1463
+ minimumRenderedBookableLabelPx: minimum(bookable.map((label) => label.renderedFontPx)),
1464
+ minimumBookableLabelContrast: minimum(bookableContrast),
1465
+ minimumRenderedHierarchyLabelPx: minimum(hierarchy.map((label) => label.renderedFontPx)),
1466
+ minimumHierarchyLabelContrast: minimum(hierarchyContrast),
1467
+ minimumRenderedFreeTextPx: minimum(freeText.map((label) => label.renderedFontPx)),
1468
+ minimumFreeTextContrast: minimum(freeTextContrast),
1469
+ minimumGAContrast: minimum(gaContrast),
1470
+ minimumEffectivePointerTargetPx: minimum(activePointerTargets.map((label) => label.pointerTarget.effectiveMinimumPx)),
1471
+ selectedRingContrast: selectedRingContrast == null ? null : Math.round(selectedRingContrast * 100) / 100
1472
+ },
1473
+ interaction: {
1474
+ applicable: {
1475
+ detail: targetLabels.length > 0 || targetGAAreas.length > 0,
1476
+ pointer: targetLabels.length > 0 || targetGAAreas.length > 0,
1477
+ held: targetLabels.length >= 2,
1478
+ booked: targetLabels.length >= 3,
1479
+ sectionFocus: targetLabels.some((label) => Boolean(label.sectionId)) || targetGAAreas.some((area) => Boolean(area.sectionId)),
1480
+ categoryFilter: (/* @__PURE__ */ new Set([
1481
+ ...evidence.labels.map((label) => label.categoryKey),
1482
+ ...evidence.gaAreas.map((area) => area.categoryKey)
1483
+ ])).size >= 2
1484
+ },
1485
+ selectedUnits: selected.length,
1486
+ heldUnits: held.length,
1487
+ bookedUnits: booked.length,
1488
+ activePointerTargets: activePointerTargets.length,
1489
+ focusedSectionId: evidence.focusedSectionId,
1490
+ focusBackdropVisible: evidence.focusBackdropVisible,
1491
+ categoryFilterKeys: evidence.categoryFilterKeys
1492
+ },
1493
+ findings: materialFindings
1494
+ };
1495
+ }
1496
+
1497
+ // src/engine/SeatmapRenderer.ts
1498
+ var import_Core = require("konva/lib/Core");
1499
+ var import_Stage = require("konva/lib/Stage");
1500
+ var import_Layer = require("konva/lib/Layer");
1501
+ var import_Group = require("konva/lib/Group");
1502
+ var import_Circle = require("konva/lib/shapes/Circle");
1503
+ var import_Rect = require("konva/lib/shapes/Rect");
1504
+ var import_Ellipse = require("konva/lib/shapes/Ellipse");
1505
+ var import_Line = require("konva/lib/shapes/Line");
1506
+ var import_Text = require("konva/lib/shapes/Text");
1507
+ var import_Path = require("konva/lib/shapes/Path");
1508
+ var import_Image = require("konva/lib/shapes/Image");
1509
+ var import_Shape = require("konva/lib/Shape");
1510
+
1062
1511
  // src/lib/money.ts
1063
1512
  var DEFAULT_CURRENCY = "USD";
1064
1513
  var displayLocale;
@@ -1097,29 +1546,17 @@ var en = {
1097
1546
  "common.done": "Done",
1098
1547
  "common.copied": "\u2713 Copied",
1099
1548
  // buyer picker
1100
- "picker.holdSeats": "Hold seats & checkout",
1101
- "picker.completeBooking": "Complete booking",
1102
- "picker.seatsHeld": "Seats held \u2014 {time}",
1103
1549
  "picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
1104
- "picker.seatTaken": "Seat {label} was just taken by another buyer.",
1105
1550
  "picker.poweredBy": "Powered by SeatLayer",
1106
1551
  "picker.testMode": "TEST MODE",
1107
- "picker.colorblind": "Colorblind-friendly colors",
1108
1552
  "picker.orphanHint": "This leaves a single seat stranded \u2014 consider shifting one seat over.",
1109
- "picker.seats.one": "{count} seat",
1110
- "picker.seats.other": "{count} seats",
1111
1553
  // renderer (drawn on the Konva map — shared by the embed SDK)
1112
1554
  "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
1113
1555
  "map.seatsLeft": "{count} LEFT",
1114
1556
  "map.fromPrice": "FROM {price}",
1115
1557
  "map.statusHeld": "On hold",
1116
1558
  "map.statusTaken": "Taken",
1117
- // buyer picker page (src/pages/PickerPage.tsx)
1118
- "picker.language": "Language",
1119
- "picker.zoomToFit": "Zoom to fit",
1120
- "picker.seatCountLabel": "seats",
1121
- "picker.capacity": "capacity",
1122
- "picker.viewMode": "View mode",
1559
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
1123
1560
  "picker.floor": "Floor",
1124
1561
  "picker.zoomLevel": "Zoom level",
1125
1562
  "picker.rungTip.zones": "Venue overview \u2014 groups of sections such as North Stand or VIP",
@@ -1133,32 +1570,13 @@ var en = {
1133
1570
  "picker.seatsLeftInSection.one": "{count} seat left",
1134
1571
  "picker.seatsLeftInSection.other": "{count} seats left",
1135
1572
  "picker.overview": "Overview",
1573
+ "picker.entrance": "Entrance",
1136
1574
  "picker.tapSeatHint": "Tap any seat to check its view",
1137
- "picker.chartSize": "Chart size",
1138
- "picker.custom": "Custom",
1139
- "picker.categories": "Categories",
1140
- "picker.accessibility": "Accessibility",
1141
- "picker.showAnyAccessible": "Show any accessible seat",
1142
- "picker.any": "Any",
1143
- "picker.yourSeats": "Your seats",
1144
- "picker.emptySeats": "Tap seats on the map",
1145
- "picker.emptySeatsWithGa": "Tap seats on the map \xB7 tap a standing area for GA tickets",
1146
- "picker.oneFewer": "One fewer",
1147
- "picker.oneMore": "One more",
1148
- "picker.remove": "Remove {label}",
1149
1575
  "picker.ticketTierFor": "Ticket tier for {label}",
1150
- "picker.total": "Total: {amount}",
1151
1576
  "picker.viewFromSeat": "View from seat {label}",
1152
1577
  "picker.real360": "REAL 360\xB0",
1153
1578
  "picker.preview": "PREVIEW",
1154
- "picker.open360": "Open 360\xB0 view",
1155
1579
  "picker.sightline": "\u2248 {m} m to stage \xB7 clear sightline",
1156
- "picker.bookedDemo": "Booked! (demo)",
1157
- "picker.bookButton.one": "Book {count} ticket \u2014 {amount}",
1158
- "picker.bookButton.other": "Book {count} tickets \u2014 {amount}",
1159
- "picker.simulateCrowd": "Simulate crowd: {state}",
1160
- "picker.on": "ON",
1161
- "picker.off": "OFF",
1162
1580
  "picker.panorama360": "360\xB0 venue photo",
1163
1581
  "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage",
1164
1582
  "picker.restrictedView": "Restricted view",
@@ -1217,6 +1635,7 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
1217
1635
  var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
1218
1636
  var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
1219
1637
  var SEAT_TAP_SLOP_PX = 14;
1638
+ var SEAT_GLYPH_MIN_PX = 6.5;
1220
1639
  var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
1221
1640
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
1222
1641
  var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
@@ -1506,6 +1925,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1506
1925
  this.boothLabelById = /* @__PURE__ */ new Map();
1507
1926
  /** Viewport seat labels are rebuilt after each settled camera change. */
1508
1927
  this.seatLabelById = /* @__PURE__ */ new Map();
1928
+ /** Coloured accommodation ring per accessible seat (few per chart). */
1929
+ this.accessRingById = /* @__PURE__ */ new Map();
1930
+ /** Centred accessibility glyph per accessible seat — shown once the seat is
1931
+ * big enough on-screen (see {@link SEAT_GLYPH_MIN_PX}); the ring is the
1932
+ * smaller-zoom fallback. Kept in a map so zoom toggles touch only the handful
1933
+ * of accessible seats, never all 13k nodes. */
1934
+ this.accessGlyphById = /* @__PURE__ */ new Map();
1935
+ /** Whether the accessibility glyph is legible at the current camera scale. */
1936
+ this.accessGlyphVisible = false;
1509
1937
  /** Authored free-text nodes obey the same rendered-size visibility floor. */
1510
1938
  this.freeTextById = /* @__PURE__ */ new Map();
1511
1939
  /** Stage/rink landmarks retain a readable screen-space caption at overview. */
@@ -1818,6 +2246,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1818
2246
  this.boothDims.clear();
1819
2247
  this.boothLabelById.clear();
1820
2248
  this.seatLabelById.clear();
2249
+ this.accessRingById.clear();
2250
+ this.accessGlyphById.clear();
1821
2251
  this.freeTextById.clear();
1822
2252
  this.primaryFocalLabels.clear();
1823
2253
  this.gaById.clear();
@@ -2726,18 +3156,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2726
3156
  this.paintSeat(c, seat.id);
2727
3157
  target.add(c);
2728
3158
  if (seat.accessible) {
2729
- target.add(
2730
- new import_Circle.Circle({
2731
- x: seat.x,
2732
- y: seat.y,
2733
- radius: this.seatR + 1,
2734
- stroke: accessibilityRingColor(seat.accessibility),
2735
- strokeWidth: 2,
2736
- listening: false,
2737
- perfectDrawEnabled: false,
2738
- shadowForStrokeEnabled: false
2739
- })
2740
- );
3159
+ const ring = new import_Circle.Circle({
3160
+ x: seat.x,
3161
+ y: seat.y,
3162
+ radius: this.seatR + 1.5,
3163
+ stroke: accessibilityRingColor(seat.accessibility),
3164
+ strokeWidth: 2.5,
3165
+ listening: false,
3166
+ perfectDrawEnabled: false,
3167
+ shadowForStrokeEnabled: false
3168
+ });
3169
+ this.accessRingById.set(seat.id, ring);
3170
+ target.add(ring);
3171
+ const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
3172
+ this.accessGlyphById.set(seat.id, glyph);
3173
+ target.add(glyph);
2741
3174
  }
2742
3175
  }
2743
3176
  }
@@ -2781,6 +3214,53 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2781
3214
  target.add(t2);
2782
3215
  this.paintSeat(rect, seat.id);
2783
3216
  }
3217
+ /**
3218
+ * Build the centred accessibility glyph for a seat. Sized relative to the seat
3219
+ * radius (so it always fills the marker at any zoom) and given a contrast-aware
3220
+ * fill against the seat's paint — recomputed per state in {@link paintSeat}.
3221
+ */
3222
+ buildAccessGlyph(seat, seatFill) {
3223
+ const k = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX;
3224
+ return new import_Path.Path({
3225
+ x: seat.x,
3226
+ y: seat.y,
3227
+ data: ACCESS_GLYPH_PATH,
3228
+ offsetX: ACCESS_GLYPH_VIEWBOX / 2,
3229
+ offsetY: ACCESS_GLYPH_VIEWBOX / 2,
3230
+ scaleX: k,
3231
+ scaleY: k,
3232
+ fill: stateAwareBookableLabelInk(seatFill, "#ffffff"),
3233
+ listening: false,
3234
+ visible: this.accessGlyphVisible,
3235
+ perfectDrawEnabled: false,
3236
+ shadowForStrokeEnabled: false
3237
+ });
3238
+ }
3239
+ /**
3240
+ * Toggle the accessibility glyphs for the current camera scale: shown once the
3241
+ * effective on-screen seat radius clears {@link SEAT_GLYPH_MIN_PX}, otherwise
3242
+ * hidden so only the ring remains. Iterates the (small) accessible-seat set,
3243
+ * never the full node graph, so it is cheap to call on every view change.
3244
+ */
3245
+ updateAccessGlyphs(scale) {
3246
+ if (!this.accessGlyphById.size) return;
3247
+ this.accessGlyphVisible = this.seatR * scale >= SEAT_GLYPH_MIN_PX;
3248
+ for (const [id, glyph] of this.accessGlyphById) {
3249
+ glyph.visible(this.accessGlyphVisible && this.accessGlyphEligible(id));
3250
+ }
3251
+ }
3252
+ /**
3253
+ * Whether an accessible seat's glyph should show for its current status. It is
3254
+ * hidden on seats a buyer cannot take (sold, or another buyer's hold) where the
3255
+ * overlay status cue (diagonal mark / lock) carries the state instead — mirrors
3256
+ * the `unavailable` test in {@link updateLabels} so the two never collide.
3257
+ */
3258
+ accessGlyphEligible(id) {
3259
+ const status = this.statusById.get(id) ?? "free";
3260
+ if (status === "booked") return false;
3261
+ if (status === "held" && !this.ownedHold.has(id) && !this.opts.manageMode) return false;
3262
+ return true;
3263
+ }
2784
3264
  /** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
2785
3265
  seatBaseColor(categoryKey) {
2786
3266
  if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
@@ -2894,6 +3374,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2894
3374
  isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
2895
3375
  );
2896
3376
  }
3377
+ const accessGlyph = this.accessGlyphById.get(id);
3378
+ if (accessGlyph) {
3379
+ const fill = c.fill();
3380
+ accessGlyph.fill(stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", "#ffffff"));
3381
+ accessGlyph.opacity(c.opacity());
3382
+ accessGlyph.visible(this.accessGlyphVisible && this.accessGlyphEligible(id));
3383
+ }
3384
+ this.accessRingById.get(id)?.opacity(c.opacity());
2897
3385
  }
2898
3386
  /** True when a seat sits in a section/zone currently marked `closed`. */
2899
3387
  seatInClosedSection(id) {
@@ -4619,6 +5107,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4619
5107
  if (this.hasSections) this.applySectionLod(scale);
4620
5108
  else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
4621
5109
  this.paintGAStateForView();
5110
+ this.updateAccessGlyphs(scale);
4622
5111
  const shouldCache = scale < CACHE_THRESHOLD;
4623
5112
  if (shouldCache && !this.cached) {
4624
5113
  this.cacheSeatLayer();
@@ -4701,6 +5190,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4701
5190
  for (const seat of this.seats) {
4702
5191
  if (seat.kind === "booth") continue;
4703
5192
  if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
5193
+ if (seat.accessible && this.accessGlyphVisible) continue;
4704
5194
  const shape = this.circleById.get(seat.id);
4705
5195
  if ((shape?.opacity() ?? 1) < 0.5) continue;
4706
5196
  const status = this.statusById.get(seat.id) ?? "free";
@@ -5050,13 +5540,17 @@ var PickerController = class {
5050
5540
  this.allIds.push(s.id);
5051
5541
  const source = chartObjects.get(s.rowId);
5052
5542
  const sourceLabel = source && "label" in source && typeof source.label === "string" ? source.label : void 0;
5053
- const rowLabel = s.kind === "booth" ? void 0 : sourceLabel;
5054
- const labelParts = s.label.split("-");
5055
- const seatNumber = rowLabel && s.label.startsWith(`${rowLabel}-`) ? s.label.slice(rowLabel.length + 1) : s.kind === "booth" ? s.label : labelParts[labelParts.length - 1] ?? s.label;
5543
+ const sourceDisplayLabel = source && "displayLabel" in source && typeof source.displayLabel === "string" && source.displayLabel ? source.displayLabel : sourceLabel;
5544
+ const rowLabel = s.kind === "booth" ? void 0 : sourceDisplayLabel;
5545
+ const rowType = source && "displayType" in source && typeof source.displayType === "string" && source.displayType.trim() ? source.displayType.trim() : void 0;
5546
+ const visibleSeatLabel = s.displayLabel ?? s.label;
5547
+ const labelParts = visibleSeatLabel.split("-");
5548
+ const seatNumber = sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) : s.kind === "booth" ? visibleSeatLabel : labelParts[labelParts.length - 1] ?? visibleSeatLabel;
5056
5549
  this.seatContext.set(s.id, {
5057
5550
  sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
5058
5551
  rowLabel,
5059
- seatNumber
5552
+ seatNumber,
5553
+ rowType
5060
5554
  });
5061
5555
  }
5062
5556
  const currency = res.event.currency ?? this.opts.currency;
@@ -5691,6 +6185,7 @@ var PickerController = class {
5691
6185
  id,
5692
6186
  label: sec.label,
5693
6187
  zoneLabel: zone?.label ?? "",
6188
+ ...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
5694
6189
  color,
5695
6190
  seatsLeft,
5696
6191
  priceMin: prices.length ? prices[0] : 0,
@@ -5722,14 +6217,16 @@ var PickerController = class {
5722
6217
  // ---- internals ------------------------------------------------------------
5723
6218
  toSeat(s) {
5724
6219
  const commercial = s.commercial ? { commercial: s.commercial } : void 0;
6220
+ const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
5725
6221
  const tiers = this.tiersFor(s.categoryKey);
5726
6222
  if (!tiers) {
5727
- return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
6223
+ return { id: s.id, label: s.label, ...display, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
5728
6224
  }
5729
6225
  const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
5730
6226
  return {
5731
6227
  id: s.id,
5732
6228
  label: s.label,
6229
+ ...display,
5733
6230
  categoryKey: s.categoryKey,
5734
6231
  price: chosen.price,
5735
6232
  tiers,
@@ -6254,6 +6751,7 @@ async function loadLocale(code) {
6254
6751
  MAX_EVENT_INVENTORY,
6255
6752
  MAX_GA_CAPACITY,
6256
6753
  PickerController,
6754
+ RENDERED_QUALITY_REPORT_VERSION,
6257
6755
  SUPPORTED_LOCALES,
6258
6756
  SeatmapRenderer,
6259
6757
  UNGROUPED_ID,
@@ -6281,6 +6779,7 @@ async function loadLocale(code) {
6281
6779
  generateSeatThumb,
6282
6780
  getLocale,
6283
6781
  hiddenObjectIds,
6782
+ inspectRenderedQualityEvidence,
6284
6783
  isGaUnitLabel,
6285
6784
  isSectionHidden,
6286
6785
  layerOf,
@@ -6290,6 +6789,8 @@ async function loadLocale(code) {
6290
6789
  pointInPolygonWithHoles,
6291
6790
  polygonLabelPoint,
6292
6791
  resolveLocale,
6792
+ rowSeatPositions,
6793
+ seatLabelPart,
6293
6794
  setLocale,
6294
6795
  setMoneyLocale,
6295
6796
  setStringOverrides,