venuekit 0.10.1 → 0.10.3

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
@@ -45,7 +45,7 @@ import {
45
45
  zoneLocalSize,
46
46
  zoneOutline,
47
47
  zoneWithLocalSize
48
- } from "./chunk-4FCSKCFJ.js";
48
+ } from "./chunk-TDC5G7L7.js";
49
49
 
50
50
  // ../canvas/src/camera.ts
51
51
  var MIN_ZOOM = 0.05;
@@ -270,7 +270,15 @@ function tableBodyBox(table) {
270
270
  function elementBounds(_layout, element) {
271
271
  return pieceBounds(element);
272
272
  }
273
+ var boundsCache = /* @__PURE__ */ new WeakMap();
273
274
  function pieceBounds(element) {
275
+ const cached = boundsCache.get(element);
276
+ if (cached !== void 0) return cached;
277
+ const box = computeBounds(element);
278
+ boundsCache.set(element, box);
279
+ return box;
280
+ }
281
+ function computeBounds(element) {
274
282
  if (element.type === "zone") return zoneBounds(element);
275
283
  if (element.type === "landmark") {
276
284
  return centredBox(element.transform, element.width, element.height, element.transform.rotation);
@@ -288,12 +296,16 @@ function pieceBounds(element) {
288
296
  }
289
297
  return own;
290
298
  }
291
- function elementAt(layout, screen, camera, viewport) {
299
+ var EDIT_HIT_SLOP_PX = 4;
300
+ function elementAt(layout, screen, camera, viewport, options = {}) {
292
301
  const world = screenToWorld(camera, viewport, screen);
302
+ const slop = (options.slopPx ?? 0) / camera.zoom;
293
303
  for (let i = layout.elements.length - 1; i >= 0; i -= 1) {
294
304
  const element = layout.elements[i];
295
305
  const box = elementBounds(layout, element);
296
- if (box && world.x >= box.minX && world.x <= box.maxX && world.y >= box.minY && world.y <= box.maxY) {
306
+ const ownsRadius = element.type === "seatBlock" || element.type === "table" && element.mode === "perPlace";
307
+ const pad = element.type === "seatBlock" || element.type === "table" ? (ownsRadius ? options.radii?.get(element.id) ?? 0 : 0) + slop : 0;
308
+ if (box && world.x >= box.minX - pad && world.x <= box.maxX + pad && world.y >= box.minY - pad && world.y <= box.maxY + pad) {
297
309
  if (element.type === "zone" && !pointInZone(element, world)) continue;
298
310
  return element;
299
311
  }
@@ -481,16 +493,12 @@ function buildScene(input) {
481
493
  }
482
494
  }
483
495
  }
484
- const byElement = /* @__PURE__ */ new Map();
485
- for (const u of units) {
486
- const list2 = byElement.get(u.elementId);
487
- if (list2) list2.push(u);
488
- else byElement.set(u.elementId, [u]);
489
- }
490
- for (const el of mode === "designer" ? layout.elements : []) {
491
- if (el.type === "landmark" || colorOf(refOf(el)) !== null) continue;
496
+ const chipped = mode === "designer" ? layout.elements.filter((el) => el.type !== "landmark" && colorOf(refOf(el)) === null) : [];
497
+ const byElement = new Map(chipped.map((el) => [el.id, []]));
498
+ if (byElement.size > 0) for (const u of units) byElement.get(u.elementId)?.push(u);
499
+ for (const el of chipped) {
492
500
  const own = byElement.get(el.id);
493
- if (!own) continue;
501
+ if (!own || own.length === 0) continue;
494
502
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
495
503
  for (const u of own) {
496
504
  if (u.x < minX) minX = u.x;
@@ -1326,6 +1334,8 @@ var es = {
1326
1334
  mapLabel: "Plano del lugar. Movete con las flechas y eleg\xED con Enter.",
1327
1335
  zoomIn: "Acercar",
1328
1336
  zoomOut: "Alejar",
1337
+ zoomToChoose: "Hay {n} lugares a la vista. Acerc\xE1 el mapa para elegir uno.",
1338
+ wheelHint: "Para acercar el mapa, us\xE1 Ctrl + rueda o los botones + y \u2212",
1329
1339
  overviewMapLabel: "Plano del lugar. Toc\xE1 una entrada en el mapa o elegila en la lista de abajo.",
1330
1340
  overviewList: "Entradas del lugar",
1331
1341
  soldOut: "Agotada",
@@ -1369,6 +1379,8 @@ var en = {
1369
1379
  mapLabel: "Venue map. Move with the arrow keys and choose with Enter.",
1370
1380
  zoomIn: "Zoom in",
1371
1381
  zoomOut: "Zoom out",
1382
+ zoomToChoose: "There are {n} spots in view. Zoom in to choose one.",
1383
+ wheelHint: "To zoom the map, use Ctrl + scroll or the + and \u2212 buttons",
1372
1384
  overviewMapLabel: "Venue map. Tap a ticket on the map or choose it from the list below.",
1373
1385
  overviewList: "Tickets at the venue",
1374
1386
  soldOut: "Sold out",
@@ -1481,11 +1493,19 @@ var PICKER_CSS = `
1481
1493
  min-height:300px;max-height:70vh;background:var(--vk-surface);
1482
1494
  border:1px solid var(--vk-line);border-radius:var(--vk-radius-lg);overflow:hidden;touch-action:none}
1483
1495
  .vk-root[data-fill="true"] .vk-map{flex:1;aspect-ratio:auto;min-height:0;max-height:none}
1496
+ .vk-map[data-cooperative="true"]{touch-action:pan-y}
1484
1497
  .vk-canvas{display:block;width:100%;height:100%}
1485
1498
  .vk-a11y{position:absolute;inset:0;pointer-events:none}
1486
1499
  .vk-seat{position:absolute;transform:translate(-50%,-50%);padding:0;margin:0;border:0;background:transparent;
1487
1500
  border-radius:50%;cursor:pointer;color:transparent;overflow:hidden}
1488
1501
  .vk-seat:focus-visible{outline:3px solid var(--vk-accent);outline-offset:2px}
1502
+ .vk-choose{position:absolute;left:8px;top:8px;max-width:calc(100% - 64px);min-height:44px;padding:8px 12px;
1503
+ border:1px solid var(--vk-line);border-radius:var(--vk-radius);background:var(--vk-bg);color:var(--vk-fg);
1504
+ font:inherit;font-size:14px;text-align:left;cursor:pointer;opacity:0}
1505
+ .vk-choose:focus{opacity:1;pointer-events:auto;outline:3px solid var(--vk-accent);outline-offset:2px}
1506
+ .vk-map-hint{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin:0;max-width:calc(100% - 32px);
1507
+ padding:8px 12px;border-radius:var(--vk-radius);background:rgba(0,0,0,.72);color:#fff;font-size:14px;
1508
+ text-align:center;pointer-events:none}
1489
1509
  .vk-zoom{position:absolute;right:8px;bottom:8px;display:flex;flex-direction:column;gap:4px}
1490
1510
  .vk-zoom button{width:36px;height:36px;border:1px solid var(--vk-line);background:var(--vk-bg);
1491
1511
  color:var(--vk-fg);border-radius:var(--vk-radius);font-size:16px;line-height:1;cursor:pointer}
@@ -1567,7 +1587,37 @@ function useImage(src) {
1567
1587
 
1568
1588
  // ../react/src/VenueCanvas.tsx
1569
1589
  import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
1590
+
1591
+ // ../react/src/map-targets.ts
1592
+ var MAX_SEAT_BUTTONS = 500;
1593
+ var MIN_PICK_RADIUS_PX = 6;
1594
+ function chooseTarget(visible, activeId, centre) {
1595
+ const active = activeId === null ? void 0 : visible.find((n) => n.id === activeId);
1596
+ if (active) return active;
1597
+ let best2 = null;
1598
+ let bestD = Infinity;
1599
+ for (const n of visible) {
1600
+ const d = (n.x - centre.x) ** 2 + (n.y - centre.y) ** 2;
1601
+ if (d < bestD) {
1602
+ bestD = d;
1603
+ best2 = n;
1604
+ }
1605
+ }
1606
+ return best2;
1607
+ }
1608
+ function zoomToChoose(camera, visibleCount, target) {
1609
+ const factor = Math.max(2, Math.sqrt(visibleCount / (MAX_SEAT_BUTTONS / 2)));
1610
+ return { x: target.x, y: target.y, zoom: clampZoom(camera.zoom * factor) };
1611
+ }
1612
+ function tapZoom(camera, viewport, screen, radiusPx) {
1613
+ if (radiusPx >= MIN_PICK_RADIUS_PX) return null;
1614
+ const next = zoomAt(camera, viewport, screen, Math.max(2, MIN_NUMBERED_RADIUS_PX / Math.max(radiusPx, 0.01)));
1615
+ return next.zoom > camera.zoom ? next : null;
1616
+ }
1617
+
1618
+ // ../react/src/VenueCanvas.tsx
1570
1619
  import { jsx, jsxs } from "react/jsx-runtime";
1620
+ var WHEEL_HINT_MS = 1600;
1571
1621
  var ARROWS = {
1572
1622
  ArrowUp: "up",
1573
1623
  ArrowDown: "down",
@@ -1638,6 +1688,9 @@ function VenueCanvas(props) {
1638
1688
  );
1639
1689
  const visible = useMemo(() => new Set(scene.visibleUnitIds), [scene]);
1640
1690
  const visibleNodes = useMemo(() => a11yNodes.filter((n) => visible.has(n.id)), [a11yNodes, visible]);
1691
+ const tooMany = visibleNodes.length > MAX_SEAT_BUTTONS;
1692
+ const unitById = useMemo(() => new Map(units.map((u) => [u.id, u])), [units]);
1693
+ const chooseRef = useRef(null);
1641
1694
  const sizeOf = (elementId) => Math.max(24, 2 * (props.radii.get(elementId) ?? DEFAULT_UNIT_RADIUS) * camera.zoom);
1642
1695
  const lastSize = useRef({ width: 0, height: 0 });
1643
1696
  const onViewportChangeRef = useRef(props.onViewportChange);
@@ -1674,10 +1727,23 @@ function VenueCanvas(props) {
1674
1727
  viewportRef.current = viewport;
1675
1728
  const onCameraChangeRef = useRef(props.onCameraChange);
1676
1729
  onCameraChangeRef.current = props.onCameraChange;
1730
+ const cooperativeRef = useRef(props.cooperative === true);
1731
+ cooperativeRef.current = props.cooperative === true;
1732
+ const [wheelHint, setWheelHint] = useState2(false);
1733
+ const hintTimer = useRef(null);
1734
+ useEffect2(() => () => {
1735
+ if (hintTimer.current) clearTimeout(hintTimer.current);
1736
+ }, []);
1677
1737
  useEffect2(() => {
1678
1738
  const canvas = canvasRef.current;
1679
1739
  if (!canvas) return;
1680
1740
  const onWheel = (e) => {
1741
+ if (cooperativeRef.current && !e.ctrlKey && !e.metaKey) {
1742
+ setWheelHint(true);
1743
+ if (hintTimer.current) clearTimeout(hintTimer.current);
1744
+ hintTimer.current = setTimeout(() => setWheelHint(false), WHEEL_HINT_MS);
1745
+ return;
1746
+ }
1681
1747
  e.preventDefault();
1682
1748
  const rect = canvas.getBoundingClientRect();
1683
1749
  const currentCamera = cameraRef.current;
@@ -1697,7 +1763,7 @@ function VenueCanvas(props) {
1697
1763
  if (!id) return;
1698
1764
  const button = wrapRef.current?.querySelector(`[data-unit-id="${id}"]`);
1699
1765
  pendingFocus.current = null;
1700
- button?.focus();
1766
+ (button ?? chooseRef.current)?.focus();
1701
1767
  }, [visibleNodes]);
1702
1768
  const send = (input) => {
1703
1769
  const result = reduceGesture(gesture.current, camera, viewport, input);
@@ -1705,8 +1771,26 @@ function VenueCanvas(props) {
1705
1771
  if (result.camera !== camera) props.onCameraChange(result.camera);
1706
1772
  if (!result.tap) return;
1707
1773
  const unitId = hitUnit({ index, camera, viewport, screenPoint: result.tap, units, radii: props.radii });
1708
- if (unitId) props.onActivateUnit(unitId);
1709
- else props.onTapEmpty?.(result.tap);
1774
+ if (!unitId) {
1775
+ props.onTapEmpty?.(result.tap);
1776
+ return;
1777
+ }
1778
+ const unit = unitById.get(unitId);
1779
+ if (unit && props.focusRef !== null && unit.placeTypeRef === props.focusRef) {
1780
+ const closer = tapZoom(camera, viewport, result.tap, (props.radii.get(unit.elementId) ?? DEFAULT_UNIT_RADIUS) * camera.zoom);
1781
+ if (closer) {
1782
+ props.onCameraChange(closer);
1783
+ return;
1784
+ }
1785
+ }
1786
+ props.onActivateUnit(unitId);
1787
+ };
1788
+ const zoomInToChoose = () => {
1789
+ const target = chooseTarget(visibleNodes, activeId, camera);
1790
+ if (!target) return;
1791
+ setActiveId(target.id);
1792
+ pendingFocus.current = target.id;
1793
+ props.onCameraChange(zoomToChoose(camera, visibleNodes.length, target));
1710
1794
  };
1711
1795
  const samplePointer = (e) => {
1712
1796
  const rect = e.currentTarget.getBoundingClientRect();
@@ -1738,6 +1822,7 @@ function VenueCanvas(props) {
1738
1822
  "data-testid": "venue-map",
1739
1823
  "data-background": props.backgroundStatus,
1740
1824
  "data-camera": `${camera.x.toFixed(2)},${camera.y.toFixed(2)},${camera.zoom.toFixed(4)}`,
1825
+ "data-cooperative": props.cooperative ? "true" : void 0,
1741
1826
  style: {
1742
1827
  ["--vk-plan-ratio"]: props.ratio ? `${Math.round(props.ratio.width)} / ${Math.round(props.ratio.height)}` : `${layout.meta.width} / ${layout.meta.height}`,
1743
1828
  ...props.height !== void 0 ? { height: props.height, aspectRatio: "auto", minHeight: 0, maxHeight: "none" } : {}
@@ -1758,7 +1843,7 @@ function VenueCanvas(props) {
1758
1843
  onPointerCancel: (e) => send({ type: "pointercancel", pointer: samplePointer(e) })
1759
1844
  }
1760
1845
  ),
1761
- /* @__PURE__ */ jsx("div", { className: "vk-a11y", role: "group", "aria-label": messages.mapLabel, onKeyDown, children: visibleNodes.map((node) => {
1846
+ /* @__PURE__ */ jsx("div", { className: "vk-a11y", role: "group", "aria-label": messages.mapLabel, onKeyDown, children: tooMany ? /* @__PURE__ */ jsx("button", { ref: chooseRef, type: "button", className: "vk-choose", "data-testid": "zoom-to-choose", onClick: zoomInToChoose, children: format(messages.zoomToChoose, { n: visibleNodes.length.toLocaleString() }) }) : visibleNodes.map((node) => {
1762
1847
  const p = worldToScreen(camera, viewport, node);
1763
1848
  const size = sizeOf(node.elementId);
1764
1849
  return /* @__PURE__ */ jsx(
@@ -1779,6 +1864,7 @@ function VenueCanvas(props) {
1779
1864
  node.id
1780
1865
  );
1781
1866
  }) }),
1867
+ wheelHint ? /* @__PURE__ */ jsx("p", { className: "vk-map-hint", role: "status", children: messages.wheelHint }) : null,
1782
1868
  /* @__PURE__ */ jsxs("div", { className: "vk-zoom", children: [
1783
1869
  /* @__PURE__ */ jsx("button", { type: "button", onClick: zoomButton(1.4), "aria-label": messages.zoomIn, children: "+" }),
1784
1870
  /* @__PURE__ */ jsx("button", { type: "button", onClick: zoomButton(1 / 1.4), "aria-label": messages.zoomOut, children: "\u2212" })
@@ -1914,7 +2000,7 @@ function SeatMap(props) {
1914
2000
  announce(`${unitName(unit, messages)}: ${added ? messages.selected : messages.free}`);
1915
2001
  onSelectionChange(result.selection);
1916
2002
  }, [unitById, focusRef, states, selection, maxSelection, messages, announce, onSelectionChange, switchTo]);
1917
- const summary = selectionSummary(units, selection, messages);
2003
+ const summary = useMemo2(() => selectionSummary(units, selection, messages), [units, selection, messages]);
1918
2004
  const focusColor = placeTypeColors.get(focusRef) ?? "var(--vk-unassigned)";
1919
2005
  const focusType = props.placeTypes.find((p) => p.ref === focusRef);
1920
2006
  const embedded = props.embedded === true;
@@ -1965,6 +2051,7 @@ function SeatMap(props) {
1965
2051
  onTapEmpty,
1966
2052
  placeTypePrices,
1967
2053
  ...props.height !== void 0 ? { height: props.height } : {},
2054
+ cooperative: embedded,
1968
2055
  images,
1969
2056
  ...backgroundStatus ? { backgroundStatus } : {}
1970
2057
  }
@@ -2133,6 +2220,7 @@ function VenueOverview(props) {
2133
2220
  highlightRefs,
2134
2221
  ...props.height !== void 0 ? { height: props.height } : {},
2135
2222
  ratio: contentRatio,
2223
+ cooperative: true,
2136
2224
  images,
2137
2225
  ...backgroundStatus ? { backgroundStatus } : {}
2138
2226
  }
@@ -2394,6 +2482,57 @@ function BackgroundControl(props) {
2394
2482
  // ../react/src/designer/DesignerCanvas.tsx
2395
2483
  import { forwardRef, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo4, useRef as useRef5, useState as useState5 } from "react";
2396
2484
 
2485
+ // ../react/src/designer/drag-preview.ts
2486
+ function changedIds(preview) {
2487
+ return preview.kind === "move" ? [...preview.ids] : [preview.element.id];
2488
+ }
2489
+ function splitUnits(units, changed) {
2490
+ const still = [];
2491
+ const moving = [];
2492
+ for (const u of units) (changed.has(u.elementId) ? moving : still).push(u);
2493
+ return { changed, still, stillIndex: new SpatialIndex(still), moving, movingIndex: new SpatialIndex(moving) };
2494
+ }
2495
+ function boxesOf(layout) {
2496
+ const out = /* @__PURE__ */ new Map();
2497
+ for (const el of layout.elements) {
2498
+ const b = pieceBounds(el);
2499
+ if (b) out.set(el.id, b);
2500
+ }
2501
+ return out;
2502
+ }
2503
+ function shifted(el, dx, dy) {
2504
+ return { ...el, transform: { ...el.transform, x: el.transform.x + dx, y: el.transform.y + dy } };
2505
+ }
2506
+ function joined(still, moving, dx, dy) {
2507
+ return {
2508
+ queryRect: (q) => {
2509
+ const out = still.queryRect(q);
2510
+ for (const id of moving.queryRect({ minX: q.minX - dx, minY: q.minY - dy, maxX: q.maxX - dx, maxY: q.maxY - dy })) out.push(id);
2511
+ return out;
2512
+ }
2513
+ };
2514
+ }
2515
+ function previewFrame(base, split, baseBoxes, preview) {
2516
+ const boxes = new Map(baseBoxes);
2517
+ if (preview.kind === "move") {
2518
+ const { dx, dy } = preview;
2519
+ const layout2 = { ...base, elements: base.elements.map((el) => split.changed.has(el.id) ? shifted(el, dx, dy) : el) };
2520
+ for (const id of split.changed) {
2521
+ const b2 = baseBoxes.get(id);
2522
+ if (b2) boxes.set(id, shiftBox(b2, dx, dy));
2523
+ }
2524
+ const units = split.still.concat(split.moving.map((u) => ({ ...u, x: u.x + dx, y: u.y + dy })));
2525
+ return { layout: layout2, units, index: joined(split.stillIndex, split.movingIndex, dx, dy), boxes };
2526
+ }
2527
+ const { element } = preview;
2528
+ const layout = { ...base, elements: base.elements.map((el) => el.id === element.id ? element : el) };
2529
+ const own = expand({ ...base, elements: [element] });
2530
+ const b = pieceBounds(element);
2531
+ if (b) boxes.set(element.id, b);
2532
+ else boxes.delete(element.id);
2533
+ return { layout, units: split.still.concat(own), index: joined(split.stillIndex, new SpatialIndex(own), 0, 0), boxes };
2534
+ }
2535
+
2397
2536
  // ../react/src/designer/sizing.ts
2398
2537
  var SIZE_STEP = 0.1;
2399
2538
  var KEY_RESIZE_STEP = 20;
@@ -2416,9 +2555,6 @@ var NO_STATES = /* @__PURE__ */ new Map();
2416
2555
  var TICK_MS = 50;
2417
2556
  var MAX_SEAT_TARGETS = 1e3;
2418
2557
  var isScaled2 = (el) => el.type === "seatBlock" || el.type === "table";
2419
- function withElement(layout, el) {
2420
- return { ...layout, elements: layout.elements.map((e) => e.id === el.id ? el : e) };
2421
- }
2422
2558
  function union(boxes) {
2423
2559
  if (boxes.length === 0) return null;
2424
2560
  return boxes.reduce((a, b) => ({
@@ -2455,9 +2591,20 @@ function fullBlock(layout, id) {
2455
2591
  }
2456
2592
  var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes, lockedUnitIds, messages, frameKey, onPieceTap, backgroundImage = null, compact = false, overlay, theme }, ref) {
2457
2593
  const { ui } = designer;
2458
- const [resizing, setResizing] = useState5(null);
2594
+ const [preview, setPreview] = useState5(null);
2459
2595
  const resizingRef = useRef5(null);
2460
- const layout = useMemo4(() => resizing ? withElement(designer.layout, resizing) : designer.layout, [designer.layout, resizing]);
2596
+ const resizing = preview?.kind === "replace" ? preview.element : null;
2597
+ const base = designer.layout;
2598
+ const baseUnits = useMemo4(() => expand(base), [base]);
2599
+ const baseIndex = useMemo4(() => new SpatialIndex(baseUnits), [baseUnits]);
2600
+ const baseBoxes = useMemo4(() => boxesOf(base), [base]);
2601
+ const changedKey = preview ? changedIds(preview).join("\0") : "";
2602
+ const split = useMemo4(() => changedKey ? splitUnits(baseUnits, new Set(changedKey.split("\0"))) : null, [baseUnits, changedKey]);
2603
+ const view = useMemo4(
2604
+ () => preview && split ? previewFrame(base, split, baseBoxes, preview) : { layout: base, units: baseUnits, index: baseIndex, boxes: baseBoxes },
2605
+ [preview, split, base, baseUnits, baseIndex, baseBoxes]
2606
+ );
2607
+ const { layout, units, index, boxes } = view;
2461
2608
  const stageRef = useRef5(null);
2462
2609
  const canvasRef = useRef5(null);
2463
2610
  const [viewport, setViewport] = useState5({ width: 0, height: 0 });
@@ -2480,8 +2627,6 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2480
2627
  const spaceHeld = useRef5(false);
2481
2628
  const [panReady, setPanReady] = useState5(false);
2482
2629
  const marqueeArmed = useRef5(false);
2483
- const units = useMemo4(() => expand(layout), [layout]);
2484
- const index = useMemo4(() => new SpatialIndex(units), [units]);
2485
2630
  const radii = useMemo4(() => unitRadii(layout), [layout]);
2486
2631
  const bg = layout.meta.background;
2487
2632
  const images = useMemo4(
@@ -2489,21 +2634,21 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2489
2634
  [bg, backgroundImage]
2490
2635
  );
2491
2636
  const placeTypeColors = useMemo4(() => new Map(placeTypes.map((p) => [p.ref, p.color])), [placeTypes]);
2492
- const seatEditUnits = useMemo4(() => fullBlock(layout, ui.seatEditId), [layout, ui.seatEditId]);
2637
+ const seatEditUnits = useMemo4(() => fullBlock(base, ui.seatEditId), [base, ui.seatEditId]);
2493
2638
  const seatEditIndex = useMemo4(() => new SpatialIndex(seatEditUnits), [seatEditUnits]);
2494
2639
  const seatEditById = useMemo4(() => new Map(seatEditUnits.map((u) => [u.id, u])), [seatEditUnits]);
2495
2640
  const elementBoxes = useMemo4(
2496
2641
  () => layout.elements.flatMap((el) => {
2497
- const b = elementBounds(layout, el);
2642
+ const b = boxes.get(el.id);
2498
2643
  return b ? [{ el, b }] : [];
2499
2644
  }),
2500
- [layout]
2645
+ [layout, boxes]
2501
2646
  );
2502
2647
  const removed = useMemo4(() => {
2503
2648
  if (seatEditUnits.length === 0) return /* @__PURE__ */ new Set();
2504
- const present = new Set(units.map((u) => u.id));
2649
+ const present = new Set(baseUnits.map((u) => u.id));
2505
2650
  return new Set(seatEditUnits.filter((u) => !present.has(u.id)).map((u) => u.id));
2506
- }, [seatEditUnits, units]);
2651
+ }, [seatEditUnits, baseUnits]);
2507
2652
  useEffect5(() => {
2508
2653
  const el = stageRef.current;
2509
2654
  if (!el) return;
@@ -2574,20 +2719,19 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2574
2719
  if (bg && ui.backgroundEditing && !bg.locked) {
2575
2720
  const a = worldToScreen(camera, viewport, { x: bg.x, y: bg.y });
2576
2721
  const b = worldToScreen(camera, viewport, { x: bg.x + bg.width, y: bg.y + bg.height });
2577
- const up2 = lifted === BACKGROUND_ID;
2722
+ const up = lifted === BACKGROUND_ID;
2578
2723
  ctx.save();
2579
- if (up2) {
2724
+ if (up) {
2580
2725
  ctx.shadowColor = "rgba(0,0,0,.35)";
2581
2726
  ctx.shadowBlur = 12;
2582
2727
  } else ctx.setLineDash([6, 4]);
2583
- ctx.lineWidth = up2 ? 3 : 1.5;
2728
+ ctx.lineWidth = up ? 3 : 1.5;
2584
2729
  ctx.strokeStyle = palette.selectedStroke;
2585
2730
  ctx.strokeRect(a.x, a.y, b.x - a.x, b.y - a.y);
2586
2731
  ctx.restore();
2587
2732
  }
2588
2733
  for (const id of ui.selection) {
2589
- const selected = layout.elements.find((e) => e.id === id);
2590
- const box = selected ? elementBounds(layout, selected) : null;
2734
+ const box = boxes.get(id);
2591
2735
  if (!box) continue;
2592
2736
  const a = worldToScreen(camera, viewport, { x: box.minX, y: box.minY });
2593
2737
  const b = worldToScreen(camera, viewport, { x: box.maxX, y: box.maxY });
@@ -2612,12 +2756,11 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2612
2756
  ctx.strokeRect(Math.min(a.x, b.x), Math.min(a.y, b.y), Math.abs(b.x - a.x), Math.abs(b.y - a.y));
2613
2757
  ctx.restore();
2614
2758
  }
2615
- const up = lifted ? layout.elements.find((e) => e.id === lifted) : void 0;
2616
- const upBox = up ? elementBounds(layout, up) : null;
2617
- if (upBox) {
2759
+ const upBox = lifted ? boxes.get(lifted) : void 0;
2760
+ if (lifted && upBox) {
2618
2761
  const a = worldToScreen(camera, viewport, { x: upBox.minX, y: upBox.minY });
2619
2762
  const b = worldToScreen(camera, viewport, { x: upBox.maxX, y: upBox.maxY });
2620
- const pad = 12 + (radii.get(up.id) ?? 0) * camera.zoom;
2763
+ const pad = 12 + (radii.get(lifted) ?? 0) * camera.zoom;
2621
2764
  ctx.save();
2622
2765
  ctx.shadowColor = "rgba(0,0,0,.35)";
2623
2766
  ctx.shadowBlur = 12;
@@ -2640,7 +2783,7 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2640
2783
  }
2641
2784
  ctx.setLineDash([]);
2642
2785
  }
2643
- }, [scene, camera, viewport, dpr, images, bg, ui.backgroundEditing, ui.selection, ui.seatEditId, layout, radii, palette, seatEditUnits, removed, lifted, hints, marquee]);
2786
+ }, [scene, camera, viewport, dpr, images, bg, ui.backgroundEditing, ui.selection, ui.seatEditId, boxes, radii, palette, seatEditUnits, removed, lifted, hints, marquee]);
2644
2787
  useImperativeHandle(ref, () => ({
2645
2788
  clientToWorld: (clientX, clientY) => {
2646
2789
  const r = stageRef.current?.getBoundingClientRect();
@@ -2679,9 +2822,12 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2679
2822
  } else if (fx.type === "moveBy") {
2680
2823
  moveDragged(fx.elementId, fx.dx, fx.dy, fx.key, zoom);
2681
2824
  } else if (fx.type === "moveEnd") {
2682
- const group = drag.current && drag.current.ids.length > 1;
2825
+ const done = drag.current;
2826
+ const group = done && done.ids.length > 1;
2683
2827
  drag.current = null;
2684
2828
  setHints(null);
2829
+ setPreview(null);
2830
+ if (done && (done.applied.x !== 0 || done.applied.y !== 0)) d.moveMany(done.ids, done.applied.x, done.applied.y);
2685
2831
  if (!group) d.dispatchUi({ type: "select", id: fx.elementId });
2686
2832
  } else if (fx.type === "marquee") {
2687
2833
  setMarquee({ from: fx.from, to: fx.to });
@@ -2699,12 +2845,12 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2699
2845
  const start = d.layout.elements.find((e) => e.id === fx.elementId);
2700
2846
  if (!start) return;
2701
2847
  resizingRef.current = snappedResize(start, fx.corner, fx.from, fx.to, zoom);
2702
- setResizing(resizingRef.current);
2848
+ setPreview({ kind: "replace", element: resizingRef.current });
2703
2849
  } else if (fx.type === "resizeEnd" || fx.type === "resizeCancel") {
2704
2850
  const done = resizingRef.current;
2705
2851
  resizingRef.current = null;
2706
2852
  resizeOthers.current = null;
2707
- setResizing(null);
2853
+ setPreview(null);
2708
2854
  setHints(null);
2709
2855
  const start = d.layout.elements.find((e) => e.id === fx.elementId);
2710
2856
  if (fx.type === "resizeEnd" && done && start && done !== start) d.run({ type: "replaceElement", element: done });
@@ -2737,9 +2883,10 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2737
2883
  current.raw = { x: current.raw.x + dx, y: current.raw.y + dy };
2738
2884
  const snap = altHeld.current ? null : snapMove(shiftBox(current.start, current.raw.x, current.raw.y), current.others, SNAP_PX / zoom, { sheet: sheetBox(d.layout) });
2739
2885
  const target = { x: current.raw.x + (snap?.dx ?? 0), y: current.raw.y + (snap?.dy ?? 0) };
2740
- const ddx = target.x - current.applied.x;
2741
- const ddy = target.y - current.applied.y;
2742
- if ((ddx !== 0 || ddy !== 0) && d.moveMany(current.ids, ddx, ddy, key)) current.applied = target;
2886
+ if (target.x !== current.applied.x || target.y !== current.applied.y) {
2887
+ current.applied = target;
2888
+ setPreview({ kind: "move", ids: current.ids, dx: target.x, dy: target.y });
2889
+ }
2743
2890
  setHints(snap && (snap.guides.length > 0 || snap.gaps.length > 0) ? { guides: snap.guides, gaps: snap.gaps, matches: [] } : null);
2744
2891
  };
2745
2892
  const snappedResize = (start, corner, from, to, zoom) => {
@@ -2864,7 +3011,7 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2864
3011
  }
2865
3012
  const d = latest.current.designer;
2866
3013
  const { camera: c, viewport: v } = latest.current;
2867
- const hit = kind === "down" ? backgroundAt(d.layout.meta.background, screenToWorld(c, v, p), d.ui.backgroundEditing) ?? elementAt(d.layout, p, c, v)?.id ?? null : null;
3014
+ const hit = kind === "down" ? backgroundAt(d.layout.meta.background, screenToWorld(c, v, p), d.ui.backgroundEditing) ?? elementAt(d.layout, p, c, v, { radii, slopPx: EDIT_HIT_SLOP_PX })?.id ?? null : null;
2868
3015
  const elementLocked = d.ui.armed !== null || hit !== null && hit === d.ui.seatEditId;
2869
3016
  if (kind !== "down") {
2870
3017
  feed({ kind, p, hitElementId: hit, elementLocked });
@@ -2931,17 +3078,16 @@ var DesignerCanvas = forwardRef(function DesignerCanvas2({ designer, placeTypes,
2931
3078
  const selectedEl = ui.selectedId ? layout.elements.find((e) => e.id === ui.selectedId) : void 0;
2932
3079
  const selectedBox = useMemo4(() => {
2933
3080
  if (viewport.width === 0) return null;
2934
- const boxes = ui.selection.flatMap((id) => {
2935
- const el = layout.elements.find((e) => e.id === id);
2936
- const b = el ? elementBounds(layout, el) : null;
3081
+ const picked2 = ui.selection.flatMap((id) => {
3082
+ const b = boxes.get(id);
2937
3083
  if (!b) return [];
2938
3084
  const a = worldToScreen(camera, viewport, { x: b.minX, y: b.minY });
2939
3085
  const z = worldToScreen(camera, viewport, { x: b.maxX, y: b.maxY });
2940
3086
  const pad = 8 + (radii.get(id) ?? 0) * camera.zoom;
2941
3087
  return [{ minX: a.x - pad, minY: a.y - pad, maxX: z.x + pad, maxY: z.y + pad }];
2942
3088
  });
2943
- return union(boxes);
2944
- }, [ui.selection, layout, camera, viewport, radii]);
3089
+ return union(picked2);
3090
+ }, [ui.selection, boxes, camera, viewport, radii]);
2945
3091
  return /* @__PURE__ */ jsxs5(
2946
3092
  "div",
2947
3093
  {
@@ -3160,6 +3306,9 @@ function violationText(v, m) {
3160
3306
  return v.reason === "table" ? format(m.rejectTable, { table: v.table }) : format(m.rejectRow, { row: v.row });
3161
3307
  }
3162
3308
  function rejectionText(r, m) {
3309
+ if (r.invalid !== void 0) {
3310
+ return /máximo de unidades vendibles/.test(r.invalid) ? m.rejectTooManyUnits : m.rejectInvalid;
3311
+ }
3163
3312
  const [over, ...moreOver] = r.capacity ?? [];
3164
3313
  if (over) {
3165
3314
  const head2 = format(m.rejectCapacity, { type: over.label, capacity: over.capacity, placed: over.placed });
@@ -3176,12 +3325,12 @@ import { useEffect as useEffect6, useLayoutEffect, useRef as useRef6, useState a
3176
3325
 
3177
3326
  // ../react/src/designer/guide.ts
3178
3327
  var overlaps = (a, b) => a.minX < b.maxX && a.maxX > b.minX && a.minY < b.maxY && a.maxY > b.minY;
3179
- var shifted = (b, dx, dy) => ({ minX: b.minX + dx, minY: b.minY + dy, maxX: b.maxX + dx, maxY: b.maxY + dy });
3328
+ var shifted2 = (b, dx, dy) => ({ minX: b.minX + dx, minY: b.minY + dy, maxX: b.maxX + dx, maxY: b.maxY + dy });
3180
3329
  function freeSpot(box, occupied, gap = 24, rings = 12) {
3181
3330
  const sx = box.maxX - box.minX + gap;
3182
3331
  const sy = box.maxY - box.minY + gap;
3183
3332
  const padded = occupied.map((o) => ({ minX: o.minX - gap / 2, minY: o.minY - gap / 2, maxX: o.maxX + gap / 2, maxY: o.maxY + gap / 2 }));
3184
- const free = (dx, dy) => !padded.some((o) => overlaps(shifted(box, dx, dy), o));
3333
+ const free = (dx, dy) => !padded.some((o) => overlaps(shifted2(box, dx, dy), o));
3185
3334
  if (free(0, 0)) return { dx: 0, dy: 0 };
3186
3335
  const dirs = [[0, 1], [1, 0], [0, -1], [-1, 0], [1, 1], [-1, 1], [1, -1], [-1, -1]];
3187
3336
  for (let k = 1; k <= rings; k++) {
@@ -3851,6 +4000,8 @@ var designerEs = {
3851
4000
  trimToCapacityLabel: "Sacar los {over} lugares que sobran de {name}",
3852
4001
  trimPartial: "Quedan {n} de m\xE1s que no se pueden sacar: ya est\xE1n vendidos o reservados.",
3853
4002
  rejectCapacity: "{type} tiene {capacity} lugares y ya ubicaste {placed}.",
4003
+ rejectInvalid: "No se pudo hacer ese cambio: la pieza quedar\xEDa demasiado grande o fuera del plano.",
4004
+ rejectTooManyUnits: "No se pudo hacer ese cambio: un plano puede tener hasta 100.000 lugares.",
3854
4005
  zonesTitle: "Tus entradas generales",
3855
4006
  zonesHint: "Si quer\xE9s, marc\xE1 en el plano d\xF3nde queda cada una. Quien compra la toca en el mapa.",
3856
4007
  acceptsZone: "Entrada general",
@@ -4069,6 +4220,8 @@ var designerEn = {
4069
4220
  trimToCapacityLabel: "Remove the {over} extra spots of {name}",
4070
4221
  trimPartial: "{n} extra spots can't be removed: they are already sold or held.",
4071
4222
  rejectCapacity: "{type} has {capacity} spots and you've already placed {placed}.",
4223
+ rejectInvalid: "That change couldn't be made: the piece would be too big or off the plan.",
4224
+ rejectTooManyUnits: "That change couldn't be made: a plan can have up to 100,000 spots.",
4072
4225
  zonesTitle: "Your general admission tickets",
4073
4226
  zonesHint: "If you want, mark on the plan where each one is. Buyers tap it on the map.",
4074
4227
  acceptsZone: "General admission",
@@ -5273,7 +5426,8 @@ function elementPrefix(el) {
5273
5426
  return el.type === "seatBlock" ? "b" : el.type === "table" ? "m" : el.type === "zone" ? "z" : "r";
5274
5427
  }
5275
5428
  function useDesigner({ initial, lockedUnitIds, onChange, placeTypes }) {
5276
- const historyRef = useRef9(new History(initial));
5429
+ const [history] = useState10(() => new History(initial));
5430
+ const historyRef = useRef9(history);
5277
5431
  const [layout, setLayout] = useState10(initial);
5278
5432
  const [rejection, setRejectionState] = useState10(null);
5279
5433
  const [rejectionSeq, setRejectionSeq] = useState10(0);
@@ -5300,10 +5454,10 @@ function useDesigner({ initial, lockedUnitIds, onChange, placeTypes }) {
5300
5454
  const previous = seenTypes.current.list;
5301
5455
  const next = placeTypesRef.current;
5302
5456
  seenTypes.current = { key: typesKey, list: next };
5303
- const history = historyRef.current;
5304
- const before = history.present;
5305
- history.rewrite((l) => syncPlaceTypes(l, previous, next));
5306
- if (history.present !== before) commit(history.present);
5457
+ const history2 = historyRef.current;
5458
+ const before = history2.present;
5459
+ history2.rewrite((l) => syncPlaceTypes(l, previous, next));
5460
+ if (history2.present !== before) commit(history2.present);
5307
5461
  }, [typesKey, commit]);
5308
5462
  const run = useCallback3(
5309
5463
  (command, key) => {
@@ -5311,20 +5465,22 @@ function useDesigner({ initial, lockedUnitIds, onChange, placeTypes }) {
5311
5465
  let candidate;
5312
5466
  try {
5313
5467
  candidate = applyCommand(before, command);
5314
- } catch {
5468
+ } catch (error) {
5469
+ setRejection({ violations: [], invalid: error instanceof Error ? error.message : String(error) });
5315
5470
  return false;
5316
5471
  }
5317
- const violations = findLockViolations(before, candidate, locked);
5472
+ const moveOnly = keepsCount(command);
5473
+ const violations = moveOnly ? [] : findLockViolations(before, candidate, locked);
5318
5474
  if (violations.length > 0) {
5319
5475
  setRejection({ violations });
5320
5476
  return false;
5321
5477
  }
5322
- const capacity = keepsCount(command) ? [] : capacityRejections(before, candidate, placeTypesRef.current);
5478
+ const capacity = moveOnly ? [] : capacityRejections(before, candidate, placeTypesRef.current);
5323
5479
  if (capacity.length > 0) {
5324
5480
  setRejection({ violations: [], capacity });
5325
5481
  return false;
5326
5482
  }
5327
- commit(historyRef.current.apply(command, key));
5483
+ commit(historyRef.current.commit(candidate, key));
5328
5484
  return true;
5329
5485
  },
5330
5486
  [locked, commit, setRejection]
@@ -5946,7 +6102,7 @@ function VenueDesigner(props) {
5946
6102
  }
5947
6103
 
5948
6104
  // ../react/src/index.ts
5949
- var VERSION = "0.10.1";
6105
+ var VERSION = "0.10.3";
5950
6106
  export {
5951
6107
  DESIGNER_CSS,
5952
6108
  PICKER_CSS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "venuekit",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
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",