@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.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,
@@ -638,19 +723,6 @@ function applyHidden(doc, hidden) {
638
723
  return { ...doc, objects };
639
724
  }
640
725
 
641
- // src/engine/SeatmapRenderer.ts
642
- import { Konva } from "konva/lib/Core";
643
- import { Stage } from "konva/lib/Stage";
644
- import { Layer } from "konva/lib/Layer";
645
- import { Group } from "konva/lib/Group";
646
- import { Circle } from "konva/lib/shapes/Circle";
647
- import { Rect } from "konva/lib/shapes/Rect";
648
- import { Ellipse } from "konva/lib/shapes/Ellipse";
649
- import { Line } from "konva/lib/shapes/Line";
650
- import { Text } from "konva/lib/shapes/Text";
651
- import { Image as KImage } from "konva/lib/shapes/Image";
652
- import { Shape } from "konva/lib/Shape";
653
-
654
726
  // src/core/chartRenderRules.ts
655
727
  var SEAT_LABEL_FONT_SIZE = 7;
656
728
  var BOOTH_LABEL_FONT_SIZE = 10;
@@ -664,6 +736,8 @@ var LIGHT_BOOKABLE_LABEL_INK = "#ffffff";
664
736
  function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
665
737
  return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
666
738
  }
739
+ var ACCESS_GLYPH_VIEWBOX = 24;
740
+ 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
741
  function bookableMarkerLabel(publicLabel) {
668
742
  return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
669
743
  }
@@ -705,6 +779,470 @@ function stateAwareBookableLabelInk(fill, preferred) {
705
779
  return darkContrast >= lightContrast ? DARK_BOOKABLE_LABEL_INK : LIGHT_BOOKABLE_LABEL_INK;
706
780
  }
707
781
 
782
+ // src/core/renderedQuality.ts
783
+ var RENDERED_QUALITY_REPORT_VERSION = 3;
784
+ var MIN_TEXT_CONTRAST = 4.5;
785
+ var MIN_GRAPHICAL_CONTRAST = 3;
786
+ var MIN_POINTER_TARGET_PX = 24;
787
+ var MAX_SAMPLES_PER_FINDING = 20;
788
+ function visibleWithBox(items) {
789
+ return items.filter((item) => item.visible && Boolean(item.screenBox));
790
+ }
791
+ function intersects(left, right) {
792
+ 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;
793
+ }
794
+ function minimum(values) {
795
+ return values.length ? Math.round(Math.min(...values) * 100) / 100 : null;
796
+ }
797
+ function finding(code, message, samples) {
798
+ if (!samples.length) return null;
799
+ return {
800
+ code,
801
+ message,
802
+ count: samples.length,
803
+ samples: samples.slice(0, MAX_SAMPLES_PER_FINDING)
804
+ };
805
+ }
806
+ function labelSample(label, measured, minimumValue) {
807
+ return {
808
+ primaryId: label.seatId,
809
+ primaryLabel: label.label,
810
+ ...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
811
+ ...minimumValue == null ? {} : { minimum: minimumValue }
812
+ };
813
+ }
814
+ function hierarchySample(label, measured, minimumValue) {
815
+ return {
816
+ primaryId: label.id,
817
+ primaryLabel: label.label,
818
+ ...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
819
+ ...minimumValue == null ? {} : { minimum: minimumValue }
820
+ };
821
+ }
822
+ function freeTextSample(label, measured, minimumValue) {
823
+ return {
824
+ primaryId: label.objectId,
825
+ primaryLabel: label.text,
826
+ ...measured == null ? {} : { measured: Math.round(measured * 100) / 100 },
827
+ ...minimumValue == null ? {} : { minimum: minimumValue }
828
+ };
829
+ }
830
+ function collisionSample(primaryId, primaryLabel, secondaryId, secondaryLabel) {
831
+ return { primaryId, primaryLabel, secondaryId, secondaryLabel };
832
+ }
833
+ function inspectRenderedQualityEvidence(evidence, state = "overview", targetCategoryKey = null) {
834
+ const bookable = visibleWithBox(evidence.labels);
835
+ const hierarchy = visibleWithBox(evidence.hierarchyLabels);
836
+ const freeText = visibleWithBox(evidence.freeTextLabels);
837
+ const visibleGA = evidence.gaAreas.filter((area) => area.visible);
838
+ const bookableContrast = bookable.map((label) => renderedTextContrast(label.ink, label.fill) ?? 0);
839
+ const hierarchyContrast = hierarchy.map((label) => renderedTextContrast(label.ink, label.fill) ?? 0);
840
+ const freeTextContrast = freeText.map((label) => renderedTextContrast(label.ink, label.background) ?? 0);
841
+ const gaContrast = visibleGA.map((area) => renderedTextContrast(area.effectiveBackground, evidence.canvasBackground) ?? 0);
842
+ const targetLabels = targetCategoryKey ? evidence.labels.filter((label) => label.categoryKey === targetCategoryKey) : evidence.labels;
843
+ const targetGAAreas = targetCategoryKey ? evidence.gaAreas.filter((area) => area.categoryKey === targetCategoryKey) : evidence.gaAreas;
844
+ const selected = targetLabels.filter((label) => label.selected);
845
+ const held = targetLabels.filter((label) => label.status === "held");
846
+ const booked = targetLabels.filter((label) => label.status === "booked");
847
+ const activePointerTargets = targetLabels.filter((label) => label.pointerTarget.active);
848
+ const activeCategoryKeys = /* @__PURE__ */ new Set([
849
+ ...evidence.labels.filter((label) => label.visible && (label.opacity > 0.25 || label.selected || label.status !== "free")).map((label) => label.categoryKey),
850
+ ...evidence.gaAreas.filter((area) => area.visible && area.opacity > 0.1).map((area) => area.categoryKey)
851
+ ]);
852
+ const selectedRingContrast = selected.length ? minimum(selected.flatMap((label) => [
853
+ renderedTextContrast(evidence.selectionRingColor, evidence.canvasBackground) ?? 0,
854
+ renderedTextContrast(evidence.selectionRingColor, label.fill) ?? 0
855
+ ])) : null;
856
+ const findings = [];
857
+ const overviewCountSamples = (count, label) => Array.from({ length: count }, (_, index) => ({
858
+ primaryId: `overview:${index + 1}`,
859
+ primaryLabel: label
860
+ }));
861
+ if (state === "overview" && evidence.rung === "sections") {
862
+ findings.push(finding(
863
+ "overview-section-category-paint",
864
+ "Section overview shells must use the neutral hierarchy palette, not category paint",
865
+ overviewCountSamples(evidence.overviewStyle.categoryPaintedSectionShells, "category-painted section shell")
866
+ ));
867
+ findings.push(finding(
868
+ "overview-category-detail-visible",
869
+ "Category-tinted section detail must wait until section focus or seat zoom",
870
+ overviewCountSamples(evidence.overviewStyle.visibleCategoryDetailOutlines, "category detail outline")
871
+ ));
872
+ findings.push(finding(
873
+ "overview-row-hints-visible",
874
+ "Row and seat patterns must not clutter the section overview",
875
+ overviewCountSamples(evidence.overviewStyle.visibleSectionRowHints, "section row hint")
876
+ ));
877
+ findings.push(finding(
878
+ "overview-availability-clutter",
879
+ "Live availability counts belong in focused detail, not on section overview shells",
880
+ overviewCountSamples(evidence.overviewStyle.visibleSectionAvailabilityLabels, "section availability label")
881
+ ));
882
+ findings.push(finding(
883
+ "overview-ga-detail-visible",
884
+ "Section-contained standing paint belongs in focused detail, not on section overview shells",
885
+ overviewCountSamples(evidence.overviewStyle.visibleSectionGADetails, "section-contained GA detail")
886
+ ));
887
+ }
888
+ findings.push(finding(
889
+ "bookable-label-undersized",
890
+ "Visible bookable labels must meet the rendered small-text size floor",
891
+ bookable.filter((label) => label.renderedFontPx < evidence.minimumVisibleLabelPx).map((label) => labelSample(label, label.renderedFontPx, evidence.minimumVisibleLabelPx))
892
+ ));
893
+ if (state === "interaction") {
894
+ const labelledInventory = targetLabels.length > 0;
895
+ const gaInventory = targetGAAreas.length > 0;
896
+ const detailInventory = labelledInventory || gaInventory;
897
+ const visibleTargetGA = targetGAAreas.filter((area) => area.visible);
898
+ const sections = /* @__PURE__ */ new Set([
899
+ ...targetLabels.flatMap((label) => label.sectionId ? [label.sectionId] : []),
900
+ ...targetGAAreas.flatMap((area) => area.sectionId ? [area.sectionId] : [])
901
+ ]);
902
+ const categories = /* @__PURE__ */ new Set([
903
+ ...evidence.labels.map((label) => label.categoryKey),
904
+ ...evidence.gaAreas.map((area) => area.categoryKey)
905
+ ]);
906
+ const inViewport = (label) => label.screenCenter.x >= 0 && label.screenCenter.x <= evidence.viewport.width && label.screenCenter.y >= 0 && label.screenCenter.y <= evidence.viewport.height;
907
+ findings.push(finding(
908
+ "detail-rung-missing",
909
+ "Interaction evidence with bookable inventory must render the seat-detail rung",
910
+ detailInventory && evidence.rung !== "seats" ? [{ primaryId: "renderer", primaryLabel: evidence.rung }] : []
911
+ ));
912
+ findings.push(finding(
913
+ "detail-inventory-not-visible",
914
+ "Interaction evidence must frame at least one real bookable unit",
915
+ detailInventory && !targetLabels.some(inViewport) && !visibleTargetGA.length ? [{ primaryId: "renderer", primaryLabel: "no target inventory in viewport" }] : []
916
+ ));
917
+ findings.push(finding(
918
+ "pointer-target-inactive",
919
+ "Interaction evidence must expose a live production pointer target",
920
+ labelledInventory && !activePointerTargets.some(inViewport) || !labelledInventory && gaInventory && !visibleTargetGA.some((area) => area.interactive) ? [{ primaryId: "renderer", primaryLabel: "no active pointer target in viewport" }] : []
921
+ ));
922
+ findings.push(finding(
923
+ "pointer-target-undersized",
924
+ "Every active production pointer target must reach at least 24 CSS pixels",
925
+ activePointerTargets.filter((label) => inViewport(label) && label.pointerTarget.effectiveMinimumPx < MIN_POINTER_TARGET_PX).map((label) => labelSample(label, label.pointerTarget.effectiveMinimumPx, MIN_POINTER_TARGET_PX))
926
+ ));
927
+ findings.push(finding(
928
+ "selected-state-missing",
929
+ "Interaction evidence must paint a selected unit and its renderer-owned ring",
930
+ labelledInventory && (!selected.length || !selected.some((label) => evidence.selectionRingSeatIds.includes(label.seatId))) ? [{ primaryId: "renderer", primaryLabel: "selected state" }] : []
931
+ ));
932
+ findings.push(finding(
933
+ "selected-state-contrast-low",
934
+ "The selected-state ring must maintain 3:1 graphical contrast with the canvas",
935
+ selected.length && (selectedRingContrast ?? 0) < MIN_GRAPHICAL_CONTRAST ? [{
936
+ primaryId: selected[0].seatId,
937
+ primaryLabel: selected[0].label,
938
+ measured: Math.round((selectedRingContrast ?? 0) * 100) / 100,
939
+ minimum: MIN_GRAPHICAL_CONTRAST
940
+ }] : []
941
+ ));
942
+ findings.push(finding(
943
+ "held-state-missing",
944
+ "Interaction evidence must paint a held state when the floor has at least two status-managed units",
945
+ targetLabels.length >= 2 && !held.length ? [{ primaryId: "renderer", primaryLabel: "held state" }] : []
946
+ ));
947
+ findings.push(finding(
948
+ "booked-state-missing",
949
+ "Interaction evidence must paint a taken state when the floor has at least three status-managed units",
950
+ targetLabels.length >= 3 && !booked.length ? [{ primaryId: "renderer", primaryLabel: "booked state" }] : []
951
+ ));
952
+ const heldSignatures = new Set(held.map((label) => `${label.fill.toLowerCase()}:${label.opacity}`));
953
+ const bookedSignatures = new Set(booked.map((label) => `${label.fill.toLowerCase()}:${label.opacity}`));
954
+ findings.push(finding(
955
+ "status-state-indistinct",
956
+ "Held and taken evidence must resolve to distinct renderer paint",
957
+ 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 }] : []
958
+ ));
959
+ findings.push(finding(
960
+ "section-focus-missing",
961
+ "Interaction evidence must exercise section focus and its backdrop when section membership exists",
962
+ sections.size && (!evidence.focusedSectionId || !evidence.focusBackdropVisible) ? [{ primaryId: "renderer", primaryLabel: "section focus" }] : []
963
+ ));
964
+ findings.push(finding(
965
+ "category-filter-missing",
966
+ "Interaction evidence must exercise a category filter when multiple categories exist",
967
+ categories.size >= 2 && (!evidence.categoryFilterKeys || !evidence.categoryFilterKeys.length) ? [{ primaryId: "renderer", primaryLabel: "category filter" }] : []
968
+ ));
969
+ const excludedFree = evidence.labels.filter((label) => label.status === "free" && !label.selected && evidence.categoryFilterKeys != null && !evidence.categoryFilterKeys.includes(label.categoryKey));
970
+ const excludedGA = evidence.gaAreas.filter((area) => evidence.categoryFilterKeys != null && !evidence.categoryFilterKeys.includes(area.categoryKey));
971
+ findings.push(finding(
972
+ "category-filter-ineffective",
973
+ "The active category filter must visibly dim excluded free inventory",
974
+ (excludedFree.length || excludedGA.length) && !excludedFree.some((label) => label.opacity <= 0.25) && !excludedGA.some((area) => area.opacity <= 0.1) ? [
975
+ ...excludedFree.map((label) => labelSample(label, label.opacity)),
976
+ ...excludedGA.map((area) => ({ primaryId: area.areaId, primaryLabel: area.label, measured: area.opacity }))
977
+ ] : []
978
+ ));
979
+ findings.push(finding(
980
+ "target-category-not-visible",
981
+ "A category-specific interaction scene must visibly paint its exact target category",
982
+ targetCategoryKey && !activeCategoryKeys.has(targetCategoryKey) ? [{ primaryId: targetCategoryKey, primaryLabel: targetCategoryKey }] : []
983
+ ));
984
+ findings.push(finding(
985
+ "target-category-filter-mismatch",
986
+ "A category-specific interaction scene must bind its filter to only the exact target category",
987
+ targetCategoryKey && (evidence.categoryFilterKeys?.length !== 1 || evidence.categoryFilterKeys[0] !== targetCategoryKey) ? [{ primaryId: targetCategoryKey, primaryLabel: targetCategoryKey }] : []
988
+ ));
989
+ }
990
+ findings.push(finding(
991
+ "bookable-label-contrast-low",
992
+ "Visible bookable labels must meet 4.5:1 contrast against their actual paint",
993
+ bookable.flatMap((label) => {
994
+ const ratio = renderedTextContrast(label.ink, label.fill) ?? 0;
995
+ return ratio < MIN_TEXT_CONTRAST ? [labelSample(label, ratio, MIN_TEXT_CONTRAST)] : [];
996
+ })
997
+ ));
998
+ const bookableCollisions = [];
999
+ for (let index = 0; index < bookable.length; index += 1) {
1000
+ for (let other = 0; other < index; other += 1) {
1001
+ if (intersects(bookable[index].screenBox, bookable[other].screenBox)) {
1002
+ bookableCollisions.push(collisionSample(
1003
+ bookable[other].seatId,
1004
+ bookable[other].label,
1005
+ bookable[index].seatId,
1006
+ bookable[index].label
1007
+ ));
1008
+ }
1009
+ }
1010
+ }
1011
+ findings.push(finding(
1012
+ "bookable-label-collision",
1013
+ "Visible bookable labels must not overlap",
1014
+ bookableCollisions
1015
+ ));
1016
+ findings.push(finding(
1017
+ "hierarchy-label-undersized",
1018
+ "Visible section and zone labels must meet the rendered small-text size floor",
1019
+ hierarchy.filter((label) => label.renderedFontPx < MIN_VISIBLE_BOOKABLE_LABEL_PX).map((label) => hierarchySample(label, label.renderedFontPx, MIN_VISIBLE_BOOKABLE_LABEL_PX))
1020
+ ));
1021
+ findings.push(finding(
1022
+ "hierarchy-label-contrast-low",
1023
+ "Visible section and zone labels must meet 4.5:1 contrast against their backing paint",
1024
+ hierarchy.flatMap((label) => {
1025
+ const ratio = renderedTextContrast(label.ink, label.fill) ?? 0;
1026
+ return ratio < MIN_TEXT_CONTRAST ? [hierarchySample(label, ratio, MIN_TEXT_CONTRAST)] : [];
1027
+ })
1028
+ ));
1029
+ findings.push(finding(
1030
+ "hierarchy-label-outside-section",
1031
+ "Visible section labels must remain inside the filled section surface and outside holes",
1032
+ hierarchy.filter((label) => label.kind === "section" && label.fitsContainer === false).map((label) => hierarchySample(label))
1033
+ ));
1034
+ const hierarchyCollisions = [];
1035
+ for (let index = 0; index < hierarchy.length; index += 1) {
1036
+ for (let other = 0; other < index; other += 1) {
1037
+ if (intersects(hierarchy[index].screenBox, hierarchy[other].screenBox)) {
1038
+ hierarchyCollisions.push(collisionSample(
1039
+ hierarchy[other].id,
1040
+ hierarchy[other].label,
1041
+ hierarchy[index].id,
1042
+ hierarchy[index].label
1043
+ ));
1044
+ }
1045
+ }
1046
+ }
1047
+ findings.push(finding(
1048
+ "hierarchy-label-collision",
1049
+ "Visible hierarchy labels must not overlap each other",
1050
+ hierarchyCollisions
1051
+ ));
1052
+ const hierarchyBookableCollisions = [];
1053
+ for (const upper of hierarchy) {
1054
+ for (const unit of bookable) {
1055
+ if (intersects(upper.screenBox, unit.screenBox)) {
1056
+ hierarchyBookableCollisions.push(collisionSample(upper.id, upper.label, unit.seatId, unit.label));
1057
+ }
1058
+ }
1059
+ }
1060
+ findings.push(finding(
1061
+ "hierarchy-bookable-collision",
1062
+ "Visible hierarchy labels must not overlap bookable labels",
1063
+ hierarchyBookableCollisions
1064
+ ));
1065
+ findings.push(finding(
1066
+ "free-text-undersized",
1067
+ "Visible chart text must meet the rendered small-text size floor",
1068
+ freeText.filter((label) => label.renderedFontPx < evidence.minimumVisibleLabelPx).map((label) => freeTextSample(label, label.renderedFontPx, evidence.minimumVisibleLabelPx))
1069
+ ));
1070
+ findings.push(finding(
1071
+ "free-text-contrast-low",
1072
+ "Visible chart text must meet 4.5:1 contrast against its measured background",
1073
+ freeText.flatMap((label) => {
1074
+ const ratio = renderedTextContrast(label.ink, label.background) ?? 0;
1075
+ return ratio < MIN_TEXT_CONTRAST ? [freeTextSample(label, ratio, MIN_TEXT_CONTRAST)] : [];
1076
+ })
1077
+ ));
1078
+ const freeTextCollisions = [];
1079
+ for (let index = 0; index < freeText.length; index += 1) {
1080
+ for (let other = 0; other < index; other += 1) {
1081
+ if (intersects(freeText[index].screenBox, freeText[other].screenBox)) {
1082
+ freeTextCollisions.push(collisionSample(
1083
+ freeText[other].objectId,
1084
+ freeText[other].text,
1085
+ freeText[index].objectId,
1086
+ freeText[index].text
1087
+ ));
1088
+ }
1089
+ }
1090
+ }
1091
+ findings.push(finding(
1092
+ "free-text-collision",
1093
+ "Visible chart text labels must not overlap each other",
1094
+ freeTextCollisions
1095
+ ));
1096
+ const freeTextBookableCollisions = [];
1097
+ for (const text of freeText) {
1098
+ for (const unit of bookable) {
1099
+ if (intersects(text.screenBox, unit.screenBox)) {
1100
+ freeTextBookableCollisions.push(collisionSample(text.objectId, text.text, unit.seatId, unit.label));
1101
+ }
1102
+ }
1103
+ }
1104
+ findings.push(finding(
1105
+ "free-text-bookable-collision",
1106
+ "Visible chart text must not overlap bookable labels",
1107
+ freeTextBookableCollisions
1108
+ ));
1109
+ const freeTextHierarchyCollisions = [];
1110
+ for (const text of freeText) {
1111
+ for (const upper of hierarchy) {
1112
+ if (intersects(text.screenBox, upper.screenBox)) {
1113
+ freeTextHierarchyCollisions.push(collisionSample(text.objectId, text.text, upper.id, upper.label));
1114
+ }
1115
+ }
1116
+ }
1117
+ findings.push(finding(
1118
+ "free-text-hierarchy-collision",
1119
+ "Visible chart text must not overlap hierarchy labels",
1120
+ freeTextHierarchyCollisions
1121
+ ));
1122
+ findings.push(finding(
1123
+ "ga-contrast-low",
1124
+ "Visible GA surfaces must maintain 3:1 graphical contrast with the canvas",
1125
+ visibleGA.flatMap((area) => {
1126
+ const ratio = renderedTextContrast(area.effectiveBackground, evidence.canvasBackground) ?? 0;
1127
+ return ratio < MIN_GRAPHICAL_CONTRAST ? [{
1128
+ primaryId: area.areaId,
1129
+ primaryLabel: area.label,
1130
+ measured: Math.round(ratio * 100) / 100,
1131
+ minimum: MIN_GRAPHICAL_CONTRAST
1132
+ }] : [];
1133
+ })
1134
+ ));
1135
+ const materialFindings = findings.filter((item) => Boolean(item));
1136
+ return {
1137
+ version: RENDERED_QUALITY_REPORT_VERSION,
1138
+ passed: materialFindings.length === 0,
1139
+ state,
1140
+ targetCategoryKey,
1141
+ resolvedRules: [
1142
+ "rendered-overview-label-size",
1143
+ "rendered-overview-label-contrast",
1144
+ "rendered-overview-label-collision",
1145
+ "rendered-overview-hierarchy-containment",
1146
+ "rendered-overview-section-first-style",
1147
+ "rendered-overview-ga-contrast",
1148
+ ...state === "interaction" ? [
1149
+ "rendered-detail-inventory",
1150
+ "rendered-pointer-target",
1151
+ "rendered-selected-held-taken-states",
1152
+ "rendered-section-focus",
1153
+ "rendered-category-filter"
1154
+ ] : []
1155
+ ],
1156
+ viewport: evidence.viewport,
1157
+ canvasBackground: evidence.canvasBackground,
1158
+ effectiveScale: evidence.effectiveScale,
1159
+ rung: evidence.rung,
1160
+ inventory: {
1161
+ totalBookableUnits: evidence.totalBookableUnits,
1162
+ totalLabelledBookableUnits: evidence.totalLabelledBookableUnits,
1163
+ visibleBookableLabels: bookable.length,
1164
+ hiddenBookableLabels: evidence.hiddenLabels,
1165
+ visibleHierarchyLabels: hierarchy.length,
1166
+ visibleFreeTextLabels: freeText.length,
1167
+ visibleGAAreas: visibleGA.length
1168
+ },
1169
+ overviewStyle: evidence.overviewStyle,
1170
+ composition: {
1171
+ hierarchy: evidence.hierarchyLabels.filter((label) => label.role === "name").map((label) => ({
1172
+ id: label.id,
1173
+ kind: label.kind,
1174
+ label: label.label,
1175
+ visible: label.visible
1176
+ })).sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id)),
1177
+ labelledObjects: evidence.freeTextLabels.map((label) => ({
1178
+ objectId: label.objectId,
1179
+ kind: label.kind,
1180
+ text: label.text,
1181
+ visible: label.visible
1182
+ })).sort((left, right) => left.objectId.localeCompare(right.objectId) || left.kind.localeCompare(right.kind)),
1183
+ gaAreas: evidence.gaAreas.map((area) => ({
1184
+ areaId: area.areaId,
1185
+ label: area.label,
1186
+ categoryKey: area.categoryKey,
1187
+ ...area.sectionId ? { sectionId: area.sectionId } : {},
1188
+ visible: area.visible
1189
+ })).sort((left, right) => left.areaId.localeCompare(right.areaId)),
1190
+ bookableSectionIds: [...new Set(evidence.labels.flatMap((label) => label.sectionId ? [label.sectionId] : []))].sort(),
1191
+ categoryKeys: [.../* @__PURE__ */ new Set([
1192
+ ...evidence.labels.map((label) => label.categoryKey),
1193
+ ...evidence.gaAreas.map((area) => area.categoryKey)
1194
+ ])].sort(),
1195
+ activeCategoryKeys: [...activeCategoryKeys].sort()
1196
+ },
1197
+ metrics: {
1198
+ minimumRenderedBookableLabelPx: minimum(bookable.map((label) => label.renderedFontPx)),
1199
+ minimumBookableLabelContrast: minimum(bookableContrast),
1200
+ minimumRenderedHierarchyLabelPx: minimum(hierarchy.map((label) => label.renderedFontPx)),
1201
+ minimumHierarchyLabelContrast: minimum(hierarchyContrast),
1202
+ minimumRenderedFreeTextPx: minimum(freeText.map((label) => label.renderedFontPx)),
1203
+ minimumFreeTextContrast: minimum(freeTextContrast),
1204
+ minimumGAContrast: minimum(gaContrast),
1205
+ minimumEffectivePointerTargetPx: minimum(activePointerTargets.map((label) => label.pointerTarget.effectiveMinimumPx)),
1206
+ selectedRingContrast: selectedRingContrast == null ? null : Math.round(selectedRingContrast * 100) / 100
1207
+ },
1208
+ interaction: {
1209
+ applicable: {
1210
+ detail: targetLabels.length > 0 || targetGAAreas.length > 0,
1211
+ pointer: targetLabels.length > 0 || targetGAAreas.length > 0,
1212
+ held: targetLabels.length >= 2,
1213
+ booked: targetLabels.length >= 3,
1214
+ sectionFocus: targetLabels.some((label) => Boolean(label.sectionId)) || targetGAAreas.some((area) => Boolean(area.sectionId)),
1215
+ categoryFilter: (/* @__PURE__ */ new Set([
1216
+ ...evidence.labels.map((label) => label.categoryKey),
1217
+ ...evidence.gaAreas.map((area) => area.categoryKey)
1218
+ ])).size >= 2
1219
+ },
1220
+ selectedUnits: selected.length,
1221
+ heldUnits: held.length,
1222
+ bookedUnits: booked.length,
1223
+ activePointerTargets: activePointerTargets.length,
1224
+ focusedSectionId: evidence.focusedSectionId,
1225
+ focusBackdropVisible: evidence.focusBackdropVisible,
1226
+ categoryFilterKeys: evidence.categoryFilterKeys
1227
+ },
1228
+ findings: materialFindings
1229
+ };
1230
+ }
1231
+
1232
+ // src/engine/SeatmapRenderer.ts
1233
+ import { Konva } from "konva/lib/Core";
1234
+ import { Stage } from "konva/lib/Stage";
1235
+ import { Layer } from "konva/lib/Layer";
1236
+ import { Group } from "konva/lib/Group";
1237
+ import { Circle } from "konva/lib/shapes/Circle";
1238
+ import { Rect } from "konva/lib/shapes/Rect";
1239
+ import { Ellipse } from "konva/lib/shapes/Ellipse";
1240
+ import { Line } from "konva/lib/shapes/Line";
1241
+ import { Text } from "konva/lib/shapes/Text";
1242
+ import { Path } from "konva/lib/shapes/Path";
1243
+ import { Image as KImage } from "konva/lib/shapes/Image";
1244
+ import { Shape } from "konva/lib/Shape";
1245
+
708
1246
  // src/lib/money.ts
709
1247
  var DEFAULT_CURRENCY = "USD";
710
1248
  var displayLocale;
@@ -743,29 +1281,17 @@ var en = {
743
1281
  "common.done": "Done",
744
1282
  "common.copied": "\u2713 Copied",
745
1283
  // buyer picker
746
- "picker.holdSeats": "Hold seats & checkout",
747
- "picker.completeBooking": "Complete booking",
748
- "picker.seatsHeld": "Seats held \u2014 {time}",
749
1284
  "picker.holdExpired": "Your hold expired \u2014 the seats were released. Pick again.",
750
- "picker.seatTaken": "Seat {label} was just taken by another buyer.",
751
1285
  "picker.poweredBy": "Powered by SeatLayer",
752
1286
  "picker.testMode": "TEST MODE",
753
- "picker.colorblind": "Colorblind-friendly colors",
754
1287
  "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
1288
  // renderer (drawn on the Konva map — shared by the embed SDK)
758
1289
  "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
759
1290
  "map.seatsLeft": "{count} LEFT",
760
1291
  "map.fromPrice": "FROM {price}",
761
1292
  "map.statusHeld": "On hold",
762
1293
  "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",
1294
+ // buyer picker widget (src/picker/widget/SeatPicker.ts)
769
1295
  "picker.floor": "Floor",
770
1296
  "picker.zoomLevel": "Zoom level",
771
1297
  "picker.rungTip.zones": "Venue overview \u2014 groups of sections such as North Stand or VIP",
@@ -779,32 +1305,13 @@ var en = {
779
1305
  "picker.seatsLeftInSection.one": "{count} seat left",
780
1306
  "picker.seatsLeftInSection.other": "{count} seats left",
781
1307
  "picker.overview": "Overview",
1308
+ "picker.entrance": "Entrance",
782
1309
  "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
1310
  "picker.ticketTierFor": "Ticket tier for {label}",
796
- "picker.total": "Total: {amount}",
797
1311
  "picker.viewFromSeat": "View from seat {label}",
798
1312
  "picker.real360": "REAL 360\xB0",
799
1313
  "picker.preview": "PREVIEW",
800
- "picker.open360": "Open 360\xB0 view",
801
1314
  "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
1315
  "picker.panorama360": "360\xB0 venue photo",
809
1316
  "picker.illustrationCaption": "illustration \xB7 \u2248 {m} m from stage",
810
1317
  "picker.restrictedView": "Restricted view",
@@ -863,6 +1370,7 @@ var CACHE_THRESHOLD = 0.55 * SEAT_LEGIBLE_SCALE;
863
1370
  var LABEL_SCALE = MIN_VISIBLE_BOOKABLE_LABEL_PX / SEAT_LABEL_FONT_SIZE;
864
1371
  var MIN_FITTED_SEAT_LABEL_FONT_SIZE = 4;
865
1372
  var SEAT_TAP_SLOP_PX = 14;
1373
+ var SEAT_GLYPH_MIN_PX = 6.5;
866
1374
  var SECTION_PROMINENT_SCALE = 0.45 * SEAT_LEGIBLE_SCALE;
867
1375
  var BLOCK_MELT_TOP = 0.9 * SEAT_LEGIBLE_SCALE;
868
1376
  var SEAT_FOCUS_SCALE = Math.max(SEAT_LEGIBLE_SCALE * 1.1, BLOCK_MELT_TOP);
@@ -1152,6 +1660,15 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1152
1660
  this.boothLabelById = /* @__PURE__ */ new Map();
1153
1661
  /** Viewport seat labels are rebuilt after each settled camera change. */
1154
1662
  this.seatLabelById = /* @__PURE__ */ new Map();
1663
+ /** Coloured accommodation ring per accessible seat (few per chart). */
1664
+ this.accessRingById = /* @__PURE__ */ new Map();
1665
+ /** Centred accessibility glyph per accessible seat — shown once the seat is
1666
+ * big enough on-screen (see {@link SEAT_GLYPH_MIN_PX}); the ring is the
1667
+ * smaller-zoom fallback. Kept in a map so zoom toggles touch only the handful
1668
+ * of accessible seats, never all 13k nodes. */
1669
+ this.accessGlyphById = /* @__PURE__ */ new Map();
1670
+ /** Whether the accessibility glyph is legible at the current camera scale. */
1671
+ this.accessGlyphVisible = false;
1155
1672
  /** Authored free-text nodes obey the same rendered-size visibility floor. */
1156
1673
  this.freeTextById = /* @__PURE__ */ new Map();
1157
1674
  /** Stage/rink landmarks retain a readable screen-space caption at overview. */
@@ -1464,6 +1981,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1464
1981
  this.boothDims.clear();
1465
1982
  this.boothLabelById.clear();
1466
1983
  this.seatLabelById.clear();
1984
+ this.accessRingById.clear();
1985
+ this.accessGlyphById.clear();
1467
1986
  this.freeTextById.clear();
1468
1987
  this.primaryFocalLabels.clear();
1469
1988
  this.gaById.clear();
@@ -2372,18 +2891,21 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2372
2891
  this.paintSeat(c, seat.id);
2373
2892
  target.add(c);
2374
2893
  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
- );
2894
+ const ring = new Circle({
2895
+ x: seat.x,
2896
+ y: seat.y,
2897
+ radius: this.seatR + 1.5,
2898
+ stroke: accessibilityRingColor(seat.accessibility),
2899
+ strokeWidth: 2.5,
2900
+ listening: false,
2901
+ perfectDrawEnabled: false,
2902
+ shadowForStrokeEnabled: false
2903
+ });
2904
+ this.accessRingById.set(seat.id, ring);
2905
+ target.add(ring);
2906
+ const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
2907
+ this.accessGlyphById.set(seat.id, glyph);
2908
+ target.add(glyph);
2387
2909
  }
2388
2910
  }
2389
2911
  }
@@ -2427,6 +2949,53 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2427
2949
  target.add(t2);
2428
2950
  this.paintSeat(rect, seat.id);
2429
2951
  }
2952
+ /**
2953
+ * Build the centred accessibility glyph for a seat. Sized relative to the seat
2954
+ * radius (so it always fills the marker at any zoom) and given a contrast-aware
2955
+ * fill against the seat's paint — recomputed per state in {@link paintSeat}.
2956
+ */
2957
+ buildAccessGlyph(seat, seatFill) {
2958
+ const k = this.seatR * 1.5 / ACCESS_GLYPH_VIEWBOX;
2959
+ return new Path({
2960
+ x: seat.x,
2961
+ y: seat.y,
2962
+ data: ACCESS_GLYPH_PATH,
2963
+ offsetX: ACCESS_GLYPH_VIEWBOX / 2,
2964
+ offsetY: ACCESS_GLYPH_VIEWBOX / 2,
2965
+ scaleX: k,
2966
+ scaleY: k,
2967
+ fill: stateAwareBookableLabelInk(seatFill, "#ffffff"),
2968
+ listening: false,
2969
+ visible: this.accessGlyphVisible,
2970
+ perfectDrawEnabled: false,
2971
+ shadowForStrokeEnabled: false
2972
+ });
2973
+ }
2974
+ /**
2975
+ * Toggle the accessibility glyphs for the current camera scale: shown once the
2976
+ * effective on-screen seat radius clears {@link SEAT_GLYPH_MIN_PX}, otherwise
2977
+ * hidden so only the ring remains. Iterates the (small) accessible-seat set,
2978
+ * never the full node graph, so it is cheap to call on every view change.
2979
+ */
2980
+ updateAccessGlyphs(scale) {
2981
+ if (!this.accessGlyphById.size) return;
2982
+ this.accessGlyphVisible = this.seatR * scale >= SEAT_GLYPH_MIN_PX;
2983
+ for (const [id, glyph] of this.accessGlyphById) {
2984
+ glyph.visible(this.accessGlyphVisible && this.accessGlyphEligible(id));
2985
+ }
2986
+ }
2987
+ /**
2988
+ * Whether an accessible seat's glyph should show for its current status. It is
2989
+ * hidden on seats a buyer cannot take (sold, or another buyer's hold) where the
2990
+ * overlay status cue (diagonal mark / lock) carries the state instead — mirrors
2991
+ * the `unavailable` test in {@link updateLabels} so the two never collide.
2992
+ */
2993
+ accessGlyphEligible(id) {
2994
+ const status = this.statusById.get(id) ?? "free";
2995
+ if (status === "booked") return false;
2996
+ if (status === "held" && !this.ownedHold.has(id) && !this.opts.manageMode) return false;
2997
+ return true;
2998
+ }
2430
2999
  /** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
2431
3000
  seatBaseColor(categoryKey) {
2432
3001
  if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
@@ -2540,6 +3109,14 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2540
3109
  isBookableLabelLegibleAtScale(bookableLabel.fontSize(), this.effScale()) && c.opacity() >= 0.5
2541
3110
  );
2542
3111
  }
3112
+ const accessGlyph = this.accessGlyphById.get(id);
3113
+ if (accessGlyph) {
3114
+ const fill = c.fill();
3115
+ accessGlyph.fill(stateAwareBookableLabelInk(typeof fill === "string" ? fill : "", "#ffffff"));
3116
+ accessGlyph.opacity(c.opacity());
3117
+ accessGlyph.visible(this.accessGlyphVisible && this.accessGlyphEligible(id));
3118
+ }
3119
+ this.accessRingById.get(id)?.opacity(c.opacity());
2543
3120
  }
2544
3121
  /** True when a seat sits in a section/zone currently marked `closed`. */
2545
3122
  seatInClosedSection(id) {
@@ -4265,6 +4842,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4265
4842
  if (this.hasSections) this.applySectionLod(scale);
4266
4843
  else if (this.primaryFocalLabels.size) this.bgLayer.batchDraw();
4267
4844
  this.paintGAStateForView();
4845
+ this.updateAccessGlyphs(scale);
4268
4846
  const shouldCache = scale < CACHE_THRESHOLD;
4269
4847
  if (shouldCache && !this.cached) {
4270
4848
  this.cacheSeatLayer();
@@ -4347,6 +4925,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4347
4925
  for (const seat of this.seats) {
4348
4926
  if (seat.kind === "booth") continue;
4349
4927
  if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
4928
+ if (seat.accessible && this.accessGlyphVisible) continue;
4350
4929
  const shape = this.circleById.get(seat.id);
4351
4930
  if ((shape?.opacity() ?? 1) < 0.5) continue;
4352
4931
  const status = this.statusById.get(seat.id) ?? "free";
@@ -4696,13 +5275,17 @@ var PickerController = class {
4696
5275
  this.allIds.push(s.id);
4697
5276
  const source = chartObjects.get(s.rowId);
4698
5277
  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;
5278
+ const sourceDisplayLabel = source && "displayLabel" in source && typeof source.displayLabel === "string" && source.displayLabel ? source.displayLabel : sourceLabel;
5279
+ const rowLabel = s.kind === "booth" ? void 0 : sourceDisplayLabel;
5280
+ const rowType = source && "displayType" in source && typeof source.displayType === "string" && source.displayType.trim() ? source.displayType.trim() : void 0;
5281
+ const visibleSeatLabel = s.displayLabel ?? s.label;
5282
+ const labelParts = visibleSeatLabel.split("-");
5283
+ const seatNumber = sourceDisplayLabel && visibleSeatLabel.startsWith(`${sourceDisplayLabel}-`) ? visibleSeatLabel.slice(sourceDisplayLabel.length + 1) : s.kind === "booth" ? visibleSeatLabel : labelParts[labelParts.length - 1] ?? visibleSeatLabel;
4702
5284
  this.seatContext.set(s.id, {
4703
5285
  sectionLabel: sectionLabels.get(membership.objectToSection.get(s.rowId) ?? ""),
4704
5286
  rowLabel,
4705
- seatNumber
5287
+ seatNumber,
5288
+ rowType
4706
5289
  });
4707
5290
  }
4708
5291
  const currency = res.event.currency ?? this.opts.currency;
@@ -5337,6 +5920,7 @@ var PickerController = class {
5337
5920
  id,
5338
5921
  label: sec.label,
5339
5922
  zoneLabel: zone?.label ?? "",
5923
+ ...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
5340
5924
  color,
5341
5925
  seatsLeft,
5342
5926
  priceMin: prices.length ? prices[0] : 0,
@@ -5368,14 +5952,16 @@ var PickerController = class {
5368
5952
  // ---- internals ------------------------------------------------------------
5369
5953
  toSeat(s) {
5370
5954
  const commercial = s.commercial ? { commercial: s.commercial } : void 0;
5955
+ const display = s.displayLabel ? { displayLabel: s.displayLabel } : void 0;
5371
5956
  const tiers = this.tiersFor(s.categoryKey);
5372
5957
  if (!tiers) {
5373
- return { id: s.id, label: s.label, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
5958
+ return { id: s.id, label: s.label, ...display, categoryKey: s.categoryKey, price: this.priceFor(s.categoryKey), ...commercial };
5374
5959
  }
5375
5960
  const chosen = tiers.find((t2) => t2.id === this.seatTiers.get(s.id)) ?? tiers[0];
5376
5961
  return {
5377
5962
  id: s.id,
5378
5963
  label: s.label,
5964
+ ...display,
5379
5965
  categoryKey: s.categoryKey,
5380
5966
  price: chosen.price,
5381
5967
  tiers,
@@ -5868,9 +6454,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
5868
6454
 
5869
6455
  // src/i18n/bundles.ts
5870
6456
  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 }))
6457
+ es: () => import("./es-SKAODLTR.js").then((m) => ({ default: m.es })),
6458
+ de: () => import("./de-73TIXYJH.js").then((m) => ({ default: m.de })),
6459
+ fr: () => import("./fr-J4637T6V.js").then((m) => ({ default: m.fr }))
5874
6460
  };
5875
6461
  var loaded = /* @__PURE__ */ new Set(["en"]);
5876
6462
  async function loadLocale(code) {
@@ -5899,6 +6485,7 @@ export {
5899
6485
  MAX_EVENT_INVENTORY,
5900
6486
  MAX_GA_CAPACITY,
5901
6487
  PickerController,
6488
+ RENDERED_QUALITY_REPORT_VERSION,
5902
6489
  SUPPORTED_LOCALES,
5903
6490
  SeatmapRenderer,
5904
6491
  UNGROUPED_ID,
@@ -5926,6 +6513,7 @@ export {
5926
6513
  generateSeatThumb,
5927
6514
  getLocale,
5928
6515
  hiddenObjectIds,
6516
+ inspectRenderedQualityEvidence,
5929
6517
  isGaUnitLabel,
5930
6518
  isSectionHidden,
5931
6519
  layerOf,
@@ -5935,6 +6523,8 @@ export {
5935
6523
  pointInPolygonWithHoles,
5936
6524
  polygonLabelPoint,
5937
6525
  resolveLocale,
6526
+ rowSeatPositions,
6527
+ seatLabelPart,
5938
6528
  setLocale,
5939
6529
  setMoneyLocale,
5940
6530
  setStringOverrides,