venuekit 0.10.3 → 0.11.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/react.js CHANGED
@@ -38,6 +38,8 @@ import {
38
38
  summarize,
39
39
  tableSize,
40
40
  toggleRemovedSeat,
41
+ toggleReservedSeat,
42
+ toggleReservedTable,
41
43
  toggleUnit,
42
44
  unitStates,
43
45
  zoneBounds,
@@ -45,7 +47,7 @@ import {
45
47
  zoneLocalSize,
46
48
  zoneOutline,
47
49
  zoneWithLocalSize
48
- } from "./chunk-TDC5G7L7.js";
50
+ } from "./chunk-IRNVYNYV.js";
49
51
 
50
52
  // ../canvas/src/camera.ts
51
53
  var MIN_ZOOM = 0.05;
@@ -199,7 +201,12 @@ var defaultPalette = {
199
201
  unitLabel: "#0b0b0d",
200
202
  landmark: "#23232a",
201
203
  landmarkStroke: "#34343c",
202
- landmarkLabel: "#d6d6dc"
204
+ landmarkLabel: "#d6d6dc",
205
+ courtesy: "#7c3aed",
206
+ courtesyLabel: "#ffffff",
207
+ heldFor: "#f59e0b",
208
+ heldForLabel: "#0b0b0d",
209
+ statusMark: "#ffffff"
203
210
  };
204
211
  var PALETTE_VARIABLES = {
205
212
  taken: "--vk-taken",
@@ -219,7 +226,12 @@ var PALETTE_VARIABLES = {
219
226
  unitLabel: "--vk-unit-fg",
220
227
  landmark: "--vk-landmark",
221
228
  landmarkStroke: "--vk-landmark-stroke",
222
- landmarkLabel: "--vk-landmark-fg"
229
+ landmarkLabel: "--vk-landmark-fg",
230
+ courtesy: "--vk-courtesy",
231
+ courtesyLabel: "--vk-courtesy-fg",
232
+ heldFor: "--vk-held-for",
233
+ heldForLabel: "--vk-held-for-fg",
234
+ statusMark: "--vk-status-mark"
223
235
  };
224
236
  function paletteFromVariables(read, base = defaultPalette) {
225
237
  const out = { ...base };
@@ -346,6 +358,7 @@ function unitRadii(layout) {
346
358
  }
347
359
  return out;
348
360
  }
361
+ var RESERVED_DASH_PX = 3;
349
362
  var MIN_NUMBERED_RADIUS_PX = 8;
350
363
  var LABEL_INSET_PX = 12;
351
364
  var TWO_LINES_PX = 40;
@@ -529,7 +542,9 @@ function buildScene(input) {
529
542
  const maxWidth = (body.maxX - body.minX) * camera.zoom - 4;
530
543
  if (maxWidth < MIN_NUMBERED_RADIUS_PX * 2) continue;
531
544
  const ref = refOf(el);
532
- const color = el.mode === "whole" ? dim(ref) ? palette.soldLabel : mapUnitPaint(states.get(el.id) ?? "free", "", palette).label : palette.landmarkLabel;
545
+ const state = states.get(el.id) ?? "free";
546
+ const mark = state !== "selected" && state !== "held" ? input.unitMarks?.get(el.id) : void 0;
547
+ const color = el.mode === "whole" ? dim(ref) ? palette.soldLabel : (mark ? markPaint(mark, palette) : mapUnitPaint(state, "", palette)).label : palette.landmarkLabel;
533
548
  labels.push({ x, y, text: el.label, size: 13, weight: 700, color, align: "centre", maxWidth });
534
549
  }
535
550
  const elementsById = new Map(layout.elements.map((el) => [el.id, el]));
@@ -539,8 +554,10 @@ function buildScene(input) {
539
554
  visibleUnitIds.push(u.id);
540
555
  const color = colorOf(u.placeTypeRef) ?? palette.unassigned;
541
556
  const state = mode === "designer" && input.locked?.has(u.id) ? "taken" : states.get(u.id) ?? "free";
542
- const mapPaint = mode === "map" ? mapUnitPaint(state, color, palette) : null;
543
- const paint2 = mapPaint ? { fill: mapPaint.fill, stroke: mapPaint.stroke, strokeWidth: mapPaint.strokeWidth } : unitPaint(state, color, palette);
557
+ const mark = mode === "map" && state !== "selected" && state !== "held" ? input.unitMarks?.get(u.id) : void 0;
558
+ const mapPaint = mode === "map" ? mark ? markPaint(mark, palette) : mapUnitPaint(state, color, palette) : null;
559
+ const base = mapPaint ? { fill: mapPaint.fill, stroke: mapPaint.stroke, strokeWidth: mapPaint.strokeWidth } : unitPaint(state, color, palette);
560
+ const paint2 = u.inviteOnly && state === "free" && !mark ? { ...base, stroke: palette.label, strokeWidth: 2, dash: RESERVED_DASH_PX } : base;
544
561
  const dimmed = dim(u.placeTypeRef);
545
562
  const alpha = dimmed ? { alpha: DIMMED_ALPHA } : {};
546
563
  const el = elementsById.get(u.elementId);
@@ -553,12 +570,30 @@ function buildScene(input) {
553
570
  if (el?.type === "table" && el.mode === "whole" && el.shape === "rect") {
554
571
  const b = tableBodyBox(el);
555
572
  shapes.push({ kind: "rect", x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY, ...paint2, ...alpha });
573
+ if (mark) shapes.push(...markBadge(mark, b.maxX - radius * 0.25, b.minY + radius * 0.25, radius, palette));
556
574
  continue;
557
575
  }
558
576
  shapes.push({ kind: "circle", x: u.x, y: u.y, radius, ...paint2, ...alpha });
577
+ if (mark) shapes.push(...markBadge(mark, u.x + radius * 0.65, u.y - radius * 0.65, radius, palette));
559
578
  }
560
579
  return { background, shapes, labels, visibleUnitIds };
561
580
  }
581
+ function markPaint(mark, palette) {
582
+ switch (mark) {
583
+ case "sold":
584
+ return mapUnitPaint("taken", "", palette);
585
+ case "courtesy":
586
+ return { fill: palette.courtesy, stroke: palette.soldStroke, strokeWidth: 1, label: palette.courtesyLabel };
587
+ case "held":
588
+ return { fill: palette.heldFor, stroke: palette.soldStroke, strokeWidth: 1, label: palette.heldForLabel };
589
+ }
590
+ }
591
+ function markBadge(mark, x, y, radius, palette) {
592
+ if (mark === "sold") return [];
593
+ const r = radius * 0.4;
594
+ if (mark === "courtesy") return [{ kind: "circle", x, y, radius: r, fill: palette.statusMark, stroke: palette.soldStroke, strokeWidth: 1 }];
595
+ return [{ kind: "rect", x: x - r, y: y - r, width: r * 2, height: r * 2, fill: palette.statusMark, stroke: palette.soldStroke, strokeWidth: 1 }];
596
+ }
562
597
 
563
598
  // ../canvas/src/label-fit.ts
564
599
  var MIN_LEGIBLE_PX = 11;
@@ -646,7 +681,10 @@ function drawShape(ctx, shape, zoom) {
646
681
  shape.points.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
647
682
  ctx.closePath();
648
683
  } else ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2);
684
+ const dash = shape.kind !== "polygon" ? shape.dash : void 0;
685
+ if (dash) ctx.setLineDash([dash / zoom, dash / zoom]);
649
686
  paint(ctx, shape.fill, shape.stroke, shape.strokeWidth, zoom);
687
+ if (dash) ctx.setLineDash([]);
650
688
  ctx.globalAlpha = previous;
651
689
  }
652
690
  function renderScene(scene, target) {
@@ -806,12 +844,12 @@ var defaultA11yLabels = {
806
844
  row: "fila",
807
845
  seat: "asiento"
808
846
  };
809
- function buildA11yNodes(units, states, placeTypeNames, labels = defaultA11yLabels) {
847
+ function buildA11yNodes(units, states, placeTypeNames, labels = defaultA11yLabels, stateWords) {
810
848
  return units.map((u) => {
811
849
  const state = states.get(u.id) ?? "free";
812
850
  const where = u.row !== void 0 && u.seat !== void 0 ? `${labels.row} ${u.row}, ${labels.seat} ${u.seat}` : u.label;
813
851
  const typeName = u.placeTypeRef === null ? void 0 : placeTypeNames.get(u.placeTypeRef);
814
- const name = [typeName, where, labels[state]].filter(Boolean).join(", ");
852
+ const name = [typeName, where, stateWords?.get(u.id) ?? labels[state]].filter(Boolean).join(", ");
815
853
  return {
816
854
  id: u.id,
817
855
  name,
@@ -1345,7 +1383,18 @@ var es = {
1345
1383
  backToMap: "Todo el mapa",
1346
1384
  focusHintSeats: "Toc\xE1 las butacas que quer\xE9s.",
1347
1385
  focusHintPlaces: "Toc\xE1 los lugares que quer\xE9s en cada mesa.",
1348
- focusHintTables: "Toc\xE1 las mesas que quer\xE9s."
1386
+ focusHintTables: "Toc\xE1 las mesas que quer\xE9s.",
1387
+ notForSale: "no est\xE1 a la venta",
1388
+ inviteOnlySeat: "Esa butaca no est\xE1 a la venta",
1389
+ inviteOnlyTable: "Esa mesa no est\xE1 a la venta",
1390
+ inviteOnlyPlace: "Ese lugar no est\xE1 a la venta",
1391
+ inviteOnly: "s\xF3lo con invitaci\xF3n",
1392
+ legendInviteOnly: "S\xF3lo con invitaci\xF3n",
1393
+ statusSold: "vendida",
1394
+ statusCourtesy: "regalada",
1395
+ statusHeld: "apartada",
1396
+ legendCourtesy: "Regalada",
1397
+ legendHeld: "Apartada"
1349
1398
  };
1350
1399
  var en = {
1351
1400
  free: "available",
@@ -1390,7 +1439,18 @@ var en = {
1390
1439
  backToMap: "Whole map",
1391
1440
  focusHintSeats: "Tap the seats you want.",
1392
1441
  focusHintPlaces: "Tap the places you want at each table.",
1393
- focusHintTables: "Tap the tables you want."
1442
+ focusHintTables: "Tap the tables you want.",
1443
+ notForSale: "not for sale",
1444
+ inviteOnlySeat: "That seat is not for sale",
1445
+ inviteOnlyTable: "That table is not for sale",
1446
+ inviteOnlyPlace: "That place is not for sale",
1447
+ inviteOnly: "invitation only",
1448
+ legendInviteOnly: "Invitation only",
1449
+ statusSold: "sold",
1450
+ statusCourtesy: "complimentary",
1451
+ statusHeld: "held",
1452
+ legendCourtesy: "Complimentary",
1453
+ legendHeld: "Held"
1394
1454
  };
1395
1455
  function format(template, vars) {
1396
1456
  return template.replace(
@@ -1476,6 +1536,12 @@ var PICKER_CSS = `
1476
1536
  --vk-landmark:#23232a;
1477
1537
  --vk-landmark-stroke:#34343c;
1478
1538
  --vk-landmark-fg:#d6d6dc;
1539
+ /* Panel de la productora: regalada, apartada y la marca que las distingue sin color. */
1540
+ --vk-courtesy:#7c3aed;
1541
+ --vk-courtesy-fg:#ffffff;
1542
+ --vk-held-for:#f59e0b;
1543
+ --vk-held-for-fg:#0b0b0d;
1544
+ --vk-status-mark:#ffffff;
1479
1545
  }
1480
1546
  .vk-root{display:flex;flex-direction:column;gap:12px;font:var(--vk-font);
1481
1547
  color:var(--vk-fg);background:var(--vk-bg);position:relative;box-sizing:border-box;
@@ -1516,6 +1582,9 @@ var PICKER_CSS = `
1516
1582
  .vk-dot{width:14px;height:14px;border-radius:50%;flex:none;box-sizing:border-box}
1517
1583
  .vk-dot[data-state="selected"]{background:var(--vk-mine);border:3px solid var(--vk-accent)}
1518
1584
  .vk-dot[data-state="taken"]{background:var(--vk-sold);border:1px solid var(--vk-sold-stroke)}
1585
+ .vk-dot[data-state="courtesy"]{background:radial-gradient(circle at 70% 30%,var(--vk-status-mark,#fff) 0 2.5px,transparent 3px),var(--vk-courtesy,#7c3aed);border:1px solid var(--vk-sold-stroke)}
1586
+ .vk-dot[data-state="held-for"]{background:linear-gradient(var(--vk-status-mark,#fff),var(--vk-status-mark,#fff)) 80% 20%/5px 5px no-repeat,var(--vk-held-for,#f59e0b);border:1px solid var(--vk-sold-stroke)}
1587
+ .vk-dot[data-state="invite-only"]{border:2px dashed var(--vk-fg)}
1519
1588
  .vk-cta{border:1px solid var(--vk-accent);background:var(--vk-accent);color:var(--vk-accent-fg);
1520
1589
  border-radius:var(--vk-radius);padding:10px 16px;cursor:pointer;font-size:14px;font-weight:600;min-height:44px}
1521
1590
  .vk-cta:disabled{opacity:.5;cursor:default}
@@ -1560,6 +1629,38 @@ var PICKER_CSS = `
1560
1629
  .vk-type-btn[data-selected="true"]{border-color:var(--vk-accent)}
1561
1630
  `;
1562
1631
 
1632
+ // ../react/src/unit-status.ts
1633
+ var KINDS = ["sold", "courtesy", "held"];
1634
+ function unitMarks(status) {
1635
+ const out = /* @__PURE__ */ new Map();
1636
+ if (!status) return out;
1637
+ for (const [id, s] of Object.entries(status)) {
1638
+ if (s && KINDS.includes(s.kind)) out.set(id, s.kind);
1639
+ }
1640
+ return out;
1641
+ }
1642
+ function presentStatusKinds(marks, units) {
1643
+ const seen = /* @__PURE__ */ new Set();
1644
+ for (const u of units) {
1645
+ const k = marks.get(u.id);
1646
+ if (k) seen.add(k);
1647
+ }
1648
+ return KINDS.filter((k) => seen.has(k));
1649
+ }
1650
+ function statusWord(kind, m) {
1651
+ return kind === "sold" ? m.statusSold : kind === "courtesy" ? m.statusCourtesy : m.statusHeld;
1652
+ }
1653
+ function statusA11y(s, m) {
1654
+ const word = statusWord(s.kind, m);
1655
+ return s.label ? `${word}, ${s.label}` : word;
1656
+ }
1657
+ function statusAnnouncement(unit, s, m) {
1658
+ return `${unitName(unit, m)}: ${s.label || statusWord(s.kind, m)}`;
1659
+ }
1660
+ function blockedInviteOnly(units, allow) {
1661
+ return allow ? [] : units.filter((u) => u.inviteOnly).map((u) => u.id);
1662
+ }
1663
+
1563
1664
  // ../react/src/useImage.ts
1564
1665
  import { useEffect, useState } from "react";
1565
1666
  function useImage(src) {
@@ -1668,7 +1769,8 @@ function VenueCanvas(props) {
1668
1769
  unassignedLabel: messages.unassigned,
1669
1770
  ...props.dimmedRefs ? { dimmedRefs: props.dimmedRefs } : {},
1670
1771
  ...props.placeTypePrices ? { placeTypePrices: props.placeTypePrices } : {},
1671
- ...props.highlightRefs ? { highlightRefs: props.highlightRefs } : {}
1772
+ ...props.highlightRefs ? { highlightRefs: props.highlightRefs } : {},
1773
+ ...props.unitMarks ? { unitMarks: props.unitMarks } : {}
1672
1774
  }),
1673
1775
  [
1674
1776
  layout,
@@ -1683,6 +1785,7 @@ function VenueCanvas(props) {
1683
1785
  props.dimmedRefs,
1684
1786
  props.placeTypePrices,
1685
1787
  props.highlightRefs,
1788
+ props.unitMarks,
1686
1789
  messages.unassigned
1687
1790
  ]
1688
1791
  );
@@ -1776,7 +1879,8 @@ function VenueCanvas(props) {
1776
1879
  return;
1777
1880
  }
1778
1881
  const unit = unitById.get(unitId);
1779
- if (unit && props.focusRef !== null && unit.placeTypeRef === props.focusRef) {
1882
+ const choosable = unit && unit.placeTypeRef !== null && (props.focusRef !== null && unit.placeTypeRef === props.focusRef || !!props.selectableRefs?.has(unit.placeTypeRef));
1883
+ if (unit && choosable) {
1780
1884
  const closer = tapZoom(camera, viewport, result.tap, (props.radii.get(unit.elementId) ?? DEFAULT_UNIT_RADIUS) * camera.zoom);
1781
1885
  if (closer) {
1782
1886
  props.onCameraChange(closer);
@@ -1928,21 +2032,40 @@ function SeatMap(props) {
1928
2032
  const first = unitById.get(ids[0]);
1929
2033
  announce(byKind(first, messages.conflictSeat, messages.conflictTable, messages.conflictPlace), true);
1930
2034
  }, [props.conflictUnitIds, unitById, messages, announce]);
2035
+ const marks = useMemo2(() => unitMarks(props.unitStatus), [props.unitStatus]);
2036
+ const allowInviteOnly = props.allowInviteOnly === true;
2037
+ const blocked = useMemo2(() => new Set(blockedInviteOnly(units, allowInviteOnly)), [units, allowInviteOnly]);
1931
2038
  const taken = useMemo2(
1932
- () => /* @__PURE__ */ new Set([...props.takenUnitIds, ...conflicted]),
1933
- [props.takenUnitIds, conflicted]
2039
+ () => /* @__PURE__ */ new Set([...props.takenUnitIds, ...conflicted, ...marks.keys(), ...blocked]),
2040
+ [props.takenUnitIds, conflicted, marks, blocked]
1934
2041
  );
1935
2042
  const held = useMemo2(() => new Set(props.heldUnitIds ?? NONE), [props.heldUnitIds]);
1936
2043
  const states = useMemo2(() => unitStates(units, taken, selection, held), [units, taken, selection, held]);
1937
- const selectable = useMemo2(() => units.filter((u) => u.placeTypeRef === focusRef), [units, focusRef]);
2044
+ const multiType = props.multiType === true;
2045
+ const selectableRefs = useMemo2(() => multiType ? new Set(placeTypeNames.keys()) : void 0, [multiType, placeTypeNames]);
2046
+ const selectable = useMemo2(
2047
+ () => units.filter((u) => selectableRefs ? u.placeTypeRef !== null && selectableRefs.has(u.placeTypeRef) : u.placeTypeRef === focusRef),
2048
+ [units, focusRef, selectableRefs]
2049
+ );
2050
+ const stateWords = useMemo2(() => {
2051
+ const out = /* @__PURE__ */ new Map();
2052
+ for (const u of selectable) {
2053
+ const s = props.unitStatus?.[u.id];
2054
+ if (s && marks.has(u.id)) out.set(u.id, statusA11y(s, messages));
2055
+ else if (blocked.has(u.id)) out.set(u.id, messages.notForSale);
2056
+ else if (u.inviteOnly && states.get(u.id) === "free") out.set(u.id, `${messages.free}, ${messages.inviteOnly}`);
2057
+ }
2058
+ return out;
2059
+ }, [selectable, props.unitStatus, marks, blocked, states, messages]);
1938
2060
  const a11yNodes = useMemo2(
1939
2061
  () => buildA11yNodes(
1940
2062
  selectable.map((u) => u.kind === "seat" ? u : { ...u, label: a11yLabel(u, messages) }),
1941
2063
  states,
1942
2064
  placeTypeNames,
1943
- { free: messages.free, taken: messages.taken, selected: messages.selected, held: messages.held, row: messages.row, seat: messages.seat }
2065
+ { free: messages.free, taken: messages.taken, selected: messages.selected, held: messages.held, row: messages.row, seat: messages.seat },
2066
+ stateWords
1944
2067
  ),
1945
- [selectable, states, placeTypeNames, messages]
2068
+ [selectable, states, placeTypeNames, messages, stateWords]
1946
2069
  );
1947
2070
  const bg = layout.meta.background;
1948
2071
  const background = useImage(bg?.src ?? null);
@@ -1979,10 +2102,20 @@ function SeatMap(props) {
1979
2102
  const activateUnit = useCallback((unitId) => {
1980
2103
  const unit = unitById.get(unitId);
1981
2104
  if (!unit) return;
1982
- if (unit.placeTypeRef !== focusRef) {
2105
+ const ownType = unit.placeTypeRef === focusRef || !!selectableRefs && unit.placeTypeRef !== null && selectableRefs.has(unit.placeTypeRef);
2106
+ if (!ownType) {
1983
2107
  switchTo(unit.placeTypeRef);
1984
2108
  return;
1985
2109
  }
2110
+ const status = props.unitStatus?.[unitId];
2111
+ if (status && marks.has(unitId) && !selection.includes(unitId)) {
2112
+ announce(statusAnnouncement(unit, status, messages), true);
2113
+ return;
2114
+ }
2115
+ if (blocked.has(unitId) && !selection.includes(unitId)) {
2116
+ announce(byKind(unit, messages.inviteOnlySeat, messages.inviteOnlyTable, messages.inviteOnlyPlace), true);
2117
+ return;
2118
+ }
1986
2119
  if (states.get(unitId) === "held" && !selection.includes(unitId)) {
1987
2120
  announce(messages.heldSeat, true);
1988
2121
  return;
@@ -1999,13 +2132,29 @@ function SeatMap(props) {
1999
2132
  const added = result.selection.includes(unitId);
2000
2133
  announce(`${unitName(unit, messages)}: ${added ? messages.selected : messages.free}`);
2001
2134
  onSelectionChange(result.selection);
2002
- }, [unitById, focusRef, states, selection, maxSelection, messages, announce, onSelectionChange, switchTo]);
2135
+ }, [
2136
+ unitById,
2137
+ focusRef,
2138
+ selectableRefs,
2139
+ props.unitStatus,
2140
+ marks,
2141
+ blocked,
2142
+ states,
2143
+ selection,
2144
+ maxSelection,
2145
+ messages,
2146
+ announce,
2147
+ onSelectionChange,
2148
+ switchTo
2149
+ ]);
2003
2150
  const summary = useMemo2(() => selectionSummary(units, selection, messages), [units, selection, messages]);
2004
2151
  const focusColor = placeTypeColors.get(focusRef) ?? "var(--vk-unassigned)";
2005
2152
  const focusType = props.placeTypes.find((p) => p.ref === focusRef);
2006
2153
  const embedded = props.embedded === true;
2007
2154
  const kind = useMemo2(() => focusKind(units, focusRef), [units, focusRef]);
2008
2155
  const hint = kind === "seat" ? messages.focusHintSeats : kind === "place" ? messages.focusHintPlaces : messages.focusHintTables;
2156
+ const statusKinds = useMemo2(() => presentStatusKinds(marks, units), [marks, units]);
2157
+ const showInviteOnly = useMemo2(() => allowInviteOnly && units.some((u) => u.inviteOnly && !marks.has(u.id)), [allowInviteOnly, units, marks]);
2009
2158
  return /* @__PURE__ */ jsxs2(
2010
2159
  "div",
2011
2160
  {
@@ -2047,7 +2196,9 @@ function SeatMap(props) {
2047
2196
  onActivateUnit: activateUnit,
2048
2197
  messages,
2049
2198
  placeTypeColors,
2050
- focusRef,
2199
+ focusRef: multiType ? null : focusRef,
2200
+ ...selectableRefs ? { selectableRefs } : {},
2201
+ ...marks.size > 0 ? { unitMarks: marks } : {},
2051
2202
  onTapEmpty,
2052
2203
  placeTypePrices,
2053
2204
  ...props.height !== void 0 ? { height: props.height } : {},
@@ -2068,7 +2219,19 @@ function SeatMap(props) {
2068
2219
  /* @__PURE__ */ jsxs2("li", { children: [
2069
2220
  /* @__PURE__ */ jsx2("span", { className: "vk-dot", "data-state": "taken" }),
2070
2221
  messages.legendTaken
2071
- ] })
2222
+ ] }),
2223
+ statusKinds.includes("courtesy") ? /* @__PURE__ */ jsxs2("li", { children: [
2224
+ /* @__PURE__ */ jsx2("span", { className: "vk-dot", "data-state": "courtesy" }),
2225
+ messages.legendCourtesy
2226
+ ] }) : null,
2227
+ statusKinds.includes("held") ? /* @__PURE__ */ jsxs2("li", { children: [
2228
+ /* @__PURE__ */ jsx2("span", { className: "vk-dot", "data-state": "held-for" }),
2229
+ messages.legendHeld
2230
+ ] }) : null,
2231
+ showInviteOnly ? /* @__PURE__ */ jsxs2("li", { children: [
2232
+ /* @__PURE__ */ jsx2("span", { className: "vk-dot", "data-state": "invite-only", style: { background: focusColor } }),
2233
+ messages.legendInviteOnly
2234
+ ] }) : null
2072
2235
  ] }),
2073
2236
  announcement.blocking && announcement.text ? /* @__PURE__ */ jsx2("p", { className: "vk-notice", "data-testid": "notice", children: announcement.text }) : null,
2074
2237
  embedded ? null : /* @__PURE__ */ jsxs2("div", { className: "vk-bar", "data-testid": "summary-bar", children: [
@@ -2139,9 +2302,11 @@ function VenueOverview(props) {
2139
2302
  const highlightRefs = useMemo3(() => new Set(selectedRefs), [selectedRefs]);
2140
2303
  const takenIds = props.takenUnitIds ?? NONE2;
2141
2304
  const selectedIds = props.selectedUnitIds ?? NONE2;
2305
+ const marks = useMemo3(() => unitMarks(props.unitStatus), [props.unitStatus]);
2306
+ const allowInviteOnly = props.allowInviteOnly === true;
2142
2307
  const states = useMemo3(
2143
- () => unitStates(units, new Set(takenIds), selectedIds, NO_HELD),
2144
- [units, takenIds, selectedIds]
2308
+ () => unitStates(units, /* @__PURE__ */ new Set([...takenIds, ...blockedInviteOnly(units, allowInviteOnly)]), selectedIds, NO_HELD),
2309
+ [units, takenIds, selectedIds, allowInviteOnly]
2145
2310
  );
2146
2311
  const hideList = props.hideList === true;
2147
2312
  const canvasMessages = useMemo3(
@@ -2218,6 +2383,7 @@ function VenueOverview(props) {
2218
2383
  dimmedRefs,
2219
2384
  placeTypePrices,
2220
2385
  highlightRefs,
2386
+ ...marks.size > 0 ? { unitMarks: marks } : {},
2221
2387
  ...props.height !== void 0 ? { height: props.height } : {},
2222
2388
  ratio: contentRatio,
2223
2389
  cooperative: true,
@@ -3366,20 +3532,23 @@ function blockRangeText(block, templates) {
3366
3532
  }
3367
3533
  function reshapeBlock(block, patch) {
3368
3534
  const next = { ...block, ...patch };
3369
- if (block.removed.length === 0) return next;
3535
+ if (block.removed.length === 0 && !block.reserved?.length) return next;
3370
3536
  const oldRows = expandLabels(block.rowLabels, block.rows);
3371
3537
  const oldSeats = expandLabels(block.seatLabels, block.cols);
3372
3538
  const newRows = expandLabels(next.rowLabels, next.rows);
3373
3539
  const newSeats = expandLabels(next.seatLabels, next.cols);
3374
- const removed = [];
3375
- for (const id of block.removed) {
3376
- const [, row, seat] = parseUnitId(id);
3377
- const r = row === void 0 ? -1 : oldRows.indexOf(row);
3378
- const c = seat === void 0 ? -1 : oldSeats.indexOf(seat);
3379
- if (r < 0 || c < 0 || r >= next.rows || c >= next.cols) continue;
3380
- removed.push(seatUnitId(block.id, newRows[r], newSeats[c]));
3381
- }
3382
- return { ...next, removed };
3540
+ const remap = (ids) => {
3541
+ const out = [];
3542
+ for (const id of ids) {
3543
+ const [, row, seat] = parseUnitId(id);
3544
+ const r = row === void 0 ? -1 : oldRows.indexOf(row);
3545
+ const c = seat === void 0 ? -1 : oldSeats.indexOf(seat);
3546
+ if (r < 0 || c < 0 || r >= next.rows || c >= next.cols) continue;
3547
+ out.push(seatUnitId(block.id, newRows[r], newSeats[c]));
3548
+ }
3549
+ return out;
3550
+ };
3551
+ return { ...next, removed: remap(block.removed), ...block.reserved ? { reserved: remap(block.reserved) } : {} };
3383
3552
  }
3384
3553
  function labelStart(kind, raw, alphabet) {
3385
3554
  const start = kind === "letters" ? raw.trim().toUpperCase() : raw.trim();
@@ -3461,8 +3630,14 @@ function unitsOnSale(placeTypes, summary) {
3461
3630
  }
3462
3631
  function numberingCounts(block, m) {
3463
3632
  const removed = block.removed.length;
3464
- const onSale = format(m.resultOnSale, { seats: plural(block.rows * block.cols - removed, m.countSeat, m.countSeats) });
3465
- return removed === 0 ? onSale : `${onSale} \xB7 ${plural(removed, m.removedOne, m.removedMany)}`;
3633
+ const out = new Set(block.removed);
3634
+ const reserved = (block.reserved ?? []).filter((id) => !out.has(id)).length;
3635
+ const onSale = format(m.resultOnSale, { seats: plural(block.rows * block.cols - removed - reserved, m.countSeat, m.countSeats) });
3636
+ return [
3637
+ onSale,
3638
+ ...reserved > 0 ? [plural(reserved, m.reservedOne, m.reservedMany)] : [],
3639
+ ...removed > 0 ? [plural(removed, m.removedOne, m.removedMany)] : []
3640
+ ].join(" \xB7 ");
3466
3641
  }
3467
3642
 
3468
3643
  // ../react/src/designer/FloatingPanel.tsx
@@ -3955,7 +4130,7 @@ var designerEs = {
3955
4130
  blockRangeOneRow: "Fila {row} \xB7 butacas {seatFrom} a {seatTo}",
3956
4131
  numbering: "Numeraci\xF3n y pasillos",
3957
4132
  numberingTitle: "Numeraci\xF3n y pasillos \xB7 {name}",
3958
- numberingHint: "As\xED lo va a ver quien compra. Toc\xE1 una butaca para sacarla (pasillo o columna); los n\xFAmeros de las dem\xE1s no cambian.",
4133
+ numberingHint: "As\xED lo va a ver quien compra. Toc\xE1 una butaca para reservarla, o eleg\xED \xABSacar (pasillo)\xBB para sacarla; los n\xFAmeros de las dem\xE1s no cambian.",
3959
4134
  numberingTooBig: "El bloque es muy grande para mostrarlo ac\xE1. La numeraci\xF3n se puede cambiar igual.",
3960
4135
  numberingFailed: "No se pudo aplicar este cambio.",
3961
4136
  rowsNamed: "Las filas se llaman",
@@ -3991,6 +4166,17 @@ var designerEs = {
3991
4166
  seatName: "Fila {row}, butaca {seat}",
3992
4167
  seatNameRemoved: "Fila {row}, butaca {seat} sacada",
3993
4168
  seatNameSold: "Fila {row}, butaca {seat}, ya vendida",
4169
+ seatNameReserved: "Fila {row}, butaca {seat}, reservada",
4170
+ tapMode: "Al tocar",
4171
+ tapReserve: "Reservar",
4172
+ tapRemove: "Sacar (pasillo)",
4173
+ reservedNotice: "Una butaca reservada se ve en el mapa pero no se vende al p\xFAblico: queda para cortes\xEDas y cupos apartados.",
4174
+ legendReserved: "Reservada",
4175
+ reservedOne: "1 reservada",
4176
+ reservedMany: "{n} reservadas",
4177
+ tableInviteOnly: "Mesa s\xF3lo con invitaci\xF3n",
4178
+ placesInviteOnly: "Lugares s\xF3lo con invitaci\xF3n",
4179
+ placeInviteOnly: "Lugar {n} s\xF3lo con invitaci\xF3n",
3994
4180
  assignTitle: "\xBFDe qu\xE9 entrada es?",
3995
4181
  noPlaceTypes: "No hay entradas que le sirvan a esta pieza.",
3996
4182
  capacityCount: "{placed} de {capacity} lugares",
@@ -4175,7 +4361,7 @@ var designerEn = {
4175
4361
  blockRangeOneRow: "Row {row} \xB7 seats {seatFrom} to {seatTo}",
4176
4362
  numbering: "Numbering and aisles",
4177
4363
  numberingTitle: "Numbering and aisles \xB7 {name}",
4178
- numberingHint: "This is how buyers will see it. Tap a seat to remove it (an aisle or a pillar); the other numbers don't change.",
4364
+ numberingHint: "This is how buyers will see it. Tap a seat to reserve it, or pick \xABRemove (aisle)\xBB to remove it; the other numbers don't change.",
4179
4365
  numberingTooBig: "The block is too big to show here. You can still change the numbering.",
4180
4366
  numberingFailed: "This change couldn't be applied.",
4181
4367
  rowsNamed: "Rows are named",
@@ -4211,6 +4397,17 @@ var designerEn = {
4211
4397
  seatName: "Row {row}, seat {seat}",
4212
4398
  seatNameRemoved: "Row {row}, seat {seat} removed",
4213
4399
  seatNameSold: "Row {row}, seat {seat}, already sold",
4400
+ seatNameReserved: "Row {row}, seat {seat}, reserved",
4401
+ tapMode: "On tap",
4402
+ tapReserve: "Reserve",
4403
+ tapRemove: "Remove (aisle)",
4404
+ reservedNotice: "A reserved seat shows on the map but isn't sold to the public: it's kept for complimentary tickets and held allocations.",
4405
+ legendReserved: "Reserved",
4406
+ reservedOne: "1 reserved",
4407
+ reservedMany: "{n} reserved",
4408
+ tableInviteOnly: "Invitation-only table",
4409
+ placesInviteOnly: "Invitation-only places",
4410
+ placeInviteOnly: "Place {n} invitation only",
4214
4411
  assignTitle: "Which ticket is it for?",
4215
4412
  noPlaceTypes: "No tickets fit this piece.",
4216
4413
  capacityCount: "{placed} of {capacity} spots",
@@ -4382,6 +4579,38 @@ function pieceHeading(el, placeTypes, m) {
4382
4579
  const count = el.type === "seatBlock" ? plural2(expandSeatBlock(el).length, m.countSeat, m.countSeats) : el.mode === "whole" ? plural2(1, m.countTable, m.countTables) : plural2(el.places, m.countPlace, m.countPlaces);
4383
4580
  return { color: option?.color ?? null, text: `${name} \xB7 ${count}` };
4384
4581
  }
4582
+ function TableInviteOnly(props) {
4583
+ const { table: t, messages: m } = props;
4584
+ const reserved = new Set(t.reservedPlaces ?? []);
4585
+ return /* @__PURE__ */ jsxs8("div", { className: "vk-d-num-group", "data-testid": "table-invite-only", style: { ["--vk-type"]: props.color ?? "var(--vk-unassigned, #9ca3af)" }, children: [
4586
+ t.mode === "whole" ? /* @__PURE__ */ jsxs8("label", { className: "vk-d-check", children: [
4587
+ /* @__PURE__ */ jsx9(
4588
+ "input",
4589
+ {
4590
+ type: "checkbox",
4591
+ checked: t.reserved === true,
4592
+ onChange: () => props.onRun({ type: "replaceElement", element: toggleReservedTable(t) })
4593
+ }
4594
+ ),
4595
+ /* @__PURE__ */ jsx9("span", { children: m.tableInviteOnly })
4596
+ ] }) : /* @__PURE__ */ jsxs8("div", { role: "group", "aria-label": m.placesInviteOnly, children: [
4597
+ /* @__PURE__ */ jsx9("span", { className: "vk-d-sub", children: m.placesInviteOnly }),
4598
+ /* @__PURE__ */ jsx9("div", { className: "vk-d-places", children: Array.from({ length: t.places }, (_, i) => i + 1).map((n) => /* @__PURE__ */ jsx9(
4599
+ "button",
4600
+ {
4601
+ type: "button",
4602
+ className: "vk-d-place",
4603
+ "aria-pressed": reserved.has(n),
4604
+ "aria-label": format(m.placeInviteOnly, { n }),
4605
+ onClick: () => props.onRun({ type: "replaceElement", element: toggleReservedTable(t, n) }),
4606
+ children: n
4607
+ },
4608
+ n
4609
+ )) })
4610
+ ] }),
4611
+ /* @__PURE__ */ jsx9("p", { className: "vk-d-hint", children: m.reservedNotice })
4612
+ ] });
4613
+ }
4385
4614
  function PieceInspector(props) {
4386
4615
  const { element: el, messages: m } = props;
4387
4616
  const [labelError, setLabelError] = useState8(null);
@@ -4523,7 +4752,8 @@ function PieceInspector(props) {
4523
4752
  /* @__PURE__ */ jsx9("span", { "data-testid": "block-range", children: blockRangeText(el, { many: m.blockRange, one: m.blockRangeOneRow }) }),
4524
4753
  /* @__PURE__ */ jsx9("button", { type: "button", className: "vk-d-link", "aria-haspopup": "dialog", onClick: props.onNumbering, children: m.numbering })
4525
4754
  ] }),
4526
- el.type === "table" && assigned && /* @__PURE__ */ jsx9("p", { className: "vk-d-hint", children: el.mode === "whole" ? m.sellWhole : m.sellPerPlace })
4755
+ el.type === "table" && assigned && /* @__PURE__ */ jsx9("p", { className: "vk-d-hint", children: el.mode === "whole" ? m.sellWhole : m.sellPerPlace }),
4756
+ el.type === "table" && assigned && /* @__PURE__ */ jsx9(TableInviteOnly, { table: el, color: heading.color, messages: m, onRun: props.onRun })
4527
4757
  ] });
4528
4758
  }
4529
4759
 
@@ -4588,6 +4818,8 @@ function NumberingDialog(props) {
4588
4818
  const rowLabels = expandLabels(draft.rowLabels, draft.rows);
4589
4819
  const seatLabels = expandLabels(draft.seatLabels, draft.cols);
4590
4820
  const removed = new Set(draft.removed);
4821
+ const reserved = new Set(draft.reserved ?? []);
4822
+ const [tapMode, setTapMode] = useState9("reserve");
4591
4823
  const origRows = expandLabels(props.block.rowLabels, props.block.rows);
4592
4824
  const origSeats = expandLabels(props.block.seatLabels, props.block.cols);
4593
4825
  const soldAt = (r, c) => r < origRows.length && c < origSeats.length && props.lockedUnitIds.has(seatUnitId(props.block.id, origRows[r], origSeats[c]));
@@ -4615,19 +4847,21 @@ function NumberingDialog(props) {
4615
4847
  const id = seatUnitId(draft.id, row, seat);
4616
4848
  const sold = soldAt(r, c) && !removed.has(id);
4617
4849
  const out = removed.has(id);
4850
+ const held = !out && reserved.has(id);
4618
4851
  return /* @__PURE__ */ jsx10(
4619
4852
  "button",
4620
4853
  {
4621
4854
  type: "button",
4622
4855
  className: "vk-d-num-seat",
4623
4856
  "data-removed": out || void 0,
4857
+ "data-reserved": held || void 0,
4624
4858
  "data-sold": sold || void 0,
4625
4859
  disabled: sold,
4626
- "aria-pressed": out,
4627
- "aria-label": format(sold ? m.seatNameSold : out ? m.seatNameRemoved : m.seatName, { row, seat }),
4860
+ "aria-pressed": tapMode === "reserve" ? held : out,
4861
+ "aria-label": format(sold ? m.seatNameSold : out ? m.seatNameRemoved : held ? m.seatNameReserved : m.seatName, { row, seat }),
4628
4862
  onClick: () => {
4629
4863
  setFailed(false);
4630
- setDraft((d) => toggleRemovedSeat(d, id));
4864
+ setDraft((d) => tapMode === "remove" || out ? toggleRemovedSeat(d, id) : toggleReservedSeat(d, id));
4631
4865
  },
4632
4866
  children: seat
4633
4867
  },
@@ -4640,6 +4874,10 @@ function NumberingDialog(props) {
4640
4874
  /* @__PURE__ */ jsx10("span", { className: "vk-d-legend-dot", "aria-hidden": "true" }),
4641
4875
  m.legendOnSale
4642
4876
  ] }),
4877
+ /* @__PURE__ */ jsxs9("span", { children: [
4878
+ /* @__PURE__ */ jsx10("span", { className: "vk-d-legend-dot vk-d-legend-reserved", "aria-hidden": "true" }),
4879
+ m.legendReserved
4880
+ ] }),
4643
4881
  /* @__PURE__ */ jsxs9("span", { children: [
4644
4882
  /* @__PURE__ */ jsx10("span", { className: "vk-d-legend-dot vk-d-legend-out", "aria-hidden": "true" }),
4645
4883
  m.legendRemoved
@@ -4648,6 +4886,18 @@ function NumberingDialog(props) {
4648
4886
  ] })
4649
4887
  ] }),
4650
4888
  /* @__PURE__ */ jsxs9("div", { className: "vk-d-num-side", children: [
4889
+ /* @__PURE__ */ jsxs9("div", { className: "vk-d-num-group", "data-testid": "tap-mode", children: [
4890
+ /* @__PURE__ */ jsx10(
4891
+ Segmented,
4892
+ {
4893
+ label: m.tapMode,
4894
+ value: tapMode,
4895
+ options: [{ value: "reserve", label: m.tapReserve }, { value: "remove", label: m.tapRemove }],
4896
+ onChange: setTapMode
4897
+ }
4898
+ ),
4899
+ /* @__PURE__ */ jsx10("p", { className: "vk-d-hint", "data-testid": "reserved-notice", children: m.reservedNotice })
4900
+ ] }),
4651
4901
  /* @__PURE__ */ jsxs9("div", { className: "vk-d-num-group", children: [
4652
4902
  /* @__PURE__ */ jsx10(
4653
4903
  Segmented,
@@ -5266,10 +5516,15 @@ var DESIGNER_CSS = `
5266
5516
  .vk-d-num-seat{flex:none;width:34px;height:34px;border-radius:999px;border:0;padding:0;background:var(--vk-type);
5267
5517
  color:#fff;font:inherit;font-size:12px;font-weight:600;cursor:pointer}
5268
5518
  .vk-d-num-seat[data-removed]{background:transparent;border:2px dashed var(--vk-unassigned,#5a5a66);color:var(--vk-muted)}
5519
+ .vk-d-num-seat[data-reserved]{background:repeating-linear-gradient(45deg,var(--vk-type) 0 4px,var(--vk-surface,#fff) 4px 7px);border:2px solid var(--vk-fg);color:var(--vk-fg)}
5269
5520
  .vk-d-num-seat[data-sold]{cursor:not-allowed;opacity:.55}
5270
- .vk-d-legend{display:flex;gap:18px;font-size:13px;color:var(--vk-muted)}
5521
+ .vk-d-legend{display:flex;flex-wrap:wrap;justify-content:center;gap:6px 18px;font-size:13px;color:var(--vk-muted)}
5271
5522
  .vk-d-legend > span{display:flex;align-items:center;gap:6px}
5272
5523
  .vk-d-legend-dot{width:12px;height:12px;border-radius:999px;background:var(--vk-type)}
5524
+ .vk-d-places{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
5525
+ .vk-d-place{min-width:44px;min-height:44px;border-radius:999px;border:1px solid var(--vk-line);background:var(--vk-seg,transparent);color:var(--vk-fg);font:inherit;font-weight:600;cursor:pointer}
5526
+ .vk-d-place[aria-pressed="true"]{background:repeating-linear-gradient(45deg,var(--vk-type,#9ca3af) 0 4px,var(--vk-surface,#fff) 4px 7px);border:2px solid var(--vk-fg)}
5527
+ .vk-d-legend-reserved{background:repeating-linear-gradient(45deg,var(--vk-type) 0 3px,var(--vk-surface,#fff) 3px 5px);border:2px solid var(--vk-fg)}
5273
5528
  .vk-d-legend-out{background:transparent;border:2px dashed var(--vk-unassigned,#5a5a66)}
5274
5529
  .vk-d-num-side{width:340px;flex:none;border-left:1px solid var(--vk-line);padding:24px;display:flex;flex-direction:column;gap:20px;overflow:auto;background:var(--vk-surface)}
5275
5530
  .vk-d-num-group{display:flex;flex-direction:column;gap:8px}
@@ -6102,7 +6357,7 @@ function VenueDesigner(props) {
6102
6357
  }
6103
6358
 
6104
6359
  // ../react/src/index.ts
6105
- var VERSION = "0.10.3";
6360
+ var VERSION = "0.11.0";
6106
6361
  export {
6107
6362
  DESIGNER_CSS,
6108
6363
  PICKER_CSS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "venuekit",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "description": "VenueKit: editor y mapa de butacas. `venuekit` es el núcleo (sin DOM, para el servidor); `venuekit/react` trae <VenueDesigner /> y <SeatMap />.",
5
5
  "keywords": [
6
6
  "venue",