@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/{de-DRWMZ2FV.js → de-73TIXYJH.js} +3 -34
- package/dist/de-73TIXYJH.js.map +1 -0
- package/dist/{es-N73CIVTC.js → es-SKAODLTR.js} +3 -34
- package/dist/es-SKAODLTR.js.map +1 -0
- package/dist/{fr-SOY2OB4P.js → fr-J4637T6V.js} +3 -34
- package/dist/fr-J4637T6V.js.map +1 -0
- package/dist/index.cjs +674 -173
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +232 -3
- package/dist/index.d.ts +232 -3
- package/dist/index.js +667 -77
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/de-DRWMZ2FV.js.map +0 -1
- package/dist/es-N73CIVTC.js.map +0 -1
- package/dist/fr-SOY2OB4P.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -111,6 +111,53 @@ function transformSectionOutlinePath(path, transform, radiusScale = 1, reflected
|
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// src/core/labeling.ts
|
|
115
|
+
var FULL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
116
|
+
var LOWER_ALPHABET = "abcdefghijklmnopqrstuvwxyz";
|
|
117
|
+
function toBijectiveBase(index, alphabet) {
|
|
118
|
+
const base = alphabet.length;
|
|
119
|
+
let n = index + 1;
|
|
120
|
+
let out = "";
|
|
121
|
+
while (n > 0) {
|
|
122
|
+
n -= 1;
|
|
123
|
+
const rem = n % base;
|
|
124
|
+
out = alphabet[rem] + out;
|
|
125
|
+
n = Math.floor(n / base);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
function toLetters(value, lower = false) {
|
|
130
|
+
const alphabet = lower ? LOWER_ALPHABET : FULL_ALPHABET;
|
|
131
|
+
return toBijectiveBase(Math.max(0, Math.floor(value) - 1), alphabet);
|
|
132
|
+
}
|
|
133
|
+
var ROMAN_TABLE = [
|
|
134
|
+
[1e3, "M"],
|
|
135
|
+
[900, "CM"],
|
|
136
|
+
[500, "D"],
|
|
137
|
+
[400, "CD"],
|
|
138
|
+
[100, "C"],
|
|
139
|
+
[90, "XC"],
|
|
140
|
+
[50, "L"],
|
|
141
|
+
[40, "XL"],
|
|
142
|
+
[10, "X"],
|
|
143
|
+
[9, "IX"],
|
|
144
|
+
[5, "V"],
|
|
145
|
+
[4, "IV"],
|
|
146
|
+
[1, "I"]
|
|
147
|
+
];
|
|
148
|
+
function toRoman(value) {
|
|
149
|
+
if (!Number.isFinite(value) || value <= 0) return String(value);
|
|
150
|
+
let remaining = Math.floor(value);
|
|
151
|
+
let out = "";
|
|
152
|
+
for (const [n, sym] of ROMAN_TABLE) {
|
|
153
|
+
while (remaining >= n) {
|
|
154
|
+
out += sym;
|
|
155
|
+
remaining -= n;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
114
161
|
// src/core/layout.ts
|
|
115
162
|
function overrideAccessibility(o) {
|
|
116
163
|
if (!o) return [];
|
|
@@ -156,24 +203,62 @@ function overrideMap(row) {
|
|
|
156
203
|
if (row.overrides) for (const o of row.overrides) m.set(o.index, o);
|
|
157
204
|
return m;
|
|
158
205
|
}
|
|
159
|
-
function
|
|
160
|
-
const
|
|
206
|
+
function centerRank(n) {
|
|
207
|
+
const rank = new Array(n);
|
|
208
|
+
Array.from({ length: n }, (_, i) => i).sort((a, b) => Math.abs(2 * a - (n - 1)) - Math.abs(2 * b - (n - 1)) || a - b).forEach((idx, k) => rank[idx] = k);
|
|
209
|
+
return rank;
|
|
210
|
+
}
|
|
211
|
+
function seatLabelPart(row, i) {
|
|
212
|
+
const rawStart = row.seatLabelStart ?? 1;
|
|
161
213
|
const dir = row.seatNumbering?.direction ?? "ltr";
|
|
162
214
|
const step = row.seatNumbering?.step ?? 1;
|
|
215
|
+
const scheme = row.seatNumbering?.scheme ?? "decimal";
|
|
216
|
+
const prefix = row.seatNumbering?.prefix ?? "";
|
|
217
|
+
const endAt = row.seatNumbering?.endAt;
|
|
163
218
|
const n = row.seatCount;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
219
|
+
if (scheme === "updown") {
|
|
220
|
+
const half = Math.ceil(n / 2);
|
|
221
|
+
const core2 = i < half ? rawStart + 2 * i : rawStart - 1 + 2 * (n - i);
|
|
222
|
+
return `${prefix}${core2}`;
|
|
223
|
+
}
|
|
224
|
+
const effStep = scheme === "odd" || scheme === "even" ? 2 : step;
|
|
225
|
+
const start = endAt != null && Number.isFinite(endAt) ? endAt - (n - 1) * effStep : rawStart;
|
|
226
|
+
const p = dir === "center" ? centerRank(n)[i] : dir === "rtl" ? n - 1 - i : i;
|
|
227
|
+
let core;
|
|
228
|
+
switch (scheme) {
|
|
229
|
+
case "odd": {
|
|
230
|
+
const firstOdd = start % 2 === 1 ? start : start + 1;
|
|
231
|
+
core = String(firstOdd + p * 2);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case "even": {
|
|
235
|
+
const firstEven = start % 2 === 0 ? start : start + 1;
|
|
236
|
+
core = String(firstEven + p * 2);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "roman":
|
|
240
|
+
core = toRoman(start + p * step);
|
|
241
|
+
break;
|
|
242
|
+
case "letters-upper":
|
|
243
|
+
core = toLetters(start + p * step, false);
|
|
244
|
+
break;
|
|
245
|
+
case "letters-lower":
|
|
246
|
+
core = toLetters(start + p * step, true);
|
|
247
|
+
break;
|
|
248
|
+
case "decimal":
|
|
249
|
+
default:
|
|
250
|
+
core = String(start + p * step);
|
|
251
|
+
break;
|
|
171
252
|
}
|
|
253
|
+
return `${prefix}${core}`;
|
|
254
|
+
}
|
|
255
|
+
function expandRowSlots(row) {
|
|
172
256
|
const ov = overrideMap(row);
|
|
173
257
|
return rowSeatPositions(row).map((p, i) => {
|
|
174
258
|
const o = ov.get(i);
|
|
175
259
|
const accessibility = overrideAccessibility(o);
|
|
176
|
-
const
|
|
260
|
+
const part = seatLabelPart(row, i);
|
|
261
|
+
const inventoryLabel = o?.label ?? `${row.label}-${part}`;
|
|
177
262
|
const displayPrefix = row.displayLabel ?? row.label;
|
|
178
263
|
const commercial = { ...row.commercial, ...o?.commercial };
|
|
179
264
|
return {
|
|
@@ -181,7 +266,7 @@ function expandRowSlots(row) {
|
|
|
181
266
|
x: p.x + (o?.dx ?? 0),
|
|
182
267
|
y: p.y + (o?.dy ?? 0),
|
|
183
268
|
label: inventoryLabel,
|
|
184
|
-
displayLabel: o?.displayLabel ?? `${displayPrefix}-${
|
|
269
|
+
displayLabel: o?.displayLabel ?? `${displayPrefix}-${part}`,
|
|
185
270
|
categoryKey: o?.categoryKey ?? row.categoryKey,
|
|
186
271
|
skipped: !!o?.skip,
|
|
187
272
|
accessible: accessibility.length > 0,
|
|
@@ -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
|
|
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
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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
|
|
4700
|
-
const
|
|
4701
|
-
const
|
|
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-
|
|
5872
|
-
de: () => import("./de-
|
|
5873
|
-
fr: () => import("./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,
|