@seatlayer/core 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -48,6 +48,7 @@ var init_es = __esm({
48
48
  // renderer (drawn on the Konva map — shared by the embed SDK)
49
49
  "map.aria": "Mapa de asientos. Usa las flechas para moverte entre asientos, Intro para seleccionar.",
50
50
  "map.seatsLeft": "{count} LIBRES",
51
+ "map.soldOut": "AGOTADO",
51
52
  "map.statusHeld": "En espera",
52
53
  "map.statusTaken": "Ocupado",
53
54
  "map.fromPrice": "DESDE {price}",
@@ -55,7 +56,7 @@ var init_es = __esm({
55
56
  "picker.floor": "Piso",
56
57
  "picker.zoomLevel": "Nivel de zoom",
57
58
  "picker.rungTip.zones": "Vista general \u2014 grupos de secciones como Grada norte o VIP",
58
- "picker.rungTip.sections": "Bloques de secci\xF3n \u2014 mezcla de categor\xEDas + matiz de disponibilidad",
59
+ "picker.rungTip.sections": "Bloques de secci\xF3n \u2014 las secciones agotadas y casi llenas se aten\xFAan para ver la disponibilidad de un vistazo",
59
60
  "picker.rungTip.seats": "Asientos individuales \u2014 los bloques se convierten en puntos",
60
61
  "picker.rungLabel.zones": "ZONAS",
61
62
  "picker.rungLabel.sections": "SECCIONES",
@@ -107,6 +108,7 @@ var init_de = __esm({
107
108
  // renderer (drawn on the Konva map — shared by the embed SDK)
108
109
  "map.aria": "Sitzplan. Mit Pfeiltasten zwischen Pl\xE4tzen navigieren, Eingabe zum Ausw\xE4hlen.",
109
110
  "map.seatsLeft": "{count} FREI",
111
+ "map.soldOut": "AUSVERKAUFT",
110
112
  "map.statusHeld": "Reserviert",
111
113
  "map.statusTaken": "Vergeben",
112
114
  "map.fromPrice": "AB {price}",
@@ -114,7 +116,7 @@ var init_de = __esm({
114
116
  "picker.floor": "Etage",
115
117
  "picker.zoomLevel": "Zoomstufe",
116
118
  "picker.rungTip.zones": "\xDCbersicht \u2014 Bereichsgruppen wie Nordtrib\xFCne oder VIP",
117
- "picker.rungTip.sections": "Bereichs-Bl\xF6cke \u2014 Kategoriesortierung + Verf\xFCgbarkeitst\xF6nung",
119
+ "picker.rungTip.sections": "Bereichs-Bl\xF6cke \u2014 ausverkaufte und fast volle Bereiche werden abgedunkelt, um die Verf\xFCgbarkeit auf einen Blick zu zeigen",
118
120
  "picker.rungTip.seats": "Einzelne Pl\xE4tze \u2014 Bl\xF6cke l\xF6sen sich zu Punkten auf",
119
121
  "picker.rungLabel.zones": "ZONEN",
120
122
  "picker.rungLabel.sections": "BEREICHE",
@@ -166,6 +168,7 @@ var init_fr = __esm({
166
168
  // renderer (drawn on the Konva map — shared by the embed SDK)
167
169
  "map.aria": "Plan de salle. Utilisez les fl\xE8ches pour naviguer entre les si\xE8ges, Entr\xE9e pour s\xE9lectionner.",
168
170
  "map.seatsLeft": "{count} LIBRES",
171
+ "map.soldOut": "COMPLET",
169
172
  "map.statusHeld": "En attente",
170
173
  "map.statusTaken": "Pris",
171
174
  "map.fromPrice": "D\xC8S {price}",
@@ -173,7 +176,7 @@ var init_fr = __esm({
173
176
  "picker.floor": "\xC9tage",
174
177
  "picker.zoomLevel": "Niveau de zoom",
175
178
  "picker.rungTip.zones": "Vue g\xE9n\xE9rale \u2014 groupes de sections comme Tribune nord ou VIP",
176
- "picker.rungTip.sections": "Blocs de sections \u2014 m\xE9lange de cat\xE9gories + teinte de disponibilit\xE9",
179
+ "picker.rungTip.sections": "Blocs de sections \u2014 les sections compl\xE8tes ou presque pleines sont estomp\xE9es pour voir la disponibilit\xE9 en un coup d\u2019\u0153il",
177
180
  "picker.rungTip.seats": "Si\xE8ges individuels \u2014 les blocs se transforment en points",
178
181
  "picker.rungLabel.zones": "ZONES",
179
182
  "picker.rungLabel.sections": "SECTIONS",
@@ -1823,6 +1826,169 @@ function buyerBackgroundImage(owner) {
1823
1826
  return background?.url && !background.assetId ? background : void 0;
1824
1827
  }
1825
1828
 
1829
+ // src/core/icons.ts
1830
+ var VENUE_ICON_VIEWBOX = 24;
1831
+ var VENUE_ICON_STROKE = 2;
1832
+ var WHEELCHAIR = "M13 4.6a1.6 1.6 0 1 0-3.2 0 1.6 1.6 0 0 0 3.2 0 M11 6.5V13h5l2.4 6 M15 16.4A5 5 0 1 1 9 11.3";
1833
+ var VENUE_ICONS = [
1834
+ // ---- Facilities --------------------------------------------------------
1835
+ {
1836
+ key: "restroom-men",
1837
+ label: "Men's restroom",
1838
+ group: "facilities",
1839
+ path: "M16 7a4 4 0 1 0-8 0 4 4 0 0 0 8 0 M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"
1840
+ },
1841
+ {
1842
+ key: "restroom-women",
1843
+ label: "Women's restroom",
1844
+ group: "facilities",
1845
+ path: "M15 5a3 3 0 1 0-6 0 3 3 0 0 0 6 0 M12 8l-4 10h8l-4-10 M10 18v4 M14 18v4"
1846
+ },
1847
+ {
1848
+ key: "restroom-accessible",
1849
+ label: "Accessible restroom",
1850
+ group: "facilities",
1851
+ path: WHEELCHAIR
1852
+ },
1853
+ {
1854
+ key: "first-aid",
1855
+ label: "First aid",
1856
+ group: "facilities",
1857
+ path: "M5 4h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z M12 8v8 M8 12h8"
1858
+ },
1859
+ {
1860
+ key: "coat-check",
1861
+ label: "Coat check",
1862
+ group: "facilities",
1863
+ path: "M12 6a2 2 0 0 1 0-4 2 2 0 0 1 1.6 3.2L21 13H3l8.4-7.8 M3 13h18"
1864
+ },
1865
+ {
1866
+ key: "atm",
1867
+ label: "ATM / cash",
1868
+ group: "facilities",
1869
+ path: "M2 6h20v12H2V6Z M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0 M6 9h.01 M18 15h.01"
1870
+ },
1871
+ {
1872
+ key: "info",
1873
+ label: "Info point",
1874
+ group: "facilities",
1875
+ path: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18 M12 11v5 M12 8h.01"
1876
+ },
1877
+ {
1878
+ key: "lost-found",
1879
+ label: "Lost & found",
1880
+ group: "facilities",
1881
+ path: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14 M16 16l5 5 M9 8.5a2 2 0 1 1 3 1.7c-.8.5-1 .8-1 1.8 M11 15h.01"
1882
+ },
1883
+ {
1884
+ key: "charging",
1885
+ label: "Charging point",
1886
+ group: "facilities",
1887
+ path: "M3 8h13v8H3V8Z M16 10h3v4h-3 M9.6 9l-2 3.5h2.5l-1.5 3"
1888
+ },
1889
+ {
1890
+ key: "smoking",
1891
+ label: "Smoking area",
1892
+ group: "facilities",
1893
+ path: "M2 15h13v3H2v-3Z M18 16h2 M19 8c0 1 1 1.5 1 2.5S19 12 19 13"
1894
+ },
1895
+ {
1896
+ key: "no-smoking",
1897
+ label: "No smoking",
1898
+ group: "facilities",
1899
+ path: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18 M5.6 5.6l12.8 12.8 M7 13h6v2H7v-2Z"
1900
+ },
1901
+ // ---- Food & drink ------------------------------------------------------
1902
+ {
1903
+ key: "food",
1904
+ label: "Food",
1905
+ group: "food-drink",
1906
+ path: "M7 3v6a2 2 0 0 0 4 0V3 M9 11v10 M17 3c-1.6 1-2.2 3-2.2 6 0 2 .9 3.2 2.2 3.6V21"
1907
+ },
1908
+ {
1909
+ key: "bar",
1910
+ label: "Bar",
1911
+ group: "food-drink",
1912
+ path: "M4 5h16l-8 8-8-8Z M12 13v6 M8 20h8"
1913
+ },
1914
+ {
1915
+ key: "coffee",
1916
+ label: "Caf\xE9 / coffee",
1917
+ group: "food-drink",
1918
+ path: "M4 8h13v4a5 5 0 0 1-5 5H9a5 5 0 0 1-5-5V8Z M17 9h2a2 2 0 0 1 0 4h-2 M8 2v2 M11 2v2 M4 21h14"
1919
+ },
1920
+ {
1921
+ key: "water",
1922
+ label: "Water / drinks",
1923
+ group: "food-drink",
1924
+ path: "M12 22a7 7 0 0 1-7-7c0-5 7-12 7-12s7 7 7 12a7 7 0 0 1-7 7Z"
1925
+ },
1926
+ {
1927
+ key: "merch",
1928
+ label: "Merch / shop",
1929
+ group: "food-drink",
1930
+ path: "M6 8h12l-1 12H7L6 8Z M9 8V6a3 3 0 0 1 6 0v2"
1931
+ },
1932
+ // ---- Getting around ----------------------------------------------------
1933
+ {
1934
+ key: "entrance",
1935
+ label: "Entrance",
1936
+ group: "navigation",
1937
+ path: "M14 3h5a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-5 M3 12h11 M10 8l4 4-4 4"
1938
+ },
1939
+ {
1940
+ key: "exit",
1941
+ label: "Exit",
1942
+ group: "navigation",
1943
+ path: "M10 3H5a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h5 M14 12h7 M17 8l4 4-4 4"
1944
+ },
1945
+ {
1946
+ key: "emergency-exit",
1947
+ label: "Emergency exit",
1948
+ group: "navigation",
1949
+ path: "M14 4.6a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0 M12.5 8l-3.5 2 1.6 3.6-2 4.4 M12.5 10l3.5 1.5 M9 10.5l-3.5 1 M15 12h6 M18 9l3 3-3 3"
1950
+ },
1951
+ {
1952
+ key: "stairs",
1953
+ label: "Stairs",
1954
+ group: "navigation",
1955
+ path: "M3 20v-4h4v-4h4v-4h4V4h5"
1956
+ },
1957
+ {
1958
+ key: "elevator",
1959
+ label: "Elevator",
1960
+ group: "navigation",
1961
+ path: "M5 3h14a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z M8 11l1.5-2 1.5 2 M8 14l1.5 2 1.5-2 M15 8v8"
1962
+ },
1963
+ {
1964
+ key: "parking",
1965
+ label: "Parking",
1966
+ group: "navigation",
1967
+ path: "M5 3h14a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z M9 17V7h4a3 3 0 0 1 0 6H9"
1968
+ },
1969
+ // ---- Accessibility -----------------------------------------------------
1970
+ {
1971
+ key: "wheelchair",
1972
+ label: "Wheelchair access",
1973
+ group: "accessibility",
1974
+ path: WHEELCHAIR
1975
+ },
1976
+ {
1977
+ key: "hearing",
1978
+ label: "Hearing assistance",
1979
+ group: "accessibility",
1980
+ path: "M8 20a4 4 0 0 1-2-3c0-1 .5-2 .5-3a4.5 4.5 0 1 1 9 0c0 1.4-1 2-2 2.4-1.2.5-1.5 1-1.5 2.2 M17 6a5 5 0 0 1 1 6 M19.5 4a8 8 0 0 1 1.2 9"
1981
+ }
1982
+ ];
1983
+ var BY_KEY = new Map(VENUE_ICONS.map((icon) => [icon.key, icon]));
1984
+ function venueIcon(key) {
1985
+ return key ? BY_KEY.get(key) : void 0;
1986
+ }
1987
+ function venueIconPath(key) {
1988
+ return venueIcon(key)?.path;
1989
+ }
1990
+ var VENUE_ICON_KEYS = VENUE_ICONS.map((icon) => icon.key);
1991
+
1826
1992
  // src/core/spatialIndex.ts
1827
1993
  var DEFAULT_SEAT_RADIUS = 9;
1828
1994
  function halfExtents(shape) {
@@ -2160,6 +2326,7 @@ var en = {
2160
2326
  // renderer (drawn on the Konva map — shared by the embed SDK)
2161
2327
  "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
2162
2328
  "map.seatsLeft": "{count} LEFT",
2329
+ "map.soldOut": "SOLD OUT",
2163
2330
  "map.fromPrice": "FROM {price}",
2164
2331
  "map.statusHeld": "On hold",
2165
2332
  "map.statusTaken": "Taken",
@@ -2167,7 +2334,7 @@ var en = {
2167
2334
  "picker.floor": "Floor",
2168
2335
  "picker.zoomLevel": "Zoom level",
2169
2336
  "picker.rungTip.zones": "Venue overview \u2014 groups of sections such as North Stand or VIP",
2170
- "picker.rungTip.sections": "Section blocks \u2014 category-mix fill + availability tint",
2337
+ "picker.rungTip.sections": "Section blocks \u2014 sold-out and nearly-gone sections shade to show availability at a glance",
2171
2338
  "picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
2172
2339
  "picker.rungLabel.zones": "ZONES",
2173
2340
  "picker.rungLabel.sections": "SECTIONS",
@@ -2368,6 +2535,22 @@ function overviewPalette(canvasBackground) {
2368
2535
  focalStroke: DARK_OVERVIEW_FOCAL_STROKE
2369
2536
  };
2370
2537
  }
2538
+ var SECTION_NEARLY_GONE_RATIO = 0.1;
2539
+ function overviewStateFill(palette, state) {
2540
+ const base = palette.sectionFill;
2541
+ const soldOut = desaturate(darken(base, 0.2), 0.85);
2542
+ switch (state) {
2543
+ case "closed":
2544
+ return darken(base, 0.12);
2545
+ case "sold-out":
2546
+ return soldOut;
2547
+ case "nearly-gone":
2548
+ return lerpColor(base, soldOut, 0.4);
2549
+ case "normal":
2550
+ default:
2551
+ return base;
2552
+ }
2553
+ }
2371
2554
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
2372
2555
  function seatIdOf(target) {
2373
2556
  const n = target;
@@ -2390,6 +2573,13 @@ function darken(hex, amt) {
2390
2573
  const mix = (c) => Math.round(c * (1 - amt));
2391
2574
  return `#${(1 << 24 | mix(n >> 16 & 255) << 16 | mix(n >> 8 & 255) << 8 | mix(n & 255)).toString(16).slice(1)}`;
2392
2575
  }
2576
+ function desaturate(hex, amt) {
2577
+ const rgb2 = hexToRgb(hex);
2578
+ if (!rgb2) return hex;
2579
+ const [r, g, b] = rgb2;
2580
+ const grey = 0.299 * r + 0.587 * g + 0.114 * b;
2581
+ return toHex(r + (grey - r) * amt, g + (grey - g) * amt, b + (grey - b) * amt);
2582
+ }
2393
2583
  function polyBounds(pts) {
2394
2584
  let minX = Infinity;
2395
2585
  let minY = Infinity;
@@ -2453,18 +2643,36 @@ function polygonLabelCandidates(outer, holes, preferred) {
2453
2643
  }
2454
2644
  return points.sort((left, right) => Math.hypot(left.x - centre.x, left.y - centre.y) - Math.hypot(right.x - centre.x, right.y - centre.y)).filter((point, index, all) => index === all.findIndex((other) => Math.abs(other.x - point.x) < 1e-6 && Math.abs(other.y - point.y) < 1e-6));
2455
2645
  }
2646
+ function roundedPolygonSubpath(context, points, radius) {
2647
+ const n = points.length;
2648
+ const start = { x: (points[n - 1].x + points[0].x) / 2, y: (points[n - 1].y + points[0].y) / 2 };
2649
+ context.moveTo(start.x, start.y);
2650
+ for (let i = 0; i < n; i += 1) {
2651
+ const curr = points[i];
2652
+ const next = points[(i + 1) % n];
2653
+ const prev = points[(i - 1 + n) % n];
2654
+ const rMax = Math.min(
2655
+ Math.hypot(curr.x - prev.x, curr.y - prev.y) / 2,
2656
+ Math.hypot(next.x - curr.x, next.y - curr.y) / 2
2657
+ );
2658
+ context.arcTo(curr.x, curr.y, next.x, next.y, Math.max(0, Math.min(radius, rMax)));
2659
+ }
2660
+ context.closePath();
2661
+ }
2456
2662
  function polygonWithHolesShape(outer, holes, attrs, outerPath) {
2457
2663
  const signedArea = (points) => points.reduce((sum, point, index) => {
2458
2664
  const next = points[(index + 1) % points.length];
2459
2665
  return sum + point.x * next.y - next.x * point.y;
2460
2666
  }, 0);
2461
2667
  const outerClockwise = signedArea(outer) > 0;
2668
+ const cornerRadius = attrs.cornerRadius && attrs.cornerRadius > 0 ? attrs.cornerRadius : 0;
2462
2669
  return new import_Shape.Shape({
2463
2670
  ...attrs,
2464
2671
  sceneFunc(context, shape) {
2465
2672
  context.beginPath();
2466
2673
  const polygonPath = (points) => {
2467
2674
  if (!points.length) return;
2675
+ if (cornerRadius > 0 && points.length >= 3) return roundedPolygonSubpath(context, points, cornerRadius);
2468
2676
  context.moveTo(points[0].x, points[0].y);
2469
2677
  for (let index = 1; index < points.length; index += 1) context.lineTo(points[index].x, points[index].y);
2470
2678
  context.closePath();
@@ -2599,6 +2807,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2599
2807
  this.accessGlyphById = /* @__PURE__ */ new Map();
2600
2808
  /** Authored free-text nodes obey the same rendered-size visibility floor. */
2601
2809
  this.freeTextById = /* @__PURE__ */ new Map();
2810
+ /** Vector venue-icon Path nodes + authored size; obey the free-text size floor. */
2811
+ this.iconNodeById = /* @__PURE__ */ new Map();
2602
2812
  /** Stage/rink landmarks retain a readable screen-space caption at overview. */
2603
2813
  this.primaryFocalLabels = /* @__PURE__ */ new Map();
2604
2814
  /** GA paint and text share price/highlight filter state. */
@@ -2612,6 +2822,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2612
2822
  this.effSelection = DEF_SELECTION;
2613
2823
  /** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
2614
2824
  this.colorblind = false;
2825
+ /** Stable per-category-KEY colorblind hue (OV-46) — order-independent. */
2826
+ this.cbHueByKey = /* @__PURE__ */ new Map();
2827
+ /** OV-45(a): authored row-letter labels for the buyer/preview map, resolved in
2828
+ * world space (matching seat labels) and honouring each row's labelPresentation
2829
+ * (position/rotation/visibility/style). Painted at the seats LOD rung. */
2830
+ this.rowLabelPlan = [];
2615
2831
  /** Category order from the doc — the stable index into the CB palette. */
2616
2832
  this.catOrder = [];
2617
2833
  this.selection = /* @__PURE__ */ new Set();
@@ -2981,6 +3197,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2981
3197
  this.accessRingById.clear();
2982
3198
  this.accessGlyphById.clear();
2983
3199
  this.freeTextById.clear();
3200
+ this.iconNodeById.clear();
2984
3201
  this.primaryFocalLabels.clear();
2985
3202
  this.gaById.clear();
2986
3203
  for (const marker of this.selectionMarkers.values()) marker.destroy();
@@ -3035,13 +3252,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3035
3252
  this.catColor.clear();
3036
3253
  this.catPrice.clear();
3037
3254
  this.catOrder = doc.categories.map((c) => c.key);
3255
+ this.cbHueByKey.clear();
3256
+ [...this.catOrder].sort().forEach((key, i) => {
3257
+ this.cbHueByKey.set(key, CB_PALETTE[i % CB_PALETTE.length]);
3258
+ });
3038
3259
  for (const c of doc.categories) {
3039
3260
  this.catColor.set(c.key, c.color);
3040
3261
  if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
3041
3262
  }
3042
3263
  for (const obj of view.objects) {
3043
3264
  if (obj.type === "booth") {
3044
- this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
3265
+ this.boothDims.set(obj.id, {
3266
+ width: obj.width,
3267
+ height: obj.height,
3268
+ rotation: obj.rotation,
3269
+ ...obj.points && obj.points.length >= 3 ? { points: obj.points.map((p) => ({ x: p.x, y: p.y })) } : {}
3270
+ });
3045
3271
  }
3046
3272
  }
3047
3273
  this.seats = expandChart(view, {
@@ -3061,6 +3287,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3061
3287
  this.unsectionedSeatGroup = new import_Group.Group({ listening: true });
3062
3288
  this.seatLayer.add(this.unsectionedSeatGroup);
3063
3289
  this.renderSeats();
3290
+ this.buildRowLabelPlan(view);
3064
3291
  this.overlayLayer.add(this.labelGroup);
3065
3292
  this.overlayLayer.add(this.fgDecorGroup);
3066
3293
  this.overlayLayer.add(this.hoverRing);
@@ -4372,25 +4599,42 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4372
4599
  renderBoothUnit(seat) {
4373
4600
  const target = this.seatContainer(seat.id);
4374
4601
  const dims = this.boothDims.get(seat.rowId) ?? { width: 40, height: 30, rotation: 0 };
4375
- const rect = new import_Rect.Rect({
4376
- x: seat.x,
4377
- y: seat.y,
4378
- width: dims.width,
4379
- height: dims.height,
4380
- offsetX: dims.width / 2,
4381
- offsetY: dims.height / 2,
4382
- rotation: dims.rotation,
4383
- cornerRadius: 4,
4384
- perfectDrawEnabled: false,
4385
- shadowForStrokeEnabled: false,
4386
- hitStrokeWidth: 0
4387
- });
4388
- rect.setAttr("seatId", seat.id);
4389
- this.circleById.set(seat.id, rect);
4390
- target.add(rect);
4602
+ let labelX = seat.x;
4603
+ let labelY = seat.y;
4604
+ let node;
4605
+ if (dims.points && dims.points.length >= 3) {
4606
+ node = new import_Line.Line({
4607
+ x: seat.x,
4608
+ y: seat.y,
4609
+ points: dims.points.flatMap((p) => [p.x, p.y]),
4610
+ closed: true,
4611
+ perfectDrawEnabled: false,
4612
+ shadowForStrokeEnabled: false,
4613
+ hitStrokeWidth: 0
4614
+ });
4615
+ labelX = seat.x + dims.points.reduce((a, p) => a + p.x, 0) / dims.points.length;
4616
+ labelY = seat.y + dims.points.reduce((a, p) => a + p.y, 0) / dims.points.length;
4617
+ } else {
4618
+ node = new import_Rect.Rect({
4619
+ x: seat.x,
4620
+ y: seat.y,
4621
+ width: dims.width,
4622
+ height: dims.height,
4623
+ offsetX: dims.width / 2,
4624
+ offsetY: dims.height / 2,
4625
+ rotation: dims.rotation,
4626
+ cornerRadius: 4,
4627
+ perfectDrawEnabled: false,
4628
+ shadowForStrokeEnabled: false,
4629
+ hitStrokeWidth: 0
4630
+ });
4631
+ }
4632
+ node.setAttr("seatId", seat.id);
4633
+ this.circleById.set(seat.id, node);
4634
+ target.add(node);
4391
4635
  const t2 = new import_Text.Text({
4392
- x: seat.x,
4393
- y: seat.y,
4636
+ x: labelX,
4637
+ y: labelY,
4394
4638
  text: seat.displayLabel ?? seat.label,
4395
4639
  fontSize: BOOTH_LABEL_FONT_SIZE,
4396
4640
  fontStyle: "600",
@@ -4405,7 +4649,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4405
4649
  this.hasBoothText = true;
4406
4650
  this.boothLabelById.set(seat.id, t2);
4407
4651
  target.add(t2);
4408
- this.paintSeat(rect, seat.id);
4652
+ this.paintSeat(node, seat.id);
4409
4653
  }
4410
4654
  /**
4411
4655
  * Build the centred accessibility glyph for a seat. Sized relative to the seat
@@ -4473,8 +4717,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4473
4717
  /** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
4474
4718
  seatBaseColor(categoryKey) {
4475
4719
  if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
4476
- const idx = this.catOrder.indexOf(categoryKey);
4477
- return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
4720
+ return this.cbHueByKey.get(categoryKey) ?? CB_PALETTE[0];
4478
4721
  }
4479
4722
  /** Per-seat authored label size relative to the neutral default (1 = default),
4480
4723
  * mirroring the section convention (labelStyle.size ?? 18) / 18. Only real
@@ -4945,6 +5188,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4945
5188
  renderText(obj) {
4946
5189
  const background = this.canvasBackground;
4947
5190
  const preferredInk = obj.color ?? this.theme.textColor ?? DEF_TEXT;
5191
+ const iconPath = obj.semanticKind === "icon" ? venueIconPath(obj.iconKey) : void 0;
5192
+ if (iconPath) {
5193
+ const scale = obj.fontSize / VENUE_ICON_VIEWBOX;
5194
+ const iconNode = new import_Path.Path({
5195
+ x: obj.position.x,
5196
+ y: obj.position.y,
5197
+ data: iconPath,
5198
+ rotation: obj.rotation,
5199
+ scaleX: scale,
5200
+ scaleY: scale,
5201
+ stroke: stateAwareBookableLabelInk(background, preferredInk),
5202
+ strokeWidth: VENUE_ICON_STROKE,
5203
+ lineCap: "round",
5204
+ lineJoin: "round",
5205
+ fillEnabled: false,
5206
+ listening: false,
5207
+ perfectDrawEnabled: false
5208
+ });
5209
+ this.iconNodeById.set(obj.id, { node: iconNode, fontSize: obj.fontSize });
5210
+ this.bgLayer.add(iconNode);
5211
+ return;
5212
+ }
4948
5213
  const node = new import_Text.Text({
4949
5214
  x: obj.position.x,
4950
5215
  y: obj.position.y,
@@ -5112,7 +5377,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5112
5377
  fill: color,
5113
5378
  opacity: GA_FILL_OPACITY,
5114
5379
  stroke: color,
5115
- strokeWidth: 1.5
5380
+ strokeWidth: 1.5,
5381
+ cornerRadius: obj.cornerRadius
5116
5382
  });
5117
5383
  poly.setAttr("gaId", obj.id);
5118
5384
  poly.on("click tap", (e) => {
@@ -5324,20 +5590,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5324
5590
  sec.outlinePoly.shadowBlur(4 + raw * 12);
5325
5591
  sec.outlinePoly.shadowOpacity(0.25 + raw * 0.45);
5326
5592
  }
5327
- /** Recompute a section's neutral overview state and retained detail count. */
5593
+ /** Recompute a section's availability shading + retained availability text. */
5328
5594
  refreshSectionFill(sec) {
5329
5595
  sec.blockPoly.fill(this.sectionBlockFill(sec));
5330
- sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
5596
+ sec.subLabel.text(
5597
+ this.sectionAvailabilityState(sec) === "sold-out" ? t("map.soldOut") : t("map.seatsLeft", { count: sec.free })
5598
+ );
5331
5599
  sec.subLabel.offsetX(sec.subLabel.width() / 2);
5332
5600
  }
5333
5601
  /** True when a section/zone is currently in the `closed` event-state. */
5334
5602
  isSectionClosed(sec) {
5335
5603
  return this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone);
5336
5604
  }
5337
- /** Clean overview shells never leak category, price, or live availability paint. */
5605
+ /** Resolve the four-bucket overview availability state for a section (OV-69).
5606
+ * `closed` (operator) outranks live availability; the availability buckets
5607
+ * only apply to seated sections (GA-only shells have no seat capacity here). */
5608
+ sectionAvailabilityState(sec) {
5609
+ if (this.isSectionClosed(sec)) return "closed";
5610
+ if (sec.total <= 0) return "normal";
5611
+ if (sec.free <= 0) return "sold-out";
5612
+ if (sec.free <= sec.total * SECTION_NEARLY_GONE_RATIO) return "nearly-gone";
5613
+ return "normal";
5614
+ }
5615
+ /** Overview shells stay neutral except for the bounded availability shading
5616
+ * (nearly-gone / sold-out) and the operator `closed` slab — never category
5617
+ * or price paint, which is deferred to section focus/seat detail. */
5338
5618
  sectionBlockFill(sec) {
5339
- const fill = overviewPalette(this.canvasBackground).sectionFill;
5340
- return this.isSectionClosed(sec) ? darken(fill, 0.12) : fill;
5619
+ return overviewStateFill(overviewPalette(this.canvasBackground), this.sectionAvailabilityState(sec));
5341
5620
  }
5342
5621
  /**
5343
5622
  * Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
@@ -6343,10 +6622,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6343
6622
  };
6344
6623
  });
6345
6624
  const palette = overviewPalette(this.canvasBackground);
6346
- const neutralSectionFills = /* @__PURE__ */ new Set([
6347
- palette.sectionFill.toLowerCase(),
6348
- darken(palette.sectionFill, 0.12).toLowerCase()
6349
- ]);
6625
+ const neutralSectionFills = new Set(
6626
+ ["normal", "nearly-gone", "sold-out", "closed"].map((state) => overviewStateFill(palette, state).toLowerCase())
6627
+ );
6350
6628
  const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
6351
6629
  return {
6352
6630
  viewport,
@@ -6570,6 +6848,63 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6570
6848
  const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
6571
6849
  node.visible(!gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
6572
6850
  }
6851
+ for (const { node, fontSize } of this.iconNodeById.values()) {
6852
+ node.visible(isBookableLabelLegibleAtScale(fontSize, effectiveScale));
6853
+ }
6854
+ }
6855
+ /** OV-45(a): resolve authored row-letter labels for the buyer/preview map, in
6856
+ * world space, matching the Designer's placement contract (start/end/both/none
6857
+ * preset, a free-drag `position` that wins over the preset, rotation, and
6858
+ * labelStyle colour/size fed through the same auto-contrast guard). Segmented
6859
+ * rows keep their own logical-label handling and are resolved by their owner. */
6860
+ buildRowLabelPlan(view) {
6861
+ this.rowLabelPlan = [];
6862
+ const background = this.theme.background ?? "#0e1117";
6863
+ const seatsByRow = /* @__PURE__ */ new Map();
6864
+ for (const seat of this.seats) {
6865
+ if (seat.kind === "booth") continue;
6866
+ const key = seat.logicalRowId ?? seat.rowId;
6867
+ (seatsByRow.get(key) ?? seatsByRow.set(key, []).get(key)).push(seat);
6868
+ }
6869
+ const seatOrder = (seat) => seat.logicalSeatIndex ?? Number(seat.id.split(":").pop() ?? 0);
6870
+ for (const obj of view.objects) {
6871
+ if (obj.type !== "row") continue;
6872
+ if (obj.segmentedRow && obj.segmentedRow.componentIndex !== 0) continue;
6873
+ const presentation = obj.segmentedRow?.labelPresentation ?? obj.labelPresentation;
6874
+ if (presentation?.visible === false || presentation?.positionPreset === "none") continue;
6875
+ const key = obj.segmentedRow ? obj.segmentedRow.groupId : obj.id;
6876
+ const seats = (seatsByRow.get(key) ?? []).slice().sort((a, b) => seatOrder(a) - seatOrder(b));
6877
+ if (!seats.length) continue;
6878
+ const first = seats[0];
6879
+ const last = seats[seats.length - 1];
6880
+ const labelStyle = presentation?.labelStyle;
6881
+ const ink = stateAwareBookableLabelInk(
6882
+ background,
6883
+ labelStyle?.color ?? this.theme.rowLabelColor ?? this.theme.textColor ?? "#e6e9f0"
6884
+ );
6885
+ const fontSize = labelStyle?.size ?? 12;
6886
+ const rotation = presentation?.rotation ?? 0;
6887
+ const text = obj.segmentedRow?.displayLabel ?? obj.displayLabel ?? obj.label;
6888
+ const opacity = this.objectFilteredOut(obj) ? 0.15 : 1;
6889
+ const push = (x, y) => this.rowLabelPlan.push({ text, x, y, rotation, fontSize, ink, opacity });
6890
+ if (presentation?.position) {
6891
+ push(presentation.position.x, presentation.position.y);
6892
+ continue;
6893
+ }
6894
+ const radians = obj.rotation * Math.PI / 180;
6895
+ const along = { x: Math.cos(radians), y: Math.sin(radians) };
6896
+ const gap = this.seatR + 12;
6897
+ const preset = presentation?.positionPreset ?? "start";
6898
+ if (preset === "start" || preset === "both") push(first.x - along.x * gap, first.y - along.y * gap);
6899
+ if (preset === "end" || preset === "both") push(last.x + along.x * gap, last.y + along.y * gap);
6900
+ }
6901
+ }
6902
+ /** Row labels never carry status; the buyer just dims them under the same
6903
+ * category/price filters that dim their seats. */
6904
+ objectFilteredOut(obj) {
6905
+ const key = obj.categoryKey;
6906
+ if (!key) return false;
6907
+ return Boolean(this.categoryHighlight && key !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(key));
6573
6908
  }
6574
6909
  updateLabels() {
6575
6910
  const effectiveScale = this.effScale();
@@ -6663,6 +6998,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6663
6998
  this.seatLabelById.set(seat.id, t2);
6664
6999
  if (++count >= MAX_LABELS) break;
6665
7000
  }
7001
+ for (const rl of this.rowLabelPlan) {
7002
+ const screen = this.worldToScreen({ x: rl.x, y: rl.y });
7003
+ if (screen.x < -40 || screen.x > this.stage.width() + 40 || screen.y < -40 || screen.y > this.stage.height() + 40) continue;
7004
+ const t2 = new import_Text.Text({
7005
+ x: rl.x,
7006
+ y: rl.y,
7007
+ text: rl.text,
7008
+ fontSize: rl.fontSize,
7009
+ fontStyle: "700",
7010
+ fontFamily: this.labelFont(),
7011
+ fill: rl.ink,
7012
+ rotation: rl.rotation,
7013
+ opacity: rl.opacity,
7014
+ shadowColor: "#05070c",
7015
+ shadowBlur: rl.ink.toLowerCase() === "#ffffff" ? 5 : 0,
7016
+ shadowOpacity: rl.ink.toLowerCase() === "#ffffff" ? 0.8 : 0,
7017
+ shadowForStrokeEnabled: false,
7018
+ listening: false,
7019
+ perfectDrawEnabled: false
7020
+ });
7021
+ t2.offsetX(t2.width() / 2);
7022
+ t2.offsetY(t2.height() / 2);
7023
+ this.labelGroup.add(t2);
7024
+ }
6666
7025
  if (this.isoT > 0 && this.viewMode !== "perspective") this.applyUprightLabels();
6667
7026
  this.overlayLayer.batchDraw();
6668
7027
  }
@@ -7909,7 +8268,7 @@ var PickerController = class {
7909
8268
  const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
7910
8269
  return {
7911
8270
  id,
7912
- label: sec.label,
8271
+ label: sec.displayLabel ?? sec.label,
7913
8272
  zoneLabel: zone?.label ?? "",
7914
8273
  ...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
7915
8274
  color,