@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.js CHANGED
@@ -1034,7 +1034,30 @@ function isBookableLabelLegibleAtScale(fontSize, effectiveScale) {
1034
1034
  return fontSize * effectiveScale >= MIN_VISIBLE_BOOKABLE_LABEL_PX;
1035
1035
  }
1036
1036
  var ACCESS_GLYPH_VIEWBOX = 24;
1037
- 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";
1037
+ var ACCESS_TYPE_GLYPH_PATHS = {
1038
+ // ISO-style wheelchair: seated figure (head + L-shaped body) over a ring wheel.
1039
+ 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",
1040
+ // Companion: two overlapping head-and-shoulders figures (seat + a guest).
1041
+ 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",
1042
+ // Semi-ambulatory: walking figure leaning on a cane at its right side.
1043
+ "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",
1044
+ // Hearing: a bold ear silhouette (assistive listening).
1045
+ 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",
1046
+ // CART captions: a rounded caption bubble with two bold caption bars cut out.
1047
+ 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",
1048
+ // Sign language: a raised open hand (palm with four fingers and a thumb).
1049
+ "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",
1050
+ // Plus-size: a broad-shouldered wide figure.
1051
+ "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",
1052
+ // Lift-up armrest: a chair side-profile with an up arrow over the armrest.
1053
+ "lift-armrest": "M5 6L8 6L8 19L5 19zM5 16L18 16L18 19L5 19zM8 12L17 12L17 14.5L8 14.5zM14 7L16 7L16 12L14 12zM12 7L18 7L15 3Z"
1054
+ };
1055
+ var ACCESS_GLYPH_PATH = ACCESS_TYPE_GLYPH_PATHS.wheelchair;
1056
+ function accessGlyphPath(accessibility) {
1057
+ if (!accessibility?.length) return null;
1058
+ const primary = accessibility.includes("wheelchair") ? "wheelchair" : accessibility[0];
1059
+ return ACCESS_TYPE_GLYPH_PATHS[primary] ?? null;
1060
+ }
1038
1061
  function bookableMarkerLabel(publicLabel) {
1039
1062
  return /-(\d{1,5})$/.exec(publicLabel)?.[1] ?? publicLabel;
1040
1063
  }
@@ -1547,6 +1570,187 @@ function buyerBackgroundImage(owner) {
1547
1570
  return background?.url && !background.assetId ? background : void 0;
1548
1571
  }
1549
1572
 
1573
+ // src/core/icons.ts
1574
+ var VENUE_ICON_VIEWBOX = 24;
1575
+ var VENUE_ICON_STROKE = 2;
1576
+ 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";
1577
+ var VENUE_ICONS = [
1578
+ // ---- Facilities --------------------------------------------------------
1579
+ {
1580
+ key: "restroom-men",
1581
+ label: "Men's restroom",
1582
+ group: "facilities",
1583
+ 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"
1584
+ },
1585
+ {
1586
+ key: "restroom-women",
1587
+ label: "Women's restroom",
1588
+ group: "facilities",
1589
+ 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"
1590
+ },
1591
+ {
1592
+ key: "restroom-accessible",
1593
+ label: "Accessible restroom",
1594
+ group: "facilities",
1595
+ path: WHEELCHAIR
1596
+ },
1597
+ {
1598
+ key: "first-aid",
1599
+ label: "First aid",
1600
+ group: "facilities",
1601
+ 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"
1602
+ },
1603
+ {
1604
+ key: "coat-check",
1605
+ label: "Coat check",
1606
+ group: "facilities",
1607
+ path: "M12 6a2 2 0 0 1 0-4 2 2 0 0 1 1.6 3.2L21 13H3l8.4-7.8 M3 13h18"
1608
+ },
1609
+ {
1610
+ key: "atm",
1611
+ label: "ATM / cash",
1612
+ group: "facilities",
1613
+ path: "M2 6h20v12H2V6Z M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0 M6 9h.01 M18 15h.01"
1614
+ },
1615
+ {
1616
+ key: "info",
1617
+ label: "Info point",
1618
+ group: "facilities",
1619
+ path: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18 M12 11v5 M12 8h.01"
1620
+ },
1621
+ {
1622
+ key: "lost-found",
1623
+ label: "Lost & found",
1624
+ group: "facilities",
1625
+ 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"
1626
+ },
1627
+ {
1628
+ key: "restrooms",
1629
+ label: "Restrooms",
1630
+ group: "facilities",
1631
+ 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"
1632
+ },
1633
+ {
1634
+ key: "charging",
1635
+ label: "Charging point",
1636
+ group: "facilities",
1637
+ path: "M3 8h13v8H3V8Z M16 10h3v4h-3 M9.6 9l-2 3.5h2.5l-1.5 3"
1638
+ },
1639
+ {
1640
+ key: "smoking",
1641
+ label: "Smoking area",
1642
+ group: "facilities",
1643
+ path: "M2 15h13v3H2v-3Z M18 16h2 M19 8c0 1 1 1.5 1 2.5S19 12 19 13"
1644
+ },
1645
+ {
1646
+ key: "no-smoking",
1647
+ label: "No smoking",
1648
+ group: "facilities",
1649
+ 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"
1650
+ },
1651
+ // ---- Food & drink ------------------------------------------------------
1652
+ {
1653
+ key: "food",
1654
+ label: "Food",
1655
+ group: "food-drink",
1656
+ 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"
1657
+ },
1658
+ {
1659
+ key: "bar",
1660
+ label: "Bar",
1661
+ group: "food-drink",
1662
+ path: "M4 5h16l-8 8-8-8Z M12 13v6 M8 20h8"
1663
+ },
1664
+ {
1665
+ key: "coffee",
1666
+ label: "Caf\xE9 / coffee",
1667
+ group: "food-drink",
1668
+ 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"
1669
+ },
1670
+ {
1671
+ key: "water",
1672
+ label: "Water / drinks",
1673
+ group: "food-drink",
1674
+ path: "M12 22a7 7 0 0 1-7-7c0-5 7-12 7-12s7 7 7 12a7 7 0 0 1-7 7Z"
1675
+ },
1676
+ {
1677
+ key: "merch",
1678
+ label: "Merch / shop",
1679
+ group: "food-drink",
1680
+ path: "M6 8h12l-1 12H7L6 8Z M9 8V6a3 3 0 0 1 6 0v2"
1681
+ },
1682
+ {
1683
+ key: "screen",
1684
+ label: "Screen",
1685
+ group: "facilities",
1686
+ path: "M3 4h18v12H3V4Z M9 20h6 M12 16v4 M10 8l4 2-4 2V8Z"
1687
+ },
1688
+ {
1689
+ key: "sound-booth",
1690
+ label: "Sound booth",
1691
+ group: "facilities",
1692
+ 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"
1693
+ },
1694
+ // ---- Getting around ----------------------------------------------------
1695
+ {
1696
+ key: "entrance",
1697
+ label: "Entrance",
1698
+ group: "navigation",
1699
+ path: "M14 3h5a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-5 M3 12h11 M10 8l4 4-4 4"
1700
+ },
1701
+ {
1702
+ key: "exit",
1703
+ label: "Exit",
1704
+ group: "navigation",
1705
+ path: "M10 3H5a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h5 M14 12h7 M17 8l4 4-4 4"
1706
+ },
1707
+ {
1708
+ key: "emergency-exit",
1709
+ label: "Emergency exit",
1710
+ group: "navigation",
1711
+ 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"
1712
+ },
1713
+ {
1714
+ key: "stairs",
1715
+ label: "Stairs",
1716
+ group: "navigation",
1717
+ path: "M3 20v-4h4v-4h4v-4h4V4h5"
1718
+ },
1719
+ {
1720
+ key: "elevator",
1721
+ label: "Elevator",
1722
+ group: "navigation",
1723
+ 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"
1724
+ },
1725
+ {
1726
+ key: "parking",
1727
+ label: "Parking",
1728
+ group: "navigation",
1729
+ 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"
1730
+ },
1731
+ // ---- Accessibility -----------------------------------------------------
1732
+ {
1733
+ key: "wheelchair",
1734
+ label: "Wheelchair access",
1735
+ group: "accessibility",
1736
+ path: WHEELCHAIR
1737
+ },
1738
+ {
1739
+ key: "hearing",
1740
+ label: "Hearing assistance",
1741
+ group: "accessibility",
1742
+ 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"
1743
+ }
1744
+ ];
1745
+ var BY_KEY = new Map(VENUE_ICONS.map((icon) => [icon.key, icon]));
1746
+ function venueIcon(key) {
1747
+ return key ? BY_KEY.get(key) : void 0;
1748
+ }
1749
+ function venueIconPath(key) {
1750
+ return venueIcon(key)?.path;
1751
+ }
1752
+ var VENUE_ICON_KEYS = VENUE_ICONS.map((icon) => icon.key);
1753
+
1550
1754
  // src/core/spatialIndex.ts
1551
1755
  var DEFAULT_SEAT_RADIUS = 9;
1552
1756
  function halfExtents(shape) {
@@ -1884,6 +2088,7 @@ var en = {
1884
2088
  // renderer (drawn on the Konva map — shared by the embed SDK)
1885
2089
  "map.aria": "Seating map. Use arrow keys to move between seats, Enter to select.",
1886
2090
  "map.seatsLeft": "{count} LEFT",
2091
+ "map.soldOut": "SOLD OUT",
1887
2092
  "map.fromPrice": "FROM {price}",
1888
2093
  "map.statusHeld": "On hold",
1889
2094
  "map.statusTaken": "Taken",
@@ -1891,7 +2096,7 @@ var en = {
1891
2096
  "picker.floor": "Floor",
1892
2097
  "picker.zoomLevel": "Zoom level",
1893
2098
  "picker.rungTip.zones": "Venue overview \u2014 groups of sections such as North Stand or VIP",
1894
- "picker.rungTip.sections": "Section blocks \u2014 category-mix fill + availability tint",
2099
+ "picker.rungTip.sections": "Section blocks \u2014 sold-out and nearly-gone sections shade to show availability at a glance",
1895
2100
  "picker.rungTip.seats": "Individual seats \u2014 blocks melt into dots",
1896
2101
  "picker.rungLabel.zones": "ZONES",
1897
2102
  "picker.rungLabel.sections": "SECTIONS",
@@ -2092,6 +2297,22 @@ function overviewPalette(canvasBackground) {
2092
2297
  focalStroke: DARK_OVERVIEW_FOCAL_STROKE
2093
2298
  };
2094
2299
  }
2300
+ var SECTION_NEARLY_GONE_RATIO = 0.1;
2301
+ function overviewStateFill(palette, state) {
2302
+ const base = palette.sectionFill;
2303
+ const soldOut = desaturate(darken(base, 0.2), 0.85);
2304
+ switch (state) {
2305
+ case "closed":
2306
+ return darken(base, 0.12);
2307
+ case "sold-out":
2308
+ return soldOut;
2309
+ case "nearly-gone":
2310
+ return lerpColor(base, soldOut, 0.4);
2311
+ case "normal":
2312
+ default:
2313
+ return base;
2314
+ }
2315
+ }
2095
2316
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
2096
2317
  function seatIdOf(target) {
2097
2318
  const n = target;
@@ -2114,6 +2335,13 @@ function darken(hex, amt) {
2114
2335
  const mix = (c) => Math.round(c * (1 - amt));
2115
2336
  return `#${(1 << 24 | mix(n >> 16 & 255) << 16 | mix(n >> 8 & 255) << 8 | mix(n & 255)).toString(16).slice(1)}`;
2116
2337
  }
2338
+ function desaturate(hex, amt) {
2339
+ const rgb2 = hexToRgb(hex);
2340
+ if (!rgb2) return hex;
2341
+ const [r, g, b] = rgb2;
2342
+ const grey = 0.299 * r + 0.587 * g + 0.114 * b;
2343
+ return toHex(r + (grey - r) * amt, g + (grey - g) * amt, b + (grey - b) * amt);
2344
+ }
2117
2345
  function polyBounds(pts) {
2118
2346
  let minX = Infinity;
2119
2347
  let minY = Infinity;
@@ -2177,18 +2405,36 @@ function polygonLabelCandidates(outer, holes, preferred) {
2177
2405
  }
2178
2406
  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));
2179
2407
  }
2408
+ function roundedPolygonSubpath(context, points, radius) {
2409
+ const n = points.length;
2410
+ const start = { x: (points[n - 1].x + points[0].x) / 2, y: (points[n - 1].y + points[0].y) / 2 };
2411
+ context.moveTo(start.x, start.y);
2412
+ for (let i = 0; i < n; i += 1) {
2413
+ const curr = points[i];
2414
+ const next = points[(i + 1) % n];
2415
+ const prev = points[(i - 1 + n) % n];
2416
+ const rMax = Math.min(
2417
+ Math.hypot(curr.x - prev.x, curr.y - prev.y) / 2,
2418
+ Math.hypot(next.x - curr.x, next.y - curr.y) / 2
2419
+ );
2420
+ context.arcTo(curr.x, curr.y, next.x, next.y, Math.max(0, Math.min(radius, rMax)));
2421
+ }
2422
+ context.closePath();
2423
+ }
2180
2424
  function polygonWithHolesShape(outer, holes, attrs, outerPath) {
2181
2425
  const signedArea = (points) => points.reduce((sum, point, index) => {
2182
2426
  const next = points[(index + 1) % points.length];
2183
2427
  return sum + point.x * next.y - next.x * point.y;
2184
2428
  }, 0);
2185
2429
  const outerClockwise = signedArea(outer) > 0;
2430
+ const cornerRadius = attrs.cornerRadius && attrs.cornerRadius > 0 ? attrs.cornerRadius : 0;
2186
2431
  return new Shape({
2187
2432
  ...attrs,
2188
2433
  sceneFunc(context, shape) {
2189
2434
  context.beginPath();
2190
2435
  const polygonPath = (points) => {
2191
2436
  if (!points.length) return;
2437
+ if (cornerRadius > 0 && points.length >= 3) return roundedPolygonSubpath(context, points, cornerRadius);
2192
2438
  context.moveTo(points[0].x, points[0].y);
2193
2439
  for (let index = 1; index < points.length; index += 1) context.lineTo(points[index].x, points[index].y);
2194
2440
  context.closePath();
@@ -2323,6 +2569,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2323
2569
  this.accessGlyphById = /* @__PURE__ */ new Map();
2324
2570
  /** Authored free-text nodes obey the same rendered-size visibility floor. */
2325
2571
  this.freeTextById = /* @__PURE__ */ new Map();
2572
+ /** Vector venue-icon Path nodes + authored size; obey the free-text size floor. */
2573
+ this.iconNodeById = /* @__PURE__ */ new Map();
2326
2574
  /** Stage/rink landmarks retain a readable screen-space caption at overview. */
2327
2575
  this.primaryFocalLabels = /* @__PURE__ */ new Map();
2328
2576
  /** GA paint and text share price/highlight filter state. */
@@ -2336,6 +2584,12 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2336
2584
  this.effSelection = DEF_SELECTION;
2337
2585
  /** Colorblind-safe mode (Okabe-Ito hues + hollow booked seats). */
2338
2586
  this.colorblind = false;
2587
+ /** Stable per-category-KEY colorblind hue (OV-46) — order-independent. */
2588
+ this.cbHueByKey = /* @__PURE__ */ new Map();
2589
+ /** OV-45(a): authored row-letter labels for the buyer/preview map, resolved in
2590
+ * world space (matching seat labels) and honouring each row's labelPresentation
2591
+ * (position/rotation/visibility/style). Painted at the seats LOD rung. */
2592
+ this.rowLabelPlan = [];
2339
2593
  /** Category order from the doc — the stable index into the CB palette. */
2340
2594
  this.catOrder = [];
2341
2595
  this.selection = /* @__PURE__ */ new Set();
@@ -2705,6 +2959,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2705
2959
  this.accessRingById.clear();
2706
2960
  this.accessGlyphById.clear();
2707
2961
  this.freeTextById.clear();
2962
+ this.iconNodeById.clear();
2708
2963
  this.primaryFocalLabels.clear();
2709
2964
  this.gaById.clear();
2710
2965
  for (const marker of this.selectionMarkers.values()) marker.destroy();
@@ -2759,13 +3014,22 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2759
3014
  this.catColor.clear();
2760
3015
  this.catPrice.clear();
2761
3016
  this.catOrder = doc.categories.map((c) => c.key);
3017
+ this.cbHueByKey.clear();
3018
+ [...this.catOrder].sort().forEach((key, i) => {
3019
+ this.cbHueByKey.set(key, CB_PALETTE[i % CB_PALETTE.length]);
3020
+ });
2762
3021
  for (const c of doc.categories) {
2763
3022
  this.catColor.set(c.key, c.color);
2764
3023
  if (typeof c.price === "number") this.catPrice.set(c.key, c.price);
2765
3024
  }
2766
3025
  for (const obj of view.objects) {
2767
3026
  if (obj.type === "booth") {
2768
- this.boothDims.set(obj.id, { width: obj.width, height: obj.height, rotation: obj.rotation });
3027
+ this.boothDims.set(obj.id, {
3028
+ width: obj.width,
3029
+ height: obj.height,
3030
+ rotation: obj.rotation,
3031
+ ...obj.points && obj.points.length >= 3 ? { points: obj.points.map((p) => ({ x: p.x, y: p.y })) } : {}
3032
+ });
2769
3033
  }
2770
3034
  }
2771
3035
  this.seats = expandChart(view, {
@@ -2785,6 +3049,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2785
3049
  this.unsectionedSeatGroup = new Group({ listening: true });
2786
3050
  this.seatLayer.add(this.unsectionedSeatGroup);
2787
3051
  this.renderSeats();
3052
+ this.buildRowLabelPlan(view);
2788
3053
  this.overlayLayer.add(this.labelGroup);
2789
3054
  this.overlayLayer.add(this.fgDecorGroup);
2790
3055
  this.overlayLayer.add(this.hoverRing);
@@ -4084,7 +4349,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4084
4349
  this.accessRingById.set(seat.id, ring);
4085
4350
  target.add(ring);
4086
4351
  }
4087
- if (seat.accessibility?.includes("wheelchair")) {
4352
+ if (accessGlyphPath(seat.accessibility ?? [])) {
4088
4353
  const glyph = this.buildAccessGlyph(seat, typeof c.fill() === "string" ? c.fill() : "");
4089
4354
  this.accessGlyphById.set(seat.id, glyph);
4090
4355
  target.add(glyph);
@@ -4096,25 +4361,42 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4096
4361
  renderBoothUnit(seat) {
4097
4362
  const target = this.seatContainer(seat.id);
4098
4363
  const dims = this.boothDims.get(seat.rowId) ?? { width: 40, height: 30, rotation: 0 };
4099
- const rect = new Rect({
4100
- x: seat.x,
4101
- y: seat.y,
4102
- width: dims.width,
4103
- height: dims.height,
4104
- offsetX: dims.width / 2,
4105
- offsetY: dims.height / 2,
4106
- rotation: dims.rotation,
4107
- cornerRadius: 4,
4108
- perfectDrawEnabled: false,
4109
- shadowForStrokeEnabled: false,
4110
- hitStrokeWidth: 0
4111
- });
4112
- rect.setAttr("seatId", seat.id);
4113
- this.circleById.set(seat.id, rect);
4114
- target.add(rect);
4364
+ let labelX = seat.x;
4365
+ let labelY = seat.y;
4366
+ let node;
4367
+ if (dims.points && dims.points.length >= 3) {
4368
+ node = new Line({
4369
+ x: seat.x,
4370
+ y: seat.y,
4371
+ points: dims.points.flatMap((p) => [p.x, p.y]),
4372
+ closed: true,
4373
+ perfectDrawEnabled: false,
4374
+ shadowForStrokeEnabled: false,
4375
+ hitStrokeWidth: 0
4376
+ });
4377
+ labelX = seat.x + dims.points.reduce((a, p) => a + p.x, 0) / dims.points.length;
4378
+ labelY = seat.y + dims.points.reduce((a, p) => a + p.y, 0) / dims.points.length;
4379
+ } else {
4380
+ node = new Rect({
4381
+ x: seat.x,
4382
+ y: seat.y,
4383
+ width: dims.width,
4384
+ height: dims.height,
4385
+ offsetX: dims.width / 2,
4386
+ offsetY: dims.height / 2,
4387
+ rotation: dims.rotation,
4388
+ cornerRadius: 4,
4389
+ perfectDrawEnabled: false,
4390
+ shadowForStrokeEnabled: false,
4391
+ hitStrokeWidth: 0
4392
+ });
4393
+ }
4394
+ node.setAttr("seatId", seat.id);
4395
+ this.circleById.set(seat.id, node);
4396
+ target.add(node);
4115
4397
  const t2 = new Text({
4116
- x: seat.x,
4117
- y: seat.y,
4398
+ x: labelX,
4399
+ y: labelY,
4118
4400
  text: seat.displayLabel ?? seat.label,
4119
4401
  fontSize: BOOTH_LABEL_FONT_SIZE,
4120
4402
  fontStyle: "600",
@@ -4129,7 +4411,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4129
4411
  this.hasBoothText = true;
4130
4412
  this.boothLabelById.set(seat.id, t2);
4131
4413
  target.add(t2);
4132
- this.paintSeat(rect, seat.id);
4414
+ this.paintSeat(node, seat.id);
4133
4415
  }
4134
4416
  /**
4135
4417
  * Build the centred accessibility glyph for a seat. Sized relative to the seat
@@ -4141,7 +4423,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4141
4423
  return new Path({
4142
4424
  x: seat.x,
4143
4425
  y: seat.y,
4144
- data: ACCESS_GLYPH_PATH,
4426
+ // Non-null: buildAccessGlyph is only called for seats the gate confirmed
4427
+ // resolve to a primary-accommodation glyph.
4428
+ data: accessGlyphPath(seat.accessibility ?? []) ?? "",
4145
4429
  offsetX: ACCESS_GLYPH_VIEWBOX / 2,
4146
4430
  offsetY: ACCESS_GLYPH_VIEWBOX / 2,
4147
4431
  scaleX: k,
@@ -4197,8 +4481,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4197
4481
  /** The category's display color — Okabe-Ito hue when colorblind-safe is on. */
4198
4482
  seatBaseColor(categoryKey) {
4199
4483
  if (!this.colorblind) return this.catColor.get(categoryKey) ?? "#6e7bff";
4200
- const idx = this.catOrder.indexOf(categoryKey);
4201
- return CB_PALETTE[(idx >= 0 ? idx : 0) % CB_PALETTE.length];
4484
+ return this.cbHueByKey.get(categoryKey) ?? CB_PALETTE[0];
4202
4485
  }
4203
4486
  /** Per-seat authored label size relative to the neutral default (1 = default),
4204
4487
  * mirroring the section convention (labelStyle.size ?? 18) / 18. Only real
@@ -4669,6 +4952,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4669
4952
  renderText(obj) {
4670
4953
  const background = this.canvasBackground;
4671
4954
  const preferredInk = obj.color ?? this.theme.textColor ?? DEF_TEXT;
4955
+ const iconPath = obj.semanticKind === "icon" ? venueIconPath(obj.iconKey) : void 0;
4956
+ if (iconPath) {
4957
+ const scale = obj.fontSize / VENUE_ICON_VIEWBOX;
4958
+ const iconNode = new Path({
4959
+ x: obj.position.x,
4960
+ y: obj.position.y,
4961
+ data: iconPath,
4962
+ rotation: obj.rotation,
4963
+ scaleX: scale,
4964
+ scaleY: scale,
4965
+ stroke: stateAwareBookableLabelInk(background, preferredInk),
4966
+ strokeWidth: VENUE_ICON_STROKE,
4967
+ lineCap: "round",
4968
+ lineJoin: "round",
4969
+ fillEnabled: false,
4970
+ listening: false,
4971
+ perfectDrawEnabled: false
4972
+ });
4973
+ this.iconNodeById.set(obj.id, { node: iconNode, fontSize: obj.fontSize });
4974
+ this.bgLayer.add(iconNode);
4975
+ return;
4976
+ }
4672
4977
  const node = new Text({
4673
4978
  x: obj.position.x,
4674
4979
  y: obj.position.y,
@@ -4836,7 +5141,8 @@ var _SeatmapRenderer = class _SeatmapRenderer {
4836
5141
  fill: color,
4837
5142
  opacity: GA_FILL_OPACITY,
4838
5143
  stroke: color,
4839
- strokeWidth: 1.5
5144
+ strokeWidth: 1.5,
5145
+ cornerRadius: obj.cornerRadius
4840
5146
  });
4841
5147
  poly.setAttr("gaId", obj.id);
4842
5148
  poly.on("click tap", (e) => {
@@ -5048,20 +5354,33 @@ var _SeatmapRenderer = class _SeatmapRenderer {
5048
5354
  sec.outlinePoly.shadowBlur(4 + raw * 12);
5049
5355
  sec.outlinePoly.shadowOpacity(0.25 + raw * 0.45);
5050
5356
  }
5051
- /** Recompute a section's neutral overview state and retained detail count. */
5357
+ /** Recompute a section's availability shading + retained availability text. */
5052
5358
  refreshSectionFill(sec) {
5053
5359
  sec.blockPoly.fill(this.sectionBlockFill(sec));
5054
- sec.subLabel.text(t("map.seatsLeft", { count: sec.free }));
5360
+ sec.subLabel.text(
5361
+ this.sectionAvailabilityState(sec) === "sold-out" ? t("map.soldOut") : t("map.seatsLeft", { count: sec.free })
5362
+ );
5055
5363
  sec.subLabel.offsetX(sec.subLabel.width() / 2);
5056
5364
  }
5057
5365
  /** True when a section/zone is currently in the `closed` event-state. */
5058
5366
  isSectionClosed(sec) {
5059
5367
  return this.closedSections.has(sec.id) || this.closedSections.has(sec.logicalId) || sec.zone != null && this.closedSections.has(sec.zone);
5060
5368
  }
5061
- /** Clean overview shells never leak category, price, or live availability paint. */
5369
+ /** Resolve the four-bucket overview availability state for a section (OV-69).
5370
+ * `closed` (operator) outranks live availability; the availability buckets
5371
+ * only apply to seated sections (GA-only shells have no seat capacity here). */
5372
+ sectionAvailabilityState(sec) {
5373
+ if (this.isSectionClosed(sec)) return "closed";
5374
+ if (sec.total <= 0) return "normal";
5375
+ if (sec.free <= 0) return "sold-out";
5376
+ if (sec.free <= sec.total * SECTION_NEARLY_GONE_RATIO) return "nearly-gone";
5377
+ return "normal";
5378
+ }
5379
+ /** Overview shells stay neutral except for the bounded availability shading
5380
+ * (nearly-gone / sold-out) and the operator `closed` slab — never category
5381
+ * or price paint, which is deferred to section focus/seat detail. */
5062
5382
  sectionBlockFill(sec) {
5063
- const fill = overviewPalette(this.canvasBackground).sectionFill;
5064
- return this.isSectionClosed(sec) ? darken(fill, 0.12) : fill;
5383
+ return overviewStateFill(overviewPalette(this.canvasBackground), this.sectionAvailabilityState(sec));
5065
5384
  }
5066
5385
  /**
5067
5386
  * Zone rung: one giant screen-constant label per zone (+ optional "FROM $n"),
@@ -6067,10 +6386,9 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6067
6386
  };
6068
6387
  });
6069
6388
  const palette = overviewPalette(this.canvasBackground);
6070
- const neutralSectionFills = /* @__PURE__ */ new Set([
6071
- palette.sectionFill.toLowerCase(),
6072
- darken(palette.sectionFill, 0.12).toLowerCase()
6073
- ]);
6389
+ const neutralSectionFills = new Set(
6390
+ ["normal", "nearly-gone", "sold-out", "closed"].map((state) => overviewStateFill(palette, state).toLowerCase())
6391
+ );
6074
6392
  const visibleSectionShells = this.sections.filter((section) => section.blockPoly.opacity() > 0.05);
6075
6393
  return {
6076
6394
  viewport,
@@ -6240,7 +6558,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6240
6558
  if (shouldCache && !this.cached) {
6241
6559
  this.cacheSeatLayer();
6242
6560
  } else if (!shouldCache && (this.cached || !this.seatLayer.listening())) {
6243
- if (this.cached) this.seatLayer.clearCache();
6561
+ if (this.cached) this.releaseSeatLayerBitmap();
6244
6562
  this.seatLayer.listening(true);
6245
6563
  this.cached = false;
6246
6564
  this.seatLayer.batchDraw();
@@ -6260,6 +6578,28 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6260
6578
  this.rebuildSeatCache();
6261
6579
  this.seatLayer.batchDraw();
6262
6580
  }
6581
+ /**
6582
+ * Drop the seat-layer overview bitmap while PRESERVING every descendant's
6583
+ * cached local transform + clientRect.
6584
+ *
6585
+ * Konva's public `Layer.clearCache()` releases the bitmap and then runs a deep
6586
+ * `_clearSelfAndDescendantCache()` that clears those caches on all 14k seat
6587
+ * nodes. Recomputing them on the single frame the melt first reveals a section's
6588
+ * live seats is the ~480ms first-drill stall (OV-80). A camera glide only marks
6589
+ * each seat's ABSOLUTE_TRANSFORM dirty (recomputed lazily at ordinary pan cost)
6590
+ * and never moves a seat's LOCAL transform, so the local caches stay valid —
6591
+ * keeping them lets the first melt paint at pan cost, matching the ~17ms of
6592
+ * every subsequent glide on the same section. Any edit that actually moves a
6593
+ * seat invalidates its own transform cache through the attr setter, so only
6594
+ * still-valid caches survive here.
6595
+ */
6596
+ releaseSeatLayerBitmap() {
6597
+ const layer = this.seatLayer;
6598
+ const entry = layer._cache.get("canvas");
6599
+ if (!entry) return;
6600
+ Konva.Util.releaseCanvas(entry.scene._canvas, entry.filter._canvas, entry.hit._canvas);
6601
+ layer._cache.delete("canvas");
6602
+ }
6263
6603
  /**
6264
6604
  * SYNCHRONOUS repaint that bypasses requestAnimationFrame — see the
6265
6605
  * ISeatmapRenderer.forceDraw() contract. Chrome pauses rAF (and therefore
@@ -6278,7 +6618,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6278
6618
  if (this.effScale() < CACHE_THRESHOLD) {
6279
6619
  this.rebuildSeatCache();
6280
6620
  } else if (this.cached) {
6281
- this.seatLayer.clearCache();
6621
+ this.releaseSeatLayerBitmap();
6282
6622
  this.seatLayer.listening(true);
6283
6623
  this.cached = false;
6284
6624
  }
@@ -6294,6 +6634,63 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6294
6634
  const gaOverviewHidden = objectId != null && (kind === "ga-label" || kind === "ga-capacity") && this.gaById.get(objectId)?.sectionId != null && effectiveScale < CACHE_THRESHOLD;
6295
6635
  node.visible(!gaDimmed && !gaOverviewHidden && isBookableLabelLegibleAtScale(node.fontSize(), effectiveScale));
6296
6636
  }
6637
+ for (const { node, fontSize } of this.iconNodeById.values()) {
6638
+ node.visible(isBookableLabelLegibleAtScale(fontSize, effectiveScale));
6639
+ }
6640
+ }
6641
+ /** OV-45(a): resolve authored row-letter labels for the buyer/preview map, in
6642
+ * world space, matching the Designer's placement contract (start/end/both/none
6643
+ * preset, a free-drag `position` that wins over the preset, rotation, and
6644
+ * labelStyle colour/size fed through the same auto-contrast guard). Segmented
6645
+ * rows keep their own logical-label handling and are resolved by their owner. */
6646
+ buildRowLabelPlan(view) {
6647
+ this.rowLabelPlan = [];
6648
+ const background = this.theme.background ?? "#0e1117";
6649
+ const seatsByRow = /* @__PURE__ */ new Map();
6650
+ for (const seat of this.seats) {
6651
+ if (seat.kind === "booth") continue;
6652
+ const key = seat.logicalRowId ?? seat.rowId;
6653
+ (seatsByRow.get(key) ?? seatsByRow.set(key, []).get(key)).push(seat);
6654
+ }
6655
+ const seatOrder = (seat) => seat.logicalSeatIndex ?? Number(seat.id.split(":").pop() ?? 0);
6656
+ for (const obj of view.objects) {
6657
+ if (obj.type !== "row") continue;
6658
+ if (obj.segmentedRow && obj.segmentedRow.componentIndex !== 0) continue;
6659
+ const presentation = obj.segmentedRow?.labelPresentation ?? obj.labelPresentation;
6660
+ if (presentation?.visible === false || presentation?.positionPreset === "none") continue;
6661
+ const key = obj.segmentedRow ? obj.segmentedRow.groupId : obj.id;
6662
+ const seats = (seatsByRow.get(key) ?? []).slice().sort((a, b) => seatOrder(a) - seatOrder(b));
6663
+ if (!seats.length) continue;
6664
+ const first = seats[0];
6665
+ const last = seats[seats.length - 1];
6666
+ const labelStyle = presentation?.labelStyle;
6667
+ const ink = stateAwareBookableLabelInk(
6668
+ background,
6669
+ labelStyle?.color ?? this.theme.rowLabelColor ?? this.theme.textColor ?? "#e6e9f0"
6670
+ );
6671
+ const fontSize = labelStyle?.size ?? 12;
6672
+ const rotation = presentation?.rotation ?? 0;
6673
+ const text = obj.segmentedRow?.displayLabel ?? obj.displayLabel ?? obj.label;
6674
+ const opacity = this.objectFilteredOut(obj) ? 0.15 : 1;
6675
+ const push = (x, y) => this.rowLabelPlan.push({ text, x, y, rotation, fontSize, ink, opacity });
6676
+ if (presentation?.position) {
6677
+ push(presentation.position.x, presentation.position.y);
6678
+ continue;
6679
+ }
6680
+ const radians = obj.rotation * Math.PI / 180;
6681
+ const along = { x: Math.cos(radians), y: Math.sin(radians) };
6682
+ const gap = this.seatR + 12;
6683
+ const preset = presentation?.positionPreset ?? "start";
6684
+ if (preset === "start" || preset === "both") push(first.x - along.x * gap, first.y - along.y * gap);
6685
+ if (preset === "end" || preset === "both") push(last.x + along.x * gap, last.y + along.y * gap);
6686
+ }
6687
+ }
6688
+ /** Row labels never carry status; the buyer just dims them under the same
6689
+ * category/price filters that dim their seats. */
6690
+ objectFilteredOut(obj) {
6691
+ const key = obj.categoryKey;
6692
+ if (!key) return false;
6693
+ return Boolean(this.categoryHighlight && key !== this.categoryHighlight || this.categoryFilter && !this.categoryFilter.has(key));
6297
6694
  }
6298
6695
  updateLabels() {
6299
6696
  const effectiveScale = this.effScale();
@@ -6387,6 +6784,30 @@ var _SeatmapRenderer = class _SeatmapRenderer {
6387
6784
  this.seatLabelById.set(seat.id, t2);
6388
6785
  if (++count >= MAX_LABELS) break;
6389
6786
  }
6787
+ for (const rl of this.rowLabelPlan) {
6788
+ const screen = this.worldToScreen({ x: rl.x, y: rl.y });
6789
+ if (screen.x < -40 || screen.x > this.stage.width() + 40 || screen.y < -40 || screen.y > this.stage.height() + 40) continue;
6790
+ const t2 = new Text({
6791
+ x: rl.x,
6792
+ y: rl.y,
6793
+ text: rl.text,
6794
+ fontSize: rl.fontSize,
6795
+ fontStyle: "700",
6796
+ fontFamily: this.labelFont(),
6797
+ fill: rl.ink,
6798
+ rotation: rl.rotation,
6799
+ opacity: rl.opacity,
6800
+ shadowColor: "#05070c",
6801
+ shadowBlur: rl.ink.toLowerCase() === "#ffffff" ? 5 : 0,
6802
+ shadowOpacity: rl.ink.toLowerCase() === "#ffffff" ? 0.8 : 0,
6803
+ shadowForStrokeEnabled: false,
6804
+ listening: false,
6805
+ perfectDrawEnabled: false
6806
+ });
6807
+ t2.offsetX(t2.width() / 2);
6808
+ t2.offsetY(t2.height() / 2);
6809
+ this.labelGroup.add(t2);
6810
+ }
6390
6811
  if (this.isoT > 0 && this.viewMode !== "perspective") this.applyUprightLabels();
6391
6812
  this.overlayLayer.batchDraw();
6392
6813
  }
@@ -6718,6 +7139,36 @@ var PickerController = class {
6718
7139
  const group = this.groupedTableBySeatId.get(seatIdOrLabel) ?? this.groupedTablesByLabel.get(seatIdOrLabel);
6719
7140
  return group ? this.toTableSeat(group) : null;
6720
7141
  }
7142
+ /**
7143
+ * Buyer-facing name + type word for a checkout line item, resolved from the
7144
+ * SAME author overrides the tray/tooltip already render. `label` stays the
7145
+ * booking identity; a host builds its own order summary from `displayLabel`/
7146
+ * `displayType` so the buyer sees the designer's copy, not the internal label.
7147
+ * Absent fields = no override (host falls back to `label`).
7148
+ */
7149
+ lineItemDisplay(item) {
7150
+ if (item.objectType === "ga") {
7151
+ const doc = this.visibleDoc();
7152
+ const area = gaAreasOf(doc).find((candidate) => candidate.id === item.objectId);
7153
+ return {
7154
+ ...area?.displayLabel ? { displayLabel: area.displayLabel } : {},
7155
+ ...area?.displayType ? { displayType: area.displayType } : {}
7156
+ };
7157
+ }
7158
+ if (item.objectType === "table") {
7159
+ const group = this.groupedTablesByLabel.get(item.label);
7160
+ return {
7161
+ ...group?.displayLabel && group.displayLabel !== group.label ? { displayLabel: group.displayLabel } : {},
7162
+ ...group?.displayType ? { displayType: group.displayType } : {}
7163
+ };
7164
+ }
7165
+ const seat = this.labelToSeat.get(item.label);
7166
+ const context = seat ? this.seatContext.get(seat.id) : void 0;
7167
+ return {
7168
+ ...seat?.displayLabel ? { displayLabel: seat.displayLabel } : {},
7169
+ ...context?.displayType ? { displayType: context.displayType } : {}
7170
+ };
7171
+ }
6721
7172
  /** Fetch chart, build label maps, mount the renderer, seed statuses, go live. */
6722
7173
  async render(host) {
6723
7174
  if (this.renderer) return null;
@@ -7633,7 +8084,7 @@ var PickerController = class {
7633
8084
  const color = sec.color ?? zone?.color ?? categories[0]?.color ?? "#6e7bff";
7634
8085
  return {
7635
8086
  id,
7636
- label: sec.label,
8087
+ label: sec.displayLabel ?? sec.label,
7637
8088
  zoneLabel: zone?.label ?? "",
7638
8089
  ...sec.entrance && sec.entrance.trim() ? { entrance: sec.entrance.trim() } : {},
7639
8090
  color,
@@ -8235,9 +8686,9 @@ function generateSeatThumb(seat, focalPoint, _neighborSeats) {
8235
8686
 
8236
8687
  // src/i18n/bundles.ts
8237
8688
  var LOADERS = {
8238
- es: () => import("./es-SKAODLTR.js").then((m) => ({ default: m.es })),
8239
- de: () => import("./de-73TIXYJH.js").then((m) => ({ default: m.de })),
8240
- fr: () => import("./fr-J4637T6V.js").then((m) => ({ default: m.fr }))
8689
+ es: () => import("./es-6TJS2L7S.js").then((m) => ({ default: m.es })),
8690
+ de: () => import("./de-UHAOVDS3.js").then((m) => ({ default: m.de })),
8691
+ fr: () => import("./fr-MMSY7FPE.js").then((m) => ({ default: m.fr }))
8241
8692
  };
8242
8693
  var loaded = /* @__PURE__ */ new Set(["en"]);
8243
8694
  async function loadLocale(code) {