@seatlayer/core 0.26.0 → 0.28.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",
@@ -1310,7 +1313,30 @@ function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
1310
1313
  return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
1311
1314
  }
1312
1315
  var ACCESS_GLYPH_VIEWBOX = 24;
1313
- 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";
1316
+ var ACCESS_TYPE_GLYPH_PATHS = {
1317
+ // ISO-style wheelchair: seated figure (head + L-shaped body) over a ring wheel.
1318
+ wheelchair: "M7.8 4.5a2.2 2.2 0 1 0 4.4 0a2.2 2.2 0 1 0 -4.4 0zM8 7L10.5 7L10.5 15L8 15ZM8 12.5L18 12.5L18 15L8 15ZM16 15L18 15L18 18L16 18ZM5.5 16a5.5 5.5 0 1 0 11 0a5.5 5.5 0 1 0 -11 0zM8 16a3 3 0 1 1 6 0a3 3 0 1 1 -6 0z",
1319
+ // Companion: two overlapping head-and-shoulders figures (seat + a guest).
1320
+ companion: "M5.5 6.5a3 3 0 1 0 6 0a3 3 0 1 0 -6 0zM4.5 18.5L6 11.5L11 11.5L12.5 18.5ZM12.5 7.5a3 3 0 1 0 6 0a3 3 0 1 0 -6 0zM11.5 19L13 12.5L18 12.5L19.5 19Z",
1321
+ // Semi-ambulatory: walking figure leaning on a cane at its right side.
1322
+ "semi-ambulatory": "M8.3 3.8a2.2 2.2 0 1 0 4.4 0a2.2 2.2 0 1 0 -4.4 0zM9 6.2L12 6.6L11 13.5L8.6 13L9 6.2ZM8.6 13L11 13L9.5 21L7 21ZM10.4 13L12.6 13L13.6 21L11.2 21ZM11.2 7.2L14 8L14.6 9.6L11.9 8.9ZM14.4 6.8L15.9 6.8L16.4 21L14.8 21Z",
1323
+ // Hearing: a bold ear silhouette (assistive listening).
1324
+ hearing: "M15.5 3.5C9 2.2 5.5 6 5.5 11.5C5.5 16 8 19.5 10.5 21C13.2 22.6 16.4 20.8 15.4 18C14.8 16.4 12.9 16.1 13 14C13.1 11.9 15.8 11.4 15.8 8.5C15.8 5.4 13 4 15.5 3.5Z",
1325
+ // CART captions: a rounded caption bubble with two bold caption bars cut out.
1326
+ cart: "M5.5 6L18.5 6a2.5 2.5 0 0 1 2.5 2.5L21 15.5a2.5 2.5 0 0 1 -2.5 2.5L11 18L7 20.5L7.5 18L5.5 18a2.5 2.5 0 0 1 -2.5 -2.5L3 8.5a2.5 2.5 0 0 1 2.5 -2.5zM6.5 11L17.5 11L17.5 12.6L6.5 12.6zM6.5 14L17.5 14L17.5 15.6L6.5 15.6z",
1327
+ // Sign language: a raised open hand (palm with four fingers and a thumb).
1328
+ "sign-language": "M7 11L17 11L17 18a2 2 0 0 1 -2 2L9 20a2 2 0 0 1 -2 -2zM7.6 5L9.4 5L9.4 12L7.6 12zM9.9 3.5L11.7 3.5L11.7 12L9.9 12zM12.3 3.5L14.1 3.5L14.1 12L12.3 12zM14.6 5L16.4 5L16.4 12L14.6 12zM7.2 12.5L5 10.2L6.4 8.9L8.6 11.2Z",
1329
+ // Plus-size: a broad-shouldered wide figure.
1330
+ "plus-size": "M9.4 4.3a2.6 2.6 0 1 0 5.2 0a2.6 2.6 0 1 0 -5.2 0zM5 20L4 13C4 10.5 7 9.5 12 9.5C17 9.5 20 10.5 20 13L19 20Z",
1331
+ // Lift-up armrest: a chair side-profile with an up arrow over the armrest.
1332
+ "lift-armrest": "M5 6L8 6L8 19L5 19zM5 16L18 16L18 19L5 19zM8 12L17 12L17 14.5L8 14.5zM14 7L16 7L16 12L14 12zM12 7L18 7L15 3Z"
1333
+ };
1334
+ var ACCESS_GLYPH_PATH = ACCESS_TYPE_GLYPH_PATHS.wheelchair;
1335
+ function accessGlyphPath(accessibility) {
1336
+ if (!accessibility?.length) return null;
1337
+ const primary = accessibility.includes("wheelchair") ? "wheelchair" : accessibility[0];
1338
+ return ACCESS_TYPE_GLYPH_PATHS[primary] ?? null;
1339
+ }
1314
1340
  function bookableMarkerLabel(publicLabel) {
1315
1341
  return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
1316
1342
  }
@@ -1823,6 +1849,187 @@ function buyerBackgroundImage(owner) {
1823
1849
  return background?.url && !background.assetId ? background : void 0;
1824
1850
  }
1825
1851
 
1852
+ // src/core/icons.ts
1853
+ var VENUE_ICON_VIEWBOX = 24;
1854
+ var VENUE_ICON_STROKE = 2;
1855
+ 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";
1856
+ var VENUE_ICONS = [
1857
+ // ---- Facilities --------------------------------------------------------
1858
+ {
1859
+ key: "restroom-men",
1860
+ label: "Men's restroom",
1861
+ group: "facilities",
1862
+ 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"
1863
+ },
1864
+ {
1865
+ key: "restroom-women",
1866
+ label: "Women's restroom",
1867
+ group: "facilities",
1868
+ 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"
1869
+ },
1870
+ {
1871
+ key: "restroom-accessible",
1872
+ label: "Accessible restroom",
1873
+ group: "facilities",
1874
+ path: WHEELCHAIR
1875
+ },
1876
+ {
1877
+ key: "first-aid",
1878
+ label: "First aid",
1879
+ group: "facilities",
1880
+ 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"
1881
+ },
1882
+ {
1883
+ key: "coat-check",
1884
+ label: "Coat check",
1885
+ group: "facilities",
1886
+ path: "M12 6a2 2 0 0 1 0-4 2 2 0 0 1 1.6 3.2L21 13H3l8.4-7.8 M3 13h18"
1887
+ },
1888
+ {
1889
+ key: "atm",
1890
+ label: "ATM / cash",
1891
+ group: "facilities",
1892
+ path: "M2 6h20v12H2V6Z M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0 M6 9h.01 M18 15h.01"
1893
+ },
1894
+ {
1895
+ key: "info",
1896
+ label: "Info point",
1897
+ group: "facilities",
1898
+ path: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18 M12 11v5 M12 8h.01"
1899
+ },
1900
+ {
1901
+ key: "lost-found",
1902
+ label: "Lost & found",
1903
+ group: "facilities",
1904
+ 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"
1905
+ },
1906
+ {
1907
+ key: "restrooms",
1908
+ label: "Restrooms",
1909
+ group: "facilities",
1910
+ path: "M6 3h12a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z M12 8a2 2 0 1 0 0 4 2 2 0 0 0 0-4 M9 18v-3.5a3 3 0 0 1 6 0V18"
1911
+ },
1912
+ {
1913
+ key: "charging",
1914
+ label: "Charging point",
1915
+ group: "facilities",
1916
+ path: "M3 8h13v8H3V8Z M16 10h3v4h-3 M9.6 9l-2 3.5h2.5l-1.5 3"
1917
+ },
1918
+ {
1919
+ key: "smoking",
1920
+ label: "Smoking area",
1921
+ group: "facilities",
1922
+ path: "M2 15h13v3H2v-3Z M18 16h2 M19 8c0 1 1 1.5 1 2.5S19 12 19 13"
1923
+ },
1924
+ {
1925
+ key: "no-smoking",
1926
+ label: "No smoking",
1927
+ group: "facilities",
1928
+ 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"
1929
+ },
1930
+ // ---- Food & drink ------------------------------------------------------
1931
+ {
1932
+ key: "food",
1933
+ label: "Food",
1934
+ group: "food-drink",
1935
+ 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"
1936
+ },
1937
+ {
1938
+ key: "bar",
1939
+ label: "Bar",
1940
+ group: "food-drink",
1941
+ path: "M4 5h16l-8 8-8-8Z M12 13v6 M8 20h8"
1942
+ },
1943
+ {
1944
+ key: "coffee",
1945
+ label: "Caf\xE9 / coffee",
1946
+ group: "food-drink",
1947
+ 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"
1948
+ },
1949
+ {
1950
+ key: "water",
1951
+ label: "Water / drinks",
1952
+ group: "food-drink",
1953
+ path: "M12 22a7 7 0 0 1-7-7c0-5 7-12 7-12s7 7 7 12a7 7 0 0 1-7 7Z"
1954
+ },
1955
+ {
1956
+ key: "merch",
1957
+ label: "Merch / shop",
1958
+ group: "food-drink",
1959
+ path: "M6 8h12l-1 12H7L6 8Z M9 8V6a3 3 0 0 1 6 0v2"
1960
+ },
1961
+ {
1962
+ key: "screen",
1963
+ label: "Screen",
1964
+ group: "facilities",
1965
+ path: "M3 4h18v12H3V4Z M9 20h6 M12 16v4 M10 8l4 2-4 2V8Z"
1966
+ },
1967
+ {
1968
+ key: "sound-booth",
1969
+ label: "Sound booth",
1970
+ group: "facilities",
1971
+ path: "M6 4v16 M6 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4 M12 4v16 M12 8a2 2 0 1 0 0 4 2 2 0 0 0 0-4 M18 4v16 M18 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4"
1972
+ },
1973
+ // ---- Getting around ----------------------------------------------------
1974
+ {
1975
+ key: "entrance",
1976
+ label: "Entrance",
1977
+ group: "navigation",
1978
+ path: "M14 3h5a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-5 M3 12h11 M10 8l4 4-4 4"
1979
+ },
1980
+ {
1981
+ key: "exit",
1982
+ label: "Exit",
1983
+ group: "navigation",
1984
+ path: "M10 3H5a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h5 M14 12h7 M17 8l4 4-4 4"
1985
+ },
1986
+ {
1987
+ key: "emergency-exit",
1988
+ label: "Emergency exit",
1989
+ group: "navigation",
1990
+ 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"
1991
+ },
1992
+ {
1993
+ key: "stairs",
1994
+ label: "Stairs",
1995
+ group: "navigation",
1996
+ path: "M3 20v-4h4v-4h4v-4h4V4h5"
1997
+ },
1998
+ {
1999
+ key: "elevator",
2000
+ label: "Elevator",
2001
+ group: "navigation",
2002
+ 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"
2003
+ },
2004
+ {
2005
+ key: "parking",
2006
+ label: "Parking",
2007
+ group: "navigation",
2008
+ 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"
2009
+ },
2010
+ // ---- Accessibility -----------------------------------------------------
2011
+ {
2012
+ key: "wheelchair",
2013
+ label: "Wheelchair access",
2014
+ group: "accessibility",
2015
+ path: WHEELCHAIR
2016
+ },
2017
+ {
2018
+ key: "hearing",
2019
+ label: "Hearing assistance",
2020
+ group: "accessibility",
2021
+ 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"
2022
+ }
2023
+ ];
2024
+ var BY_KEY = new Map(VENUE_ICONS.map((icon) => [icon.key, icon]));
2025
+ function venueIcon(key) {
2026
+ return key ? BY_KEY.get(key) : void 0;
2027
+ }
2028
+ function venueIconPath(key) {
2029
+ return venueIcon(key)?.path;
2030
+ }
2031
+ var VENUE_ICON_KEYS = VENUE_ICONS.map((icon) => icon.key);
2032
+
1826
2033
  // src/core/spatialIndex.ts
1827
2034
  var DEFAULT_SEAT_RADIUS = 9;
1828
2035
  function halfExtents(shape) {
@@ -2160,6 +2367,7 @@ var en = {
2160
2367
  // renderer (drawn on the Konva map — shared by the embed SDK)
2161
2368
  "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
2162
2369
  "map.seatsLeft": "{count} LEFT",
2370
+ "map.soldOut": "SOLD OUT",
2163
2371
  "map.fromPrice": "FROM {price}",
2164
2372
  "map.statusHeld": "On hold",
2165
2373
  "map.statusTaken": "Taken",
@@ -2167,7 +2375,7 @@ var en = {
2167
2375
  "picker.floor": "Floor",
2168
2376
  "picker.zoomLevel": "Zoom level",
2169
2377
  "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",
2378
+ "picker.rungTip.sections": "Section blocks \u2014 sold-out and nearly-gone sections shade to show availability at a glance",
2171
2379
  "picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
2172
2380
  "picker.rungLabel.zones": "ZONES",
2173
2381
  "picker.rungLabel.sections": "SECTIONS",
@@ -2368,6 +2576,22 @@ function overviewPalette(canvasBackground) {
2368
2576
  focalStroke: DARK_OVERVIEW_FOCAL_STROKE
2369
2577
  };
2370
2578
  }
2579
+ var SECTION_NEARLY_GONE_RATIO = 0.1;
2580
+ function overviewStateFill(palette, state) {
2581
+ const base = palette.sectionFill;
2582
+ const soldOut = desaturate(darken(base, 0.2), 0.85);
2583
+ switch (state) {
2584
+ case "closed":
2585
+ return darken(base, 0.12);
2586
+ case "sold-out":
2587
+ return soldOut;
2588
+ case "nearly-gone":
2589
+ return lerpColor(base, soldOut, 0.4);
2590
+ case "normal":
2591
+ default:
2592
+ return base;
2593
+ }
2594
+ }
2371
2595
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
2372
2596
  function seatIdOf(target) {
2373
2597
  const n = target;
@@ -2390,6 +2614,13 @@ function darken(hex, amt) {
2390
2614
  const mix = (c) => Math.round(c * (1 - amt));
2391
2615
  return `#${(1 << 24 | mix(n >> 16 & 255) << 16 | mix(n >> 8 & 255) << 8 | mix(n & 255)).toString(16).slice(1)}`;
2392
2616
  }
2617
+ function desaturate(hex, amt) {
2618
+ const rgb2 = hexToRgb(hex);
2619
+ if (!rgb2) return hex;
2620
+ const [r, g, b] = rgb2;
2621
+ const grey = 0.299 * r + 0.587 * g + 0.114 * b;
2622
+ return toHex(r + (grey - r) * amt, g + (grey - g) * amt, b + (grey - b) * amt);
2623
+ }
2393
2624
  function polyBounds(pts) {
2394
2625
  let minX = Infinity;
2395
2626
  let minY = Infinity;
@@ -2453,18 +2684,36 @@ function polygonLabelCandidates(outer, holes, preferred) {
2453
2684
  }
2454
2685
  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
2686
  }
2687
+ function roundedPolygonSubpath(context, points, radius) {
2688
+ const n = points.length;
2689
+ const start = { x: (points[n - 1].x + points[0].x) / 2, y: (points[n - 1].y + points[0].y) / 2 };
2690
+ context.moveTo(start.x, start.y);
2691
+ for (let i = 0; i < n; i += 1) {
2692
+ const curr = points[i];
2693
+ const next = points[(i + 1) % n];
2694
+ const prev = points[(i - 1 + n) % n];
2695
+ const rMax = Math.min(
2696
+ Math.hypot(curr.x - prev.x, curr.y - prev.y) / 2,
2697
+ Math.hypot(next.x - curr.x, next.y - curr.y) / 2
2698
+ );
2699
+ context.arcTo(curr.x, curr.y, next.x, next.y, Math.max(0, Math.min(radius, rMax)));
2700
+ }
2701
+ context.closePath();
2702
+ }
2456
2703
  function polygonWithHolesShape(outer, holes, attrs, outerPath) {
2457
2704
  const signedArea = (points) => points.reduce((sum, point, index) => {
2458
2705
  const next = points[(index + 1) % points.length];
2459
2706
  return sum + point.x * next.y - next.x * point.y;
2460
2707
  }, 0);
2461
2708
  const outerClockwise = signedArea(outer) > 0;
2709
+ const cornerRadius = attrs.cornerRadius && attrs.cornerRadius > 0 ? attrs.cornerRadius : 0;
2462
2710
  return new import_Shape.Shape({
2463
2711
  ...attrs,
2464
2712
  sceneFunc(context, shape) {
2465
2713
  context.beginPath();
2466
2714
  const polygonPath = (points) => {
2467
2715
  if (!points.length) return;
2716
+ if (cornerRadius > 0 && points.length >= 3) return roundedPolygonSubpath(context, points, cornerRadius);
2468
2717
  context.moveTo(points[0].x, points[0].y);
2469
2718
  for (let index = 1; index < points.length; index += 1) context.lineTo(points[index].x, points[index].y);
2470
2719
  context.closePath();
@@ -2599,6 +2848,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2599
2848
  this.accessGlyphById = /* @__PURE__ */ new Map();
2600
2849
  /** Authored free-text nodes obey the same rendered-size visibility floor. */
2601
2850
  this.freeTextById = /* @__PURE__ */ new Map();
2851
+ /** Vector venue-icon Path nodes + authored size; obey the free-text size floor. */
2852
+ this.iconNodeById = /* @__PURE__ */ new Map();
2602
2853
  /** Stage/rink landmarks retain a readable screen-space caption at overview. */
2603
2854
  this.primaryFocalLabels = /* @__PURE__ */ new Map();
2604
2855
  /** GA paint and text share price/highlight filter state. */
@@ -2612,6 +2863,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2612
2863
  this.effSelection = DEF_SELECTION;
2613
2864
  /** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
2614
2865
  this.colorblind = false;
2866
+ /** Stable per-category-KEY colorblind hue (OV-46) — order-independent. */
2867
+ this.cbHueByKey = /* @__PURE__ */ new Map();
2868
+ /** OV-45(a): authored row-letter labels for the buyer/preview map, resolved in
2869
+ * world space (matching seat labels) and honouring each row's labelPresentation
2870
+ * (position/rotation/visibility/style). Painted at the seats LOD rung. */
2871
+ this.rowLabelPlan = [];
2615
2872
  /** Category order from the doc — the stable index into the CB palette. */
2616
2873
  this.catOrder = [];
2617
2874
  this.selection = /* @__PURE__ */ new Set();
@@ -2981,6 +3238,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2981
3238
  this.accessRingById.clear();
2982
3239
  this.accessGlyphById.clear();
2983
3240
  this.freeTextById.clear();
3241
+ this.iconNodeById.clear();
2984
3242
  this.primaryFocalLabels.clear();
2985
3243
  this.gaById.clear();
2986
3244
  for (const marker of this.selectionMarkers.values()) marker.destroy();
@@ -3035,13 +3293,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3035
3293
  this.catColor.clear();
3036
3294
  this.catPrice.clear();
3037
3295
  this.catOrder = doc.categories.map((c) => c.key);
3296
+ this.cbHueByKey.clear();
3297
+ [...this.catOrder].sort().forEach((key, i) => {
3298
+ this.cbHueByKey.set(key, CB_PALETTE[i % CB_PALETTE.length]);
3299
+ });
3038
3300
  for (const c of doc.categories) {
3039
3301
  this.catColor.set(c.key, c.color);
3040
3302
  if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
3041
3303
  }
3042
3304
  for (const obj of view.objects) {
3043
3305
  if (obj.type === "booth") {
3044
- this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
3306
+ this.boothDims.set(obj.id, {
3307
+ width: obj.width,
3308
+ height: obj.height,
3309
+ rotation: obj.rotation,
3310
+ ...obj.points && obj.points.length >= 3 ? { points: obj.points.map((p) => ({ x: p.x, y: p.y })) } : {}
3311
+ });
3045
3312
  }
3046
3313
  }
3047
3314
  this.seats = expandChart(view, {
@@ -3061,6 +3328,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3061
3328
  this.unsectionedSeatGroup = new import_Group.Group({ listening: true });
3062
3329
  this.seatLayer.add(this.unsectionedSeatGroup);
3063
3330
  this.renderSeats();
3331
+ this.buildRowLabelPlan(view);
3064
3332
  this.overlayLayer.add(this.labelGroup);
3065
3333
  this.overlayLayer.add(this.fgDecorGroup);
3066
3334
  this.overlayLayer.add(this.hoverRing);
@@ -4360,7 +4628,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4360
4628
  this.accessRingById.set(seat.id, ring);
4361
4629
  target.add(ring);
4362
4630
  }
4363
- if (seat.accessibility?.includes("wheelchair")) {
4631
+ if (accessGlyphPath(seat.accessibility ?? [])) {
4364
4632
  const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
4365
4633
  this.accessGlyphById.set(seat.id, glyph);
4366
4634
  target.add(glyph);
@@ -4372,25 +4640,42 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4372
4640
  renderBoothUnit(seat) {
4373
4641
  const target = this.seatContainer(seat.id);
4374
4642
  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);
4643
+ let labelX = seat.x;
4644
+ let labelY = seat.y;
4645
+ let node;
4646
+ if (dims.points && dims.points.length >= 3) {
4647
+ node = new import_Line.Line({
4648
+ x: seat.x,
4649
+ y: seat.y,
4650
+ points: dims.points.flatMap((p) => [p.x, p.y]),
4651
+ closed: true,
4652
+ perfectDrawEnabled: false,
4653
+ shadowForStrokeEnabled: false,
4654
+ hitStrokeWidth: 0
4655
+ });
4656
+ labelX = seat.x + dims.points.reduce((a, p) => a + p.x, 0) / dims.points.length;
4657
+ labelY = seat.y + dims.points.reduce((a, p) => a + p.y, 0) / dims.points.length;
4658
+ } else {
4659
+ node = new import_Rect.Rect({
4660
+ x: seat.x,
4661
+ y: seat.y,
4662
+ width: dims.width,
4663
+ height: dims.height,
4664
+ offsetX: dims.width / 2,
4665
+ offsetY: dims.height / 2,
4666
+ rotation: dims.rotation,
4667
+ cornerRadius: 4,
4668
+ perfectDrawEnabled: false,
4669
+ shadowForStrokeEnabled: false,
4670
+ hitStrokeWidth: 0
4671
+ });
4672
+ }
4673
+ node.setAttr("seatId", seat.id);
4674
+ this.circleById.set(seat.id, node);
4675
+ target.add(node);
4391
4676
  const t2 = new import_Text.Text({
4392
- x: seat.x,
4393
- y: seat.y,
4677
+ x: labelX,
4678
+ y: labelY,
4394
4679
  text: seat.displayLabel ?? seat.label,
4395
4680
  fontSize: BOOTH_LABEL_FONT_SIZE,
4396
4681
  fontStyle: "600",
@@ -4405,7 +4690,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4405
4690
  this.hasBoothText = true;
4406
4691
  this.boothLabelById.set(seat.id, t2);
4407
4692
  target.add(t2);
4408
- this.paintSeat(rect, seat.id);
4693
+ this.paintSeat(node, seat.id);
4409
4694
  }
4410
4695
  /**
4411
4696
  * Build the centred accessibility glyph for a seat. Sized relative to the seat
@@ -4417,7 +4702,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4417
4702
  return new import_Path.Path({
4418
4703
  x: seat.x,
4419
4704
  y: seat.y,
4420
- data: ACCESS_GLYPH_PATH,
4705
+ // Non-null: buildAccessGlyph is only called for seats the gate confirmed
4706
+ // resolve to a primary-accommodation glyph.
4707
+ data: accessGlyphPath(seat.accessibility ?? []) ?? "",
4421
4708
  offsetX: ACCESS_GLYPH_VIEWBOX / 2,
4422
4709
  offsetY: ACCESS_GLYPH_VIEWBOX / 2,
4423
4710
  scaleX: k,
@@ -4473,8 +4760,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4473
4760
  /** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
4474
4761
  seatBaseColor(categoryKey) {
4475
4762
  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];
4763
+ return this.cbHueByKey.get(categoryKey) ?? CB_PALETTE[0];
4478
4764
  }
4479
4765
  /** Per-seat authored label size relative to the neutral default (1 = default),
4480
4766
  * mirroring the section convention (labelStyle.size ?? 18) / 18. Only real
@@ -4945,6 +5231,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4945
5231
  renderText(obj) {
4946
5232
  const background = this.canvasBackground;
4947
5233
  const preferredInk = obj.color ?? this.theme.textColor ?? DEF_TEXT;
5234
+ const iconPath = obj.semanticKind === "icon" ? venueIconPath(obj.iconKey) : void 0;
5235
+ if (iconPath) {
5236
+ const scale = obj.fontSize / VENUE_ICON_VIEWBOX;
5237
+ const iconNode = new import_Path.Path({
5238
+ x: obj.position.x,
5239
+ y: obj.position.y,
5240
+ data: iconPath,
5241
+ rotation: obj.rotation,
5242
+ scaleX: scale,
5243
+ scaleY: scale,
5244
+ stroke: stateAwareBookableLabelInk(background, preferredInk),
5245
+ strokeWidth: VENUE_ICON_STROKE,
5246
+ lineCap: "round",
5247
+ lineJoin: "round",
5248
+ fillEnabled: false,
5249
+ listening: false,
5250
+ perfectDrawEnabled: false
5251
+ });
5252
+ this.iconNodeById.set(obj.id, { node: iconNode, fontSize: obj.fontSize });
5253
+ this.bgLayer.add(iconNode);
5254
+ return;
5255
+ }
4948
5256
  const node = new import_Text.Text({
4949
5257
  x: obj.position.x,
4950
5258
  y: obj.position.y,
@@ -5112,7 +5420,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5112
5420
  fill: color,
5113
5421
  opacity: GA_FILL_OPACITY,
5114
5422
  stroke: color,
5115
- strokeWidth: 1.5
5423
+ strokeWidth: 1.5,
5424
+ cornerRadius: obj.cornerRadius
5116
5425
  });
5117
5426
  poly.setAttr("gaId", obj.id);
5118
5427
  poly.on("click tap", (e) => {
@@ -5324,20 +5633,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5324
5633
  sec.outlinePoly.shadowBlur(4 + raw * 12);
5325
5634
  sec.outlinePoly.shadowOpacity(0.25 + raw * 0.45);
5326
5635
  }
5327
- /** Recompute a section's neutral overview state and retained detail count. */
5636
+ /** Recompute a section's availability shading + retained availability text. */
5328
5637
  refreshSectionFill(sec) {
5329
5638
  sec.blockPoly.fill(this.sectionBlockFill(sec));
5330
- sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
5639
+ sec.subLabel.text(
5640
+ this.sectionAvailabilityState(sec) === "sold-out" ? t("map.soldOut") : t("map.seatsLeft", { count: sec.free })
5641
+ );
5331
5642
  sec.subLabel.offsetX(sec.subLabel.width() / 2);
5332
5643
  }
5333
5644
  /** True when a section/zone is currently in the `closed` event-state. */
5334
5645
  isSectionClosed(sec) {
5335
5646
  return this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone);
5336
5647
  }
5337
- /** Clean overview shells never leak category, price, or live availability paint. */
5648
+ /** Resolve the four-bucket overview availability state for a section (OV-69).
5649
+ * `closed` (operator) outranks live availability; the availability buckets
5650
+ * only apply to seated sections (GA-only shells have no seat capacity here). */
5651
+ sectionAvailabilityState(sec) {
5652
+ if (this.isSectionClosed(sec)) return "closed";
5653
+ if (sec.total <= 0) return "normal";
5654
+ if (sec.free <= 0) return "sold-out";
5655
+ if (sec.free <= sec.total * SECTION_NEARLY_GONE_RATIO) return "nearly-gone";
5656
+ return "normal";
5657
+ }
5658
+ /** Overview shells stay neutral except for the bounded availability shading
5659
+ * (nearly-gone / sold-out) and the operator `closed` slab — never category
5660
+ * or price paint, which is deferred to section focus/seat detail. */
5338
5661
  sectionBlockFill(sec) {
5339
- const fill = overviewPalette(this.canvasBackground).sectionFill;
5340
- return this.isSectionClosed(sec) ? darken(fill, 0.12) : fill;
5662
+ return overviewStateFill(overviewPalette(this.canvasBackground), this.sectionAvailabilityState(sec));
5341
5663
  }
5342
5664
  /**
5343
5665
  * Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
@@ -6343,10 +6665,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6343
6665
  };
6344
6666
  });
6345
6667
  const palette = overviewPalette(this.canvasBackground);
6346
- const neutralSectionFills = /* @__PURE__ */ new Set([
6347
- palette.sectionFill.toLowerCase(),
6348
- darken(palette.sectionFill, 0.12).toLowerCase()
6349
- ]);
6668
+ const neutralSectionFills = new Set(
6669
+ ["normal", "nearly-gone", "sold-out", "closed"].map((state) => overviewStateFill(palette, state).toLowerCase())
6670
+ );
6350
6671
  const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
6351
6672
  return {
6352
6673
  viewport,
@@ -6516,7 +6837,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6516
6837
  if (shouldCache && !this.cached) {
6517
6838
  this.cacheSeatLayer();
6518
6839
  } else if (!shouldCache && (this.cached || !this.seatLayer.listening())) {
6519
- if (this.cached) this.seatLayer.clearCache();
6840
+ if (this.cached) this.releaseSeatLayerBitmap();
6520
6841
  this.seatLayer.listening(true);
6521
6842
  this.cached = false;
6522
6843
  this.seatLayer.batchDraw();
@@ -6536,6 +6857,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6536
6857
  this.rebuildSeatCache();
6537
6858
  this.seatLayer.batchDraw();
6538
6859
  }
6860
+ /**
6861
+ * Drop the seat-layer overview bitmap while PRESERVING every descendant's
6862
+ * cached local transform + clientRect.
6863
+ *
6864
+ * Konva's public `Layer.clearCache()` releases the bitmap and then runs a deep
6865
+ * `_clearSelfAndDescendantCache()` that clears those caches on all 14k seat
6866
+ * nodes. Recomputing them on the single frame the melt first reveals a section's
6867
+ * live seats is the ~480ms first-drill stall (OV-80). A camera glide only marks
6868
+ * each seat's ABSOLUTE_TRANSFORM dirty (recomputed lazily at ordinary pan cost)
6869
+ * and never moves a seat's LOCAL transform, so the local caches stay valid —
6870
+ * keeping them lets the first melt paint at pan cost, matching the ~17ms of
6871
+ * every subsequent glide on the same section. Any edit that actually moves a
6872
+ * seat invalidates its own transform cache through the attr setter, so only
6873
+ * still-valid caches survive here.
6874
+ */
6875
+ releaseSeatLayerBitmap() {
6876
+ const layer = this.seatLayer;
6877
+ const entry = layer._cache.get("canvas");
6878
+ if (!entry) return;
6879
+ import_Core.Konva.Util.releaseCanvas(entry.scene._canvas, entry.filter._canvas, entry.hit._canvas);
6880
+ layer._cache.delete("canvas");
6881
+ }
6539
6882
  /**
6540
6883
  * SYNCHRONOUS repaint that bypasses requestAnimationFrame — see the
6541
6884
  * ISeatmapRenderer.forceDraw() contract. Chrome pauses rAF (and therefore
@@ -6554,7 +6897,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6554
6897
  if (this.effScale() < CACHE_THRESHOLD) {
6555
6898
  this.rebuildSeatCache();
6556
6899
  } else if (this.cached) {
6557
- this.seatLayer.clearCache();
6900
+ this.releaseSeatLayerBitmap();
6558
6901
  this.seatLayer.listening(true);
6559
6902
  this.cached = false;
6560
6903
  }
@@ -6570,6 +6913,63 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6570
6913
  const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
6571
6914
  node.visible(!gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
6572
6915
  }
6916
+ for (const { node, fontSize } of this.iconNodeById.values()) {
6917
+ node.visible(isBookableLabelLegibleAtScale(fontSize, effectiveScale));
6918
+ }
6919
+ }
6920
+ /** OV-45(a): resolve authored row-letter labels for the buyer/preview map, in
6921
+ * world space, matching the Designer's placement contract (start/end/both/none
6922
+ * preset, a free-drag `position` that wins over the preset, rotation, and
6923
+ * labelStyle colour/size fed through the same auto-contrast guard). Segmented
6924
+ * rows keep their own logical-label handling and are resolved by their owner. */
6925
+ buildRowLabelPlan(view) {
6926
+ this.rowLabelPlan = [];
6927
+ const background = this.theme.background ?? "#0e1117";
6928
+ const seatsByRow = /* @__PURE__ */ new Map();
6929
+ for (const seat of this.seats) {
6930
+ if (seat.kind === "booth") continue;
6931
+ const key = seat.logicalRowId ?? seat.rowId;
6932
+ (seatsByRow.get(key) ?? seatsByRow.set(key, []).get(key)).push(seat);
6933
+ }
6934
+ const seatOrder = (seat) => seat.logicalSeatIndex ?? Number(seat.id.split(":").pop() ?? 0);
6935
+ for (const obj of view.objects) {
6936
+ if (obj.type !== "row") continue;
6937
+ if (obj.segmentedRow && obj.segmentedRow.componentIndex !== 0) continue;
6938
+ const presentation = obj.segmentedRow?.labelPresentation ?? obj.labelPresentation;
6939
+ if (presentation?.visible === false || presentation?.positionPreset === "none") continue;
6940
+ const key = obj.segmentedRow ? obj.segmentedRow.groupId : obj.id;
6941
+ const seats = (seatsByRow.get(key) ?? []).slice().sort((a, b) => seatOrder(a) - seatOrder(b));
6942
+ if (!seats.length) continue;
6943
+ const first = seats[0];
6944
+ const last = seats[seats.length - 1];
6945
+ const labelStyle = presentation?.labelStyle;
6946
+ const ink = stateAwareBookableLabelInk(
6947
+ background,
6948
+ labelStyle?.color ?? this.theme.rowLabelColor ?? this.theme.textColor ?? "#e6e9f0"
6949
+ );
6950
+ const fontSize = labelStyle?.size ?? 12;
6951
+ const rotation = presentation?.rotation ?? 0;
6952
+ const text = obj.segmentedRow?.displayLabel ?? obj.displayLabel ?? obj.label;
6953
+ const opacity = this.objectFilteredOut(obj) ? 0.15 : 1;
6954
+ const push = (x, y) => this.rowLabelPlan.push({ text, x, y, rotation, fontSize, ink, opacity });
6955
+ if (presentation?.position) {
6956
+ push(presentation.position.x, presentation.position.y);
6957
+ continue;
6958
+ }
6959
+ const radians = obj.rotation * Math.PI / 180;
6960
+ const along = { x: Math.cos(radians), y: Math.sin(radians) };
6961
+ const gap = this.seatR + 12;
6962
+ const preset = presentation?.positionPreset ?? "start";
6963
+ if (preset === "start" || preset === "both") push(first.x - along.x * gap, first.y - along.y * gap);
6964
+ if (preset === "end" || preset === "both") push(last.x + along.x * gap, last.y + along.y * gap);
6965
+ }
6966
+ }
6967
+ /** Row labels never carry status; the buyer just dims them under the same
6968
+ * category/price filters that dim their seats. */
6969
+ objectFilteredOut(obj) {
6970
+ const key = obj.categoryKey;
6971
+ if (!key) return false;
6972
+ return Boolean(this.categoryHighlight && key !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(key));
6573
6973
  }
6574
6974
  updateLabels() {
6575
6975
  const effectiveScale = this.effScale();
@@ -6663,6 +7063,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6663
7063
  this.seatLabelById.set(seat.id, t2);
6664
7064
  if (++count >= MAX_LABELS) break;
6665
7065
  }
7066
+ for (const rl of this.rowLabelPlan) {
7067
+ const screen = this.worldToScreen({ x: rl.x, y: rl.y });
7068
+ if (screen.x < -40 || screen.x > this.stage.width() + 40 || screen.y < -40 || screen.y > this.stage.height() + 40) continue;
7069
+ const t2 = new import_Text.Text({
7070
+ x: rl.x,
7071
+ y: rl.y,
7072
+ text: rl.text,
7073
+ fontSize: rl.fontSize,
7074
+ fontStyle: "700",
7075
+ fontFamily: this.labelFont(),
7076
+ fill: rl.ink,
7077
+ rotation: rl.rotation,
7078
+ opacity: rl.opacity,
7079
+ shadowColor: "#05070c",
7080
+ shadowBlur: rl.ink.toLowerCase() === "#ffffff" ? 5 : 0,
7081
+ shadowOpacity: rl.ink.toLowerCase() === "#ffffff" ? 0.8 : 0,
7082
+ shadowForStrokeEnabled: false,
7083
+ listening: false,
7084
+ perfectDrawEnabled: false
7085
+ });
7086
+ t2.offsetX(t2.width() / 2);
7087
+ t2.offsetY(t2.height() / 2);
7088
+ this.labelGroup.add(t2);
7089
+ }
6666
7090
  if (this.isoT > 0 && this.viewMode !== "perspective") this.applyUprightLabels();
6667
7091
  this.overlayLayer.batchDraw();
6668
7092
  }
@@ -6994,6 +7418,36 @@ var PickerController = class {
6994
7418
  const group = this.groupedTableBySeatId.get(seatIdOrLabel) ?? this.groupedTablesByLabel.get(seatIdOrLabel);
6995
7419
  return group ? this.toTableSeat(group) : null;
6996
7420
  }
7421
+ /**
7422
+ * Buyer-facing name + type word for a checkout line item, resolved from the
7423
+ * SAME author overrides the tray/tooltip already render. `label` stays the
7424
+ * booking identity; a host builds its own order summary from `displayLabel`/
7425
+ * `displayType` so the buyer sees the designer's copy, not the internal label.
7426
+ * Absent fields = no override (host falls back to `label`).
7427
+ */
7428
+ lineItemDisplay(item) {
7429
+ if (item.objectType === "ga") {
7430
+ const doc = this.visibleDoc();
7431
+ const area = gaAreasOf(doc).find((candidate) => candidate.id === item.objectId);
7432
+ return {
7433
+ ...area?.displayLabel ? { displayLabel: area.displayLabel } : {},
7434
+ ...area?.displayType ? { displayType: area.displayType } : {}
7435
+ };
7436
+ }
7437
+ if (item.objectType === "table") {
7438
+ const group = this.groupedTablesByLabel.get(item.label);
7439
+ return {
7440
+ ...group?.displayLabel && group.displayLabel !== group.label ? { displayLabel: group.displayLabel } : {},
7441
+ ...group?.displayType ? { displayType: group.displayType } : {}
7442
+ };
7443
+ }
7444
+ const seat = this.labelToSeat.get(item.label);
7445
+ const context = seat ? this.seatContext.get(seat.id) : void 0;
7446
+ return {
7447
+ ...seat?.displayLabel ? { displayLabel: seat.displayLabel } : {},
7448
+ ...context?.displayType ? { displayType: context.displayType } : {}
7449
+ };
7450
+ }
6997
7451
  /** Fetch chart, build label maps, mount the renderer, seed statuses, go live. */
6998
7452
  async render(host) {
6999
7453
  if (this.renderer) return null;
@@ -7909,7 +8363,7 @@ var PickerController = class {
7909
8363
  const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
7910
8364
  return {
7911
8365
  id,
7912
- label: sec.label,
8366
+ label: sec.displayLabel ?? sec.label,
7913
8367
  zoneLabel: zone?.label ?? "",
7914
8368
  ...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
7915
8369
  color,