venuekit 0.10.0 → 0.10.1
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/README.md +286 -280
- package/dist/{chunk-5H5QKXPV.js → chunk-4FCSKCFJ.js} +1 -1
- package/dist/{chunk-47ILVPKZ.cjs → chunk-LFRA3GBP.cjs} +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react.cjs +97 -97
- package/dist/react.d.cts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,326 +1,332 @@
|
|
|
1
1
|
# VenueKit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/venuekit)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
**Seating charts for ticketing apps.** A floor-plan editor for organizers and an interactive
|
|
7
|
+
seat map for buyers, in one React package with a framework-free core.
|
|
8
|
+
|
|
9
|
+
Draw a venue with numbered seat blocks, tables, standing-room zones and landmarks (stage,
|
|
10
|
+
bar, restrooms…), assign each piece to a ticket type, and let buyers pick their exact seat.
|
|
11
|
+
VenueKit handles the drawing, numbering, validation and the "don't break what's already
|
|
12
|
+
sold" rules; your app keeps the money, the inventory and the database.
|
|
13
|
+
|
|
14
|
+
- **Editor (`<VenueDesigner />`)** — guided by your ticket types: blocks of seats (straight or
|
|
15
|
+
curved), round and square tables, general-admission zones, landmarks, a background image to
|
|
16
|
+
trace over, resize handles, smart alignment guides, multi-select with align/distribute,
|
|
17
|
+
undo/redo, keyboard support, and a phone layout.
|
|
18
|
+
- **Numbering your way** — rows with letters (optionally with Ñ, then AA, AB…) or numbers;
|
|
19
|
+
seats in sequence, odd only, even only, or from the center out; any starting point,
|
|
20
|
+
left-to-right or right-to-left, skipped numbers, and removed seats for aisles.
|
|
21
|
+
- **Capacity aware** — give each ticket type its capacity and the editor sizes new blocks to
|
|
22
|
+
it, refuses to go over, and offers to trim the surplus if the capacity goes down.
|
|
23
|
+
- **Sold seats are safe** — pieces with sold or held seats can be moved, rotated or resized,
|
|
24
|
+
but never deleted, renumbered or reassigned.
|
|
25
|
+
- **Buyer map (`<SeatMap />`, `<VenueOverview />`)** — the whole venue at a glance, tap a zone
|
|
26
|
+
or a section to pick it, zoom into a section to choose seats, clear free / yours / sold
|
|
27
|
+
states, screen-reader and keyboard friendly, built for touch.
|
|
28
|
+
- **Server-side core** — the same validation runs on your server: schema, limits, unit IDs,
|
|
29
|
+
lock checks, capacity checks. No DOM, no React.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
16
32
|
|
|
17
33
|
```bash
|
|
18
34
|
npm install venuekit
|
|
19
35
|
```
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
nombres hasta `MAX_NAME_LENGTH`, `ref` hasta `MAX_REF_LENGTH`, número de mesa hasta
|
|
40
|
-
`MAX_TABLE_LABEL_LENGTH`, `background.src` (una URL) hasta `MAX_SRC_LENGTH`; la primera fila
|
|
41
|
-
con letras va de 1 a `MAX_LETTERS_START` letras (con números, hasta 9 dígitos) y `skip`
|
|
42
|
-
tiene hasta `MAX_SKIP` entradas. Un plano fuera de esos topes no pasa `migrate`.
|
|
43
|
-
|
|
44
|
-
Numeración de un bloque (`rowLabels` / `seatLabels`, campos opcionales nuevos en 0.10; un
|
|
45
|
-
plano viejo sin ellos vale igual que antes): `pattern` (sólo con números) es `"sequential"`
|
|
46
|
-
(1, 2, 3…, el default), `"odd"` (1, 3, 5…), `"even"` (2, 4, 6…) o `"center"` (desde el
|
|
47
|
-
centro). Con impares o pares se arranca en el primero de esa clase que no sea menor que
|
|
48
|
-
`start`. «Desde el centro»: la primera va en el medio y las siguientes alternan alejándose
|
|
49
|
-
(la 2.ª a la izquierda, la 3.ª a la derecha…); con una fila par, la primera va en la del
|
|
50
|
-
medio de la derecha y la segunda en la de la izquierda. Empezando en 1: `6 4 2 1 3 5 7`
|
|
51
|
-
(fila impar) y `6 4 2 1 3 5` (par); `direction: "rtl"` lo espeja (impares a la izquierda).
|
|
52
|
-
`skip` saca números o letras de la secuencia en todos los patrones. `alphabet: "es"` (sólo
|
|
53
|
-
con letras) suma la Ñ después de la N; sin eso no hay Ñ, como siempre. Después de la Z sigue
|
|
54
|
-
AA, AB…
|
|
55
|
-
|
|
56
|
-
| Export | Para qué |
|
|
57
|
-
| --- | --- |
|
|
58
|
-
| `layoutSchema` | Esquema zod del plano v2. |
|
|
59
|
-
| `migrate(input)` | La puerta de entrada de un plano de afuera: lo valida (esquema y topes) y lo devuelve como Layout v2. Sólo acepta v2: la v1 nunca se publicó y da `LayoutError`, igual que cualquier plano que no sirva. |
|
|
60
|
-
| `CURRENT_VERSION` | Versión actual del plano (hoy `2`). |
|
|
61
|
-
| `expand(layout)` | Las unidades vendibles con sus IDs: butaca `"<bloque>:<fila>:<butaca>"`, lugar de mesa `"<mesa>:<n>"`, mesa entera = id de la mesa. Mover o girar no cambia IDs. |
|
|
62
|
-
| `findLockViolations(antes, despues, bloqueados)` | Qué unidades vendidas o retenidas desaparecerían, se renombrarían o cambiarían de lugar físico con el cambio (desde 0.10 también una renumeración que corre los nombres: empezar en B, saltear, invertir el sentido, cambiar el patrón). Mover, girar, escalar o curvar la pieza entera se puede. Para validar en el servidor lo mismo que el editor rechaza. |
|
|
63
|
-
| `summarize(layout)` | Conteos del plano: butacas, mesas y lugares, en total y por tipo de lugar. Por tipo trae también `duplicates: { seats, tables }`: butacas (`"fila:butaca"`, p. ej. `"A:1"`) y números de mesa repetidos adentro de ese tipo. Con algo ahí, el host no debería dejar publicar. |
|
|
64
|
-
| `rebindPlaceTypes(layout, opciones)` | Re-enlaza un plano guardado a los tipos de lugar del evento actual, por nombre. Lo que no encuentra queda sin asignar. |
|
|
65
|
-
| `canAssign(pieza, opcion)` | Si una pieza admite ese tipo de lugar. |
|
|
66
|
-
| `PlaceTypeOption` | `{ ref, label, color, accepts: "seats" \| "tableWhole" \| "tablePerPlace" \| "zone", capacitySteps? }`. `capacitySteps`: los cupos acumulados de los lotes, en orden de venta (lotes de 30, 50 y 100 → `[30, 80, 180]`); sin eso, sin tope. `"zone"`: una entrada general, que se marca como zona y no pone lugares. |
|
|
67
|
-
| `placeTypeCapacity` / `nextBlockUnits` / `capacityViolations` | El tope de un tipo (el mayor cupo), cuántas butacas trae el próximo bloque (de a tramos: `[30, 80, 180]` → bloques de 30, 50 y 100) y qué tipos pasa un cambio de su cupo. Para validar en el servidor lo mismo que el editor rechaza. |
|
|
68
|
-
| `landmarks` | Catálogo de referencias del lugar. |
|
|
37
|
+
`react` and `react-dom` ≥ 18 are peer dependencies, needed only for `venuekit/react`. The
|
|
38
|
+
core (`venuekit`) has no DOM or React dependency.
|
|
39
|
+
|
|
40
|
+
The package has two entry points:
|
|
41
|
+
|
|
42
|
+
| Import | What | Where |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `venuekit` | Core: the plan format, validation, expansion to sellable units, locking, capacity. | Server and client |
|
|
45
|
+
| `venuekit/react` | `<VenueDesigner />`, `<SeatMap />`, `<VenueOverview />`, messages and themes. Starts with `"use client"`. | Client |
|
|
46
|
+
|
|
47
|
+
> **Language:** the built-in UI text defaults to Spanish (Argentina). Pass the English
|
|
48
|
+
> messages shown in the examples (`designerEn`, `en`) or your own overrides.
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
### 1. Describe your ticket types
|
|
53
|
+
|
|
54
|
+
Each ticket type that can be placed on the plan is a `PlaceTypeOption`:
|
|
69
55
|
|
|
70
56
|
```ts
|
|
71
|
-
import {
|
|
57
|
+
import type { PlaceTypeOption } from "venuekit"
|
|
72
58
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
const placeTypes: PlaceTypeOption[] = [
|
|
60
|
+
{ ref: "orchestra", label: "Orchestra", color: "#e5482c", accepts: "seats", capacitySteps: [120] },
|
|
61
|
+
{ ref: "boxes", label: "Boxes", color: "#f2b14c", accepts: "tableWhole" },
|
|
62
|
+
{ ref: "floor", label: "Floor (standing)", color: "#3fc59a", accepts: "zone" },
|
|
63
|
+
]
|
|
76
64
|
```
|
|
77
65
|
|
|
78
|
-
|
|
66
|
+
- `ref` — your ID for the ticket type. VenueKit stores it on each piece.
|
|
67
|
+
- `accepts` — `"seats"` (numbered seats), `"tableWhole"` (a whole table or box is one
|
|
68
|
+
unit), `"tablePerPlace"` (each chair is a unit) or `"zone"` (a standing area sold by
|
|
69
|
+
quantity; drawing its zone is optional).
|
|
70
|
+
- `capacitySteps` — optional. The cumulative capacity of the type's price tiers, in sale
|
|
71
|
+
order. Tiers of 30, 50 and 100 seats are `[30, 80, 180]`: the editor offers blocks of 30,
|
|
72
|
+
50 and 100, and caps the type at 180. Leave it out for no cap.
|
|
79
73
|
|
|
80
|
-
###
|
|
74
|
+
### 2. Let organizers draw the plan
|
|
81
75
|
|
|
82
76
|
```tsx
|
|
83
|
-
|
|
84
|
-
import {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
77
|
+
"use client"
|
|
78
|
+
import { useMemo, useState } from "react"
|
|
79
|
+
import { migrate, rebindPlaceTypes, type Layout } from "venuekit"
|
|
80
|
+
import { VenueDesigner, designerEn } from "venuekit/react"
|
|
81
|
+
|
|
82
|
+
export function PlanEditor({ saved, soldOrHeld }: { saved: unknown; soldOrHeld: string[] }) {
|
|
83
|
+
// A new `layout` object means "load another plan": memoize it.
|
|
84
|
+
const initial = useMemo(() => (saved ? rebindPlaceTypes(migrate(saved), placeTypes) : null), [saved])
|
|
85
|
+
const locked = useMemo(() => new Set(soldOrHeld), [soldOrHeld])
|
|
86
|
+
const [draft, setDraft] = useState<Layout | null>(initial)
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
// The editor fills its container: give the container a height.
|
|
90
|
+
<div style={{ height: "100dvh" }}>
|
|
91
|
+
<VenueDesigner
|
|
92
|
+
layout={initial} // null = a blank plan named after `name`
|
|
93
|
+
name="Main theater"
|
|
94
|
+
placeTypes={placeTypes}
|
|
95
|
+
lockedUnitIds={locked} // sold or held by anyone
|
|
96
|
+
onChange={setDraft}
|
|
97
|
+
onDone={async (layout) => { await savePlan(layout) }} // the "Done" button
|
|
98
|
+
theme="dark" // "light" (default) or "dark"
|
|
99
|
+
background={{ onPick: uploadImageAndReturnUrl }}
|
|
100
|
+
messages={designerEn}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
103
105
|
```
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
tarjeta por cada `placeTypes` con lo que ya tiene en el plano («40 butacas», «3 mesas»,
|
|
107
|
-
«24 lugares») o «Falta ubicar», y los botones para agregarle piezas (bloque de butacas; mesa
|
|
108
|
-
redonda o cuadrada; mesa con sillas). Cada pieza nace **ya asignada** a esa entrada, en el
|
|
109
|
-
centro de lo que se ve y corrida si ahí ya hay algo. La pieza elegida muestra sus datos en un
|
|
110
|
-
panel junto a ella (filas, butacas por fila, forma; número y personas de una mesa) y
|
|
111
|
-
«Numeración y pasillos» abre un diálogo con la vista previa del bloque: cómo se llaman las
|
|
112
|
-
filas (letras, con o sin Ñ, o números), dónde empiezan y qué letras se saltean; cómo se
|
|
113
|
-
numeran las butacas (seguidas, sólo impares, sólo pares o desde el centro), dónde empiezan,
|
|
114
|
-
de qué lado y qué números se saltean; y qué butacas se sacan. Lo del
|
|
115
|
-
diálogo se aplica todo junto al tocar «Listo» (un solo paso de deshacer, chequeado por el
|
|
116
|
-
candado); «Cancelar» no toca el plano. Arriba, «+ Referencia» (escenario, baños, barra…) e
|
|
117
|
-
«Imagen de fondo».
|
|
118
|
-
|
|
119
|
-
- `placeTypes`: de ahí sale el color de cada pieza y qué piezas admite cada tipo. Sin
|
|
120
|
-
ninguno, el panel pide cargar entradas numeradas primero.
|
|
121
|
-
- Cupo (nuevo en 0.4): con `capacitySteps`, la tarjeta dice «30 de 100 lugares», «Agregar
|
|
122
|
-
bloque» trae lo que falta para el próximo lote y, completo, se apaga con «Ya ubicaste los
|
|
123
|
-
100 lugares de Platea». Ningún cambio (filas, butacas, duplicar, numeración, lugares de
|
|
124
|
-
mesa, cambiar de tipo, un `layout` nuevo) puede pasarlo: se rechaza con «Platea tiene 100
|
|
125
|
-
lugares y ya ubicaste 100.». Deshacer siempre se puede. Si el host baja el cupo y sobran
|
|
126
|
-
lugares, la tarjeta lo dice y ofrece «Ajustar» (nuevo en 0.10): saca los que sobran
|
|
127
|
-
empezando por los últimos (última fila del último bloque hacia atrás; las últimas mesas),
|
|
128
|
-
en un paso de deshacer y sin tocar nunca lo vendido o retenido. Si los lotes del host
|
|
129
|
-
suman (30, 50 y 100 = 180), `capacitySteps` va con las sumas acumuladas: `[30, 80, 180]`.
|
|
130
|
-
- Generales (nuevo en 0.4): las opciones `accepts: "zone"` aparecen en «Tus entradas
|
|
131
|
-
generales» con «+ Marcar zona de X». La zona es un rectángulo con el nombre, de ancho y
|
|
132
|
-
alto ajustables; marcarla es opcional y nunca figura como «Falta ubicar». Desde 0.8 puede
|
|
133
|
-
ser curva («Forma: Recta / Curva», como un bloque): `zone.curve` (grados, default 0) la
|
|
134
|
-
vuelve una tribuna en arco, concéntrica con un bloque de la misma curva; el ancho se mide
|
|
135
|
-
sobre el arco central y el alto es el grosor.
|
|
136
|
-
- `sidePanel` (nuevo en 0.9): `(api: DesignerHostApi) => ReactNode`, el panel de entradas
|
|
137
|
-
del host en lugar de «Tus entradas numeradas» (a la izquierda; en el celular, dentro de
|
|
138
|
-
la hoja «Entradas»). `api`: `addPiece(kind, ref)` (como los botones de la librería: cupo
|
|
139
|
-
por tramos, centro de lo visible, sin encimar; devuelve el id o `null`),
|
|
140
|
-
`nextPieceUnits(ref)` (0 = cupo completo), `placed(ref)` (`{ units, zones, pieces }`),
|
|
141
|
-
`capacityText(ref)`, `capacityFullText(ref)`, `overCapacity(ref)` (cuántos sobran del
|
|
142
|
-
cupo, 0 si ninguno), `trimToCapacity(ref)` («Ajustar»: `{ removed }`, menos de lo que
|
|
143
|
-
sobraba si lo vendido no dejó; `null` si no sobra nada), `missingRefs`, `select(id)`, `reveal(id)`,
|
|
144
|
-
`selectedRef` y `summary`. `api` no cambia al arrastrar; pasá `sidePanel` estable
|
|
145
|
-
(`useCallback`) para que el panel no se redibuje en cada cuadro.
|
|
146
|
-
- `placeTypes` cambiantes (nuevo en 0.9): con el editor abierto se pueden agregar, renombrar,
|
|
147
|
-
recolorear o borrar entradas. El plano y la historia las siguen sin un paso de deshacer:
|
|
148
|
-
las piezas de una entrada borrada quedan sin asignar (`ref: ""`, con el nombre) y se
|
|
149
|
-
avisa por `onChange`.
|
|
150
|
-
- `lockedUnitIds`: lo vendido y lo retenido por cualquier comprador. Moverlas se puede; lo
|
|
151
|
-
que las borre, renumere o pase a otro tipo se rechaza con un aviso. Vale también para
|
|
152
|
-
Deshacer/Rehacer y para un `layout` nuevo del host: si toca algo de esto, no se carga y
|
|
153
|
-
se muestra el mismo aviso. **El host tiene que validar con `findLockViolations` antes de
|
|
154
|
-
pasar un plano nuevo**, y otra vez en el servidor al guardar.
|
|
155
|
-
- `onDone` (nuevo en 0.3): el botón «Listo». Puede ser async; si falla, se avisa. Si no está,
|
|
156
|
-
el botón dice «Guardar» y llama a `onSave` como antes.
|
|
157
|
-
- `onClose` (nuevo en 0.3): el botón «Volver» de arriba a la izquierda. Sin esto, no hay.
|
|
158
|
-
- `theme` (nuevo en 0.3): `"dark"` pone los valores por defecto oscuros de los tokens
|
|
159
|
-
`--vk-*` (los del diseño); `"light"` es el de siempre. Los tokens del host siguen ganando.
|
|
160
|
-
- Teclado: «Poner en el centro» (o Enter sobre el plano) pone la referencia armada; las
|
|
161
|
-
flechas mueven la elegida (Shift, de a más).
|
|
162
|
-
- Ordenar el plano (nuevo en 0.7): al mover, la pieza se pega a bordes y centros de las
|
|
163
|
-
demás y al centro de la hoja, con una guía; también al mismo espacio que ya hay entre dos
|
|
164
|
-
vecinas, con una cota. Las manijas pegan el borde a otras piezas y el ancho/alto al de
|
|
165
|
-
otra; en bloques y mesas, la escala de otra del mismo tipo (± 5 %). Alt/Option, sin imán.
|
|
166
|
-
- Varias piezas (nuevo en 0.7): Shift o ⌘/Ctrl + clic suma o saca; arrastrar en un lugar
|
|
167
|
-
vacío dibuja un recuadro que elige lo que toca (⌘/Ctrl + A, todas). Con el dedo, mantener
|
|
168
|
-
apretado en vacío y arrastrar es el recuadro; arrastrar sin esperar sigue moviendo el
|
|
169
|
-
plano. Con el mouse, el plano se mueve con espacio + arrastrar, el botón del medio o la
|
|
170
|
-
rueda (zoom). Con dos o más elegidas, el panel ofrece alinear, repartir con el mismo
|
|
171
|
-
espacio, igualar ancho/alto o tamaño, duplicar y borrar; todo es un paso de deshacer y
|
|
172
|
-
pasa por el mismo candado y cupo.
|
|
173
|
-
- `onChange` se llama con cada cambio.
|
|
174
|
-
- `background.onPick` lo resuelve el host (sube el archivo y devuelve la URL, o `null`);
|
|
175
|
-
`suggestedSrc` ofrece con un toque la imagen del evento.
|
|
176
|
-
- En un contenedor de 768 px o menos (el celular) el lienzo ocupa todo: «Tus entradas», la
|
|
177
|
-
pieza elegida, las referencias y la imagen van en hojas desde abajo, y la numeración en una
|
|
178
|
-
hoja de todo el alto. Se mide el contenedor, no la ventana.
|
|
179
|
-
|
|
180
|
-
### `<SeatMap />`
|
|
107
|
+
### 3. Let buyers pick their seats
|
|
181
108
|
|
|
182
109
|
```tsx
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
110
|
+
"use client"
|
|
111
|
+
import { useState } from "react"
|
|
112
|
+
import type { Layout } from "venuekit"
|
|
113
|
+
import { SeatMap, en } from "venuekit/react"
|
|
114
|
+
|
|
115
|
+
export function ChooseSeats({ layout, taken }: { layout: Layout; taken: string[] }) {
|
|
116
|
+
const [selection, setSelection] = useState<string[]>([])
|
|
117
|
+
return (
|
|
118
|
+
<SeatMap
|
|
119
|
+
layout={layout}
|
|
120
|
+
placeTypes={[
|
|
121
|
+
{ ref: "orchestra", label: "Orchestra", color: "#e5482c", priceLabel: "$40" },
|
|
122
|
+
{ ref: "boxes", label: "Boxes", color: "#f2b14c", priceLabel: "$160" },
|
|
123
|
+
]}
|
|
124
|
+
focusRef="orchestra" // zoom into the section the buyer picked
|
|
125
|
+
takenUnitIds={taken} // sold + held by other buyers
|
|
126
|
+
selection={selection}
|
|
127
|
+
onSelectionChange={setSelection}
|
|
128
|
+
maxSelection={6}
|
|
129
|
+
totalLabel="$80" // preformatted: VenueKit never formats money
|
|
130
|
+
onContinue={() => checkout(selection)}
|
|
131
|
+
messages={en}
|
|
132
|
+
/>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
200
135
|
```
|
|
201
136
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
- `takenUnitIds`: lo vendido **y lo retenido por otras personas**. `heldUnitIds` es sólo lo
|
|
205
|
-
retenido **para este comprador** (lo suyo, en otra pestaña o en un paso anterior).
|
|
206
|
-
- Otros props: `maxSelection`, `continueLabel`, `fill` (el mapa es la página: barra abajo),
|
|
207
|
-
`messages`, `className`, `style`.
|
|
137
|
+
Need the whole venue first? `<VenueOverview />` shows every section and zone and calls
|
|
138
|
+
`onSelect(ref)` when the buyer taps one. See [Buyer map in a card](#buyer-map-in-a-card).
|
|
208
139
|
|
|
209
|
-
###
|
|
140
|
+
### 4. Validate on the server
|
|
210
141
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
142
|
+
Plans come from the browser: always validate before saving.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { migrate, expand, findLockViolations, summarize, LayoutError, type Layout } from "venuekit"
|
|
146
|
+
|
|
147
|
+
export function checkIncomingPlan(input: unknown, current: Layout | null, soldOrHeld: Set<string>) {
|
|
148
|
+
let next: Layout
|
|
149
|
+
try {
|
|
150
|
+
next = migrate(input) // schema + size limits; throws LayoutError
|
|
151
|
+
} catch (e) {
|
|
152
|
+
if (e instanceof LayoutError) return { ok: false as const, reason: e.message }
|
|
153
|
+
throw e
|
|
154
|
+
}
|
|
155
|
+
if (current && findLockViolations(current, next, soldOrHeld).length > 0) {
|
|
156
|
+
return { ok: false as const, reason: "The change would remove, renumber or reassign a sold seat." }
|
|
157
|
+
}
|
|
158
|
+
const units = expand(next) // every sellable unit with a stable ID
|
|
159
|
+
const perType = summarize(next).byRef // per type: units, duplicated names…
|
|
160
|
+
return { ok: true as const, next, units, perType }
|
|
161
|
+
}
|
|
220
162
|
```
|
|
221
163
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
`
|
|
226
|
-
|
|
227
|
-
|
|
164
|
+
## Core concepts
|
|
165
|
+
|
|
166
|
+
**Plan (`Layout`).** A JSON document `{ meta, elements }`. `meta` holds `id`, `name`,
|
|
167
|
+
`version`, `width`, `height` and an optional `background` image. `elements` are:
|
|
168
|
+
|
|
169
|
+
| Element | What |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| `seatBlock` | Rows × seats, straight or curved, with its own row and seat numbering and removed seats. |
|
|
172
|
+
| `table` | Round or rectangular, sold whole or per chair. |
|
|
173
|
+
| `zone` | A standing-room area for a general-admission ticket, straight or curved. No units. |
|
|
174
|
+
| `landmark` | A reference shape (stage, bar, restrooms, entrance…). Not for sale. |
|
|
175
|
+
|
|
176
|
+
Each sellable piece may carry `placeType: { ref, name }` linking it to a ticket type.
|
|
177
|
+
`ref: ""` means "unassigned, but remember the name", so a saved plan can be re-linked by name
|
|
178
|
+
with `rebindPlaceTypes` when it is reused for another event.
|
|
228
179
|
|
|
229
|
-
|
|
180
|
+
**Units.** `expand(layout)` returns every sellable unit with a stable ID: a seat is
|
|
181
|
+
`"<block>:<row>:<seat>"`, a chair is `"<table>:<n>"`, a whole table is the table's ID.
|
|
182
|
+
Moving, rotating or resizing a piece never changes IDs. Store these IDs in your inventory.
|
|
230
183
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
184
|
+
**Locking.** Pass the IDs of sold or held units as `lockedUnitIds` to the editor. Pieces can
|
|
185
|
+
still be moved, rotated, resized or curved, but anything that would delete a locked unit,
|
|
186
|
+
change its label or move a label to a different physical seat (renumbering) is refused.
|
|
187
|
+
`findLockViolations(before, after, locked)` runs the same check on the server.
|
|
234
188
|
|
|
235
|
-
|
|
189
|
+
**Capacity.** With `capacitySteps`, the editor shows "30 of 100 spots", sizes new blocks to
|
|
190
|
+
the next tier, blocks any edit that would exceed the cap and, if the cap goes down, shows
|
|
191
|
+
how many spots are left over with an "Adjust" action that removes them from the end
|
|
192
|
+
without touching locked units.
|
|
193
|
+
|
|
194
|
+
**Limits.** Plans are untrusted input, so the schema has hard limits (exported as constants):
|
|
195
|
+
`MAX_UNITS`, `MAX_ELEMENTS`, `MAX_ID_LENGTH`, `MAX_NAME_LENGTH`, `MAX_REF_LENGTH`,
|
|
196
|
+
`MAX_TABLE_LABEL_LENGTH`, `MAX_SRC_LENGTH`, `MAX_SKIP`, `MAX_LETTERS_START`.
|
|
197
|
+
|
|
198
|
+
## API
|
|
199
|
+
|
|
200
|
+
### `venuekit` (core)
|
|
201
|
+
|
|
202
|
+
| Export | Purpose |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `migrate(input)` | Validates an external plan (schema and limits) and returns a `Layout`. Throws `LayoutError`. |
|
|
205
|
+
| `layoutSchema`, `CURRENT_VERSION` | The zod schema and the current plan version. |
|
|
206
|
+
| `expand(layout)` | Sellable units with stable IDs, positions and labels. |
|
|
207
|
+
| `summarize(layout)` | Counts for the plan and per type: seats, tables, places, zones, and duplicated seat or table names. |
|
|
208
|
+
| `findLockViolations(before, after, locked)` | Changes that would break sold or held units. |
|
|
209
|
+
| `rebindPlaceTypes(layout, options)` | Re-links a saved plan to the current event's ticket types by name. |
|
|
210
|
+
| `canAssign(piece, option)` | Whether a piece accepts a ticket type. |
|
|
211
|
+
| `placeTypeCapacity`, `nextBlockUnits`, `capacityViolations` | Capacity helpers used by the editor. |
|
|
212
|
+
| `landmarks`, `landmarkById` | The landmark catalog. |
|
|
213
|
+
| `expandLabels`, `labelSequence`, `SPANISH_ALPHABET` | Row and seat numbering. |
|
|
214
|
+
|
|
215
|
+
Types: `Layout`, `Element`, `SeatBlock`, `Table`, `Zone`, `Landmark`, `PlaceTypeOption`,
|
|
216
|
+
`PlaceTypeAccepts`, `SellableUnit`, `PlanSummary`, `LockViolation`, `CapacityViolation`.
|
|
217
|
+
|
|
218
|
+
### `<VenueDesigner />`
|
|
219
|
+
|
|
220
|
+
| Prop | Type | |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `layout` | `Layout \| null` | The plan to edit; `null` starts a blank plan. A new object reloads the editor (memoize it); passing back what came from `onChange` is safe. |
|
|
223
|
+
| `name` | `string` | Name for a blank plan. |
|
|
224
|
+
| `placeTypes` | `PlaceTypeOption[]` | Ticket types that can be placed. They may change while the editor is open. |
|
|
225
|
+
| `lockedUnitIds` | `ReadonlySet<string>` | Sold or held units. |
|
|
226
|
+
| `onChange` | `(layout) => void` | Called on every change. |
|
|
227
|
+
| `onDone` | `(layout) => void \| Promise<void>` | The "Done" button. Without it, a "Save" button calls `onSave`. |
|
|
228
|
+
| `onClose` | `() => void` | The "Back" button (hidden without it). |
|
|
229
|
+
| `background` | `{ onPick: () => Promise<string \| null>; suggestedSrc?: string \| null }` | You upload the image and return its URL. |
|
|
230
|
+
| `sidePanel` | `(api: DesignerHostApi) => ReactNode` | Replaces the built-in ticket-type panel with your own (for example, ticket types with their prices). `api` offers `addPiece(kind, ref)`, `placed(ref)`, `nextPieceUnits(ref)`, `overCapacity(ref)`, `trimToCapacity(ref)`, `select(id)`, `reveal(id)`, `selectedRef`, `summary`… Keep the function stable (`useCallback`). |
|
|
231
|
+
| `theme` | `"light" \| "dark"` | Default token values. |
|
|
232
|
+
| `messages` | `DesignerOverrides` | `designerEn`, `designerEs`, or partial overrides via `mergeDesignerMessages`. |
|
|
233
|
+
|
|
234
|
+
Gestures: drag to move; corner handles to resize; Shift/⌘/Ctrl-click or drag on empty space to
|
|
235
|
+
multi-select; Space + drag, middle button or wheel to pan and zoom; Alt/Option disables
|
|
236
|
+
snapping. On touch: drag pans, long-press and drag selects. Arrow keys move the selection.
|
|
237
|
+
Below 768 px of container width the editor switches to bottom sheets.
|
|
238
|
+
|
|
239
|
+
### `<SeatMap />`
|
|
240
|
+
|
|
241
|
+
| Prop | Type | |
|
|
242
|
+
| --- | --- | --- |
|
|
243
|
+
| `layout` | `Layout` | |
|
|
244
|
+
| `placeTypes` | `{ ref, label, color, priceLabel? }[]` | Pass every type on the plan; pieces of unknown types render muted. |
|
|
245
|
+
| `focusRef` | `string` | The section the buyer is choosing in: the map zooms there and dims the rest. |
|
|
246
|
+
| `takenUnitIds` | `string[]` | Sold, plus held by other buyers. |
|
|
247
|
+
| `heldUnitIds` | `string[]` | Held for this buyer. |
|
|
248
|
+
| `selection`, `onSelectionChange` | `string[]`, `(ids) => void` | Controlled selection. |
|
|
249
|
+
| `maxSelection` | `number` | |
|
|
250
|
+
| `conflictUnitIds` | `string[]` | IDs lost to another buyer (for example after a 409): marked sold and announced. |
|
|
251
|
+
| `totalLabel`, `continueLabel`, `onContinue`, `holdMinutes` | | The bottom bar. |
|
|
252
|
+
| `embedded`, `onBack`, `onFocusChange`, `height` | | For use inside a card (see below). |
|
|
253
|
+
| `fill` | `boolean` | The map is the whole page. |
|
|
254
|
+
| `messages` | `MessageOverrides` | `en`, `es`, or partial overrides via `mergeMessages`. |
|
|
255
|
+
|
|
256
|
+
### `<VenueOverview />`
|
|
257
|
+
|
|
258
|
+
The whole venue for buyers. Tapping a zone or any unit of a type calls `onSelect(ref)`.
|
|
259
|
+
Props: `layout`, `placeTypes` (`{ ref, label, color, priceLabel?, available? }`; `available:
|
|
260
|
+
false` shows "Sold out"), `onSelect`, `hideList`, `showHint`, `selectedRefs`,
|
|
261
|
+
`takenUnitIds`, `selectedUnitIds`, `height`, `messages`.
|
|
262
|
+
|
|
263
|
+
### Buyer map in a card
|
|
236
264
|
|
|
237
265
|
```tsx
|
|
238
266
|
{focus === null ? (
|
|
239
267
|
<VenueOverview
|
|
240
268
|
layout={layout}
|
|
241
|
-
placeTypes={types}
|
|
242
|
-
onSelect={(ref) => (isZone(ref) ?
|
|
243
|
-
hideList
|
|
244
|
-
showHint
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
269
|
+
placeTypes={types}
|
|
270
|
+
onSelect={(ref) => (isZone(ref) ? addOneTicket(ref) : setFocus(ref))}
|
|
271
|
+
hideList
|
|
272
|
+
showHint
|
|
273
|
+
takenUnitIds={taken}
|
|
274
|
+
selectedUnitIds={selection}
|
|
275
|
+
messages={en}
|
|
248
276
|
/>
|
|
249
277
|
) : (
|
|
250
278
|
<SeatMap
|
|
251
|
-
embedded
|
|
252
|
-
onBack={() => setFocus(null)}
|
|
253
|
-
onFocusChange={setFocus}
|
|
254
|
-
layout={layout}
|
|
255
|
-
|
|
279
|
+
embedded
|
|
280
|
+
onBack={() => setFocus(null)}
|
|
281
|
+
onFocusChange={setFocus}
|
|
282
|
+
layout={layout}
|
|
283
|
+
placeTypes={types}
|
|
284
|
+
focusRef={focus}
|
|
285
|
+
takenUnitIds={taken}
|
|
286
|
+
selection={selection}
|
|
287
|
+
onSelectionChange={setSelection}
|
|
288
|
+
messages={en}
|
|
256
289
|
/>
|
|
257
290
|
)}
|
|
258
291
|
```
|
|
259
292
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
lienzo lee además `--vk-shape`, `--vk-shape-fill`, `--vk-unassigned`, `--vk-taken`… La
|
|
283
|
-
fuente es la de `--vk-font`: pasale la del host.
|
|
284
|
-
|
|
285
|
-
## Qué es del host
|
|
286
|
-
|
|
287
|
-
La librería dibuja y valida el plano; no sabe de plata ni de ventas. El host se ocupa de:
|
|
288
|
-
|
|
289
|
-
- **Subir imágenes**: `background.onPick` devuelve la URL.
|
|
290
|
-
- **Precios**: entran como texto ya armado (`priceLabel`, `totalLabel`).
|
|
291
|
-
- **Guardar los planos por nombre** y devolverlos al editor.
|
|
292
|
-
- **Saber qué está vendido o retenido**: en el mapa, `takenUnitIds` (vendido + retenido por
|
|
293
|
-
otros) y `heldUnitIds` (retenido para este comprador); en el editor, `lockedUnitIds`
|
|
294
|
-
(vendido + retenido por cualquiera). Y volver a validar en el servidor con `findLockViolations`.
|
|
295
|
-
- **No publicar un plano con nombres repetidos**: `summarize(layout).byRef[ref].duplicates`.
|
|
296
|
-
- **Re-enlazar un plano guardado** al evento nuevo con `rebindPlaceTypes` antes de mostrarlo.
|
|
297
|
-
|
|
298
|
-
## Notas para el host
|
|
299
|
-
|
|
300
|
-
- **El editor necesita alto**: ocupa el de su contenedor, así que el contenedor tiene que
|
|
301
|
-
tener uno (`height: 100dvh`, un flex que crezca…). Sin alto, no se ve.
|
|
302
|
-
- **`layout` y `onChange` son seguros de devolver**: si pasás por `layout` lo que salió de
|
|
303
|
-
`onChange` (setState y de vuelta como prop) el editor lo reconoce y no lo trata como un
|
|
304
|
-
plano nuevo. Un objeto distinto sí se carga (si no toca lo vendido), y se puede deshacer:
|
|
305
|
-
por eso un `layout` armado de nuevo en cada render, aunque sea igual, recarga el editor.
|
|
306
|
-
Usá `useMemo` (ejemplo arriba).
|
|
307
|
-
- **Los ids en conflicto quedan vendidos**: lo que llega en `conflictUnitIds` se trata como
|
|
308
|
-
vendido mientras el mapa siga montado, aunque el host después lo libere.
|
|
309
|
-
|
|
310
|
-
## Desarrollo
|
|
311
|
-
|
|
312
|
-
Requiere Node ≥ 20.
|
|
293
|
+
## Theming
|
|
294
|
+
|
|
295
|
+
Everything is styled with CSS custom properties on `.vk-root`, declared with `:where()` so
|
|
296
|
+
any rule of yours wins: `--vk-accent`, `--vk-bg`, `--vk-fg`, `--vk-line`, `--vk-muted`,
|
|
297
|
+
`--vk-surface`, `--vk-radius`, `--vk-font`… The buyer map adds `--vk-mine`, `--vk-sold`,
|
|
298
|
+
`--vk-unit-fg`, `--vk-landmark`…; the editor adds `--vk-panel`, `--vk-canvas`,
|
|
299
|
+
`--vk-primary-bg` and more. Set `--vk-font` to your app's font.
|
|
300
|
+
|
|
301
|
+
## What your app is responsible for
|
|
302
|
+
|
|
303
|
+
VenueKit draws and validates plans. It does not know about money or orders. Your app:
|
|
304
|
+
|
|
305
|
+
- stores plans and ticket types, and uploads background images;
|
|
306
|
+
- formats prices (`priceLabel`, `totalLabel`);
|
|
307
|
+
- tracks sold and held units, passes them to the editor and the map, and re-checks
|
|
308
|
+
`findLockViolations` on the server before saving a plan;
|
|
309
|
+
- holds seats during checkout and resolves conflicts between buyers;
|
|
310
|
+
- refuses to publish a plan with duplicated seat names (`summarize(layout).byRef[ref].duplicates`).
|
|
311
|
+
|
|
312
|
+
## Development
|
|
313
|
+
|
|
314
|
+
Requires Node ≥ 20.
|
|
313
315
|
|
|
314
316
|
```bash
|
|
315
317
|
npm install
|
|
316
|
-
npm test
|
|
318
|
+
npm test # unit tests (Vitest)
|
|
317
319
|
npm run typecheck
|
|
318
|
-
npm run build
|
|
319
|
-
npm run dev -w playground
|
|
320
|
-
npx playwright install chromium webkit #
|
|
321
|
-
npm run test:e2e
|
|
320
|
+
npm run build # builds packages/venuekit/dist
|
|
321
|
+
npm run dev -w playground # http://localhost:5173 (/ buyer map, /?designer editor)
|
|
322
|
+
npx playwright install chromium webkit # once
|
|
323
|
+
npm run test:e2e # browser tests (desktop and mobile)
|
|
322
324
|
```
|
|
323
325
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
326
|
+
`packages/core`, `packages/canvas` and `packages/react` are internal; `packages/venuekit`
|
|
327
|
+
bundles them into the published package. A Spanish reference with every detail lives in
|
|
328
|
+
[`docs/reference.es.md`](./docs/reference.es.md).
|
|
329
|
+
|
|
330
|
+
## License
|
|
331
|
+
|
|
332
|
+
[MIT](./LICENSE) © Juan Cruz Fernández
|