@seatsyoo/vue 0.0.0-stage → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tikyoo, S.L.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,366 @@
1
- # Temporary Holding Version
1
+ # @seatsyoo/vue
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ El visor de mapas de asientos de **Seatsyoo** como componente de Vue 3: el comprador ve el plano del recinto,
4
+ elige sus butacas (quedan retenidas a su nombre mientras paga) y ve en tiempo real lo que otros compran. Usa el
5
+ visor de [`@seatsyoo/visor`](https://www.npmjs.com/package/@seatsyoo/visor) y trae sus tipos de TypeScript.
6
+
7
+ Funciona con **Vue 3.3 o posterior** y con renderizado en el servidor (Nuxt, Vite SSR…). No necesita compilar
8
+ plantillas: está escrito con `defineComponent` y una función `render`.
9
+
10
+ *(English summary below.)*
11
+
12
+ ## Instalación
13
+
14
+ ```sh
15
+ npm install @seatsyoo/vue
16
+ # o: pnpm add @seatsyoo/vue · yarn add @seatsyoo/vue
17
+ ```
18
+
19
+ `vue` lo pones tú (es una dependencia «peer»); `@seatsyoo/visor` se instala solo.
20
+
21
+ ## Uso
22
+
23
+ ```vue
24
+ <script setup lang="ts">
25
+ import { SeatsyooChart, type SelectedObject } from "@seatsyoo/vue";
26
+ import { ref } from "vue";
27
+
28
+ const plano = ref<InstanceType<typeof SeatsyooChart> | null>(null);
29
+ const seleccion = ref<SelectedObject[]>([]);
30
+
31
+ async function comprar() {
32
+ // A tu servidor, solo el evento y el token de retención, nunca la lista de lo elegido: pregunta a Seatsyoo qué
33
+ // retiene ese token, cobra eso y, con el pago confirmado, lo reserva (ver «Vender»).
34
+ await fetch("/caja", {
35
+ method: "POST",
36
+ headers: { "content-type": "application/json" },
37
+ body: JSON.stringify({ evento: "hamlet-2026-11-20", holdToken: plano.value?.chart?.holdToken }),
38
+ });
39
+ }
40
+
41
+ function tiempoAgotado() {
42
+ alert("Se ha acabado el tiempo: vuelve a elegir.");
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <!-- El visor ocupa todo el <div>: dale altura. -->
48
+ <SeatsyooChart
49
+ ref="plano"
50
+ server="https://seatsyoo.example"
51
+ public-key="pk_tu_clave_publica"
52
+ event="hamlet-2026-11-20"
53
+ :pricing="{ platea: 30, anfiteatro: 22 }"
54
+ style="height: 600px"
55
+ @change="(elegidas: SelectedObject[]) => (seleccion = elegidas)"
56
+ @hold-expired="tiempoAgotado"
57
+ />
58
+ <button :disabled="seleccion.length === 0" @click="comprar">Comprar</button>
59
+ </template>
60
+ ```
61
+
62
+ Para tenerlo en todas las plantillas sin importarlo, registra el plugin:
63
+
64
+ ```ts
65
+ import { SeatsyooPlugin } from "@seatsyoo/vue";
66
+
67
+ createApp(App).use(SeatsyooPlugin).mount("#app");
68
+ ```
69
+
70
+ Para probarlo en un proyecto nuevo de Vite:
71
+
72
+ ```sh
73
+ npm create vite@latest entradas -- --template vue-ts
74
+ cd entradas
75
+ npm install @seatsyoo/vue
76
+ # pon el ejemplo de arriba en src/App.vue y arranca con:
77
+ npm run dev
78
+ ```
79
+
80
+ ### Props
81
+
82
+ Todas las opciones del visor que no son avisos (`server`, `public-key`, `event`, `season`, `link`, `document`,
83
+ `language`, `theme`, `max-selected`, `pricing`, `channels`, `hold-token`, `mode`, `status-styles`, `marks`, `view3d`…), con los mismos
84
+ nombres y tipos que en `@seatsyoo/visor` (en las plantillas, en camelCase o en kebab-case), salvo `container`, que
85
+ es el propio componente. `class`, `style`, `id` y el resto de atributos pasan al `<div>` que lo contiene. Los números
86
+ y booleanos, con `:` (`:max-selected="6"`, `:view3d="false"`).
87
+
88
+ Cómo reacciona a los cambios de props:
89
+
90
+ | Qué cambia | Qué pasa |
91
+ | --- | --- |
92
+ | `server`, `publicKey`, `event`, `link`, `document`, `season`, `seasonEvents`, `channels`, `pricing`, `currency`, `locale`, `language`, `maxSelected`, `session`, `holdToken`, `mode`, `showLegend`, `showSelectionBar`, `floor`, `sectors`, `view3d`, `entrance`, `texts` | Se desmonta el visor y se monta otro con lo nuevo. Los arrays y objetos se comparan **por su valor**: `:channels="['web']"` no lo vuelve a montar en cada render, y cambiar un array reactivo en el sitio (`canales.push("taquilla")`) sí. |
93
+ | `theme`, `colorBy` | Se aplica al visor montado (`setTheme`, `setColorBy`), sin volver a montarlo. |
94
+ | `statusStyles`, `marks` | Solo con `mode="manager"` (un panel de gestión): cómo se ve cada estado y las marcas sobre plazas libres (ver «Estados propios y marcas»). Se aplican al visor montado (`setStatusStyles`, `setMarks`), sin volver a montarlo, solo si cambia su valor: se comparan como datos (da igual el prototipo, y una clave que vale `undefined` es como si no estuviera), así que un objeto nuevo con lo mismo no hace nada, y cambiar uno reactivo en el sitio (`marcas["Platea-A-1"] = "reducida"`) sí cuenta. Quitarlas es dejar el visor sin estilos ni marcas. |
95
+ | `load3d` | Se usa la última función; ponerlo o quitarlo vuelve a montar. |
96
+
97
+ El plano (`document`) no se copia, porque puede ser muy grande: un plano nuevo con lo mismo no vuelve a montar el
98
+ visor, pero si cambias el que ya pasaste (en el sitio), no se nota, ni aunque después pases una copia. Pasa un plano
99
+ nuevo sin tocar el anterior.
100
+
101
+ **Lo que el comprador tiene retenido al volver a montar.** Si es en el mismo evento y con `session: "continue"` (por
102
+ defecto), el visor nuevo lo recupera. Si el nuevo no puede recuperarlo (has cambiado de evento o de temporada, de
103
+ `server` o de `publicKey`, usas `session: "start"`, le pasas un `holdToken` o el modo `"manager"`), el componente lo
104
+ suelta antes, como `clearSelection()`: se emiten `deselect` y `change` con la selección vacía, y esas butacas quedan
105
+ libres para los demás en lugar de bloqueadas hasta que caduque la retención. Así, para cambiar de función basta con
106
+ cambiar `event`.
107
+
108
+ Lo retenido con un `holdToken` tuyo (creado por tu servidor) no se suelta nunca: es de tu token, y tu servidor lo
109
+ reserva o lo suelta. Es la forma de llevar un carrito con varias funciones. Al desmontar el componente (por ejemplo,
110
+ al pasar a la página de pago) tampoco se suelta nada: tu servidor lo reserva con el token, o el comprador lo
111
+ recupera al volver.
112
+
113
+ ### Vender
114
+
115
+ A tu servidor la página le manda el evento y el `holdToken`, nada más: nunca la lista de lo elegido. En la caja, tu
116
+ servidor pregunta a Seatsyoo qué retiene ese token (`GET /api/v1/events/{evento}/statuses?holdToken=…`: lo suyo lleva
117
+ `mine: true` y, en una zona de pie, `mq` con las plazas del token; `mq` solo viene si en la zona hay más de una plaza
118
+ ocupada, contando las de otros compradores, y si no viene es 1), calcula el importe con sus precios (precio × plazas:
119
+ una mesa entera cuenta todas sus sillas, como el total del visor) y guarda esa lista con el pedido, cada objeto con su
120
+ cantidad (`{ key, quantity: mq ?? 1 }`). Con el pago confirmado, reserva exactamente lo guardado
121
+ (`POST /api/v1/events/{evento}/actions/book` con `holdToken` y tu número de pedido, `orderId`), sin volver a preguntar.
122
+
123
+ No reserves una lista que mande la página: `book` reserva también objetos libres, así que cualquiera podría añadirle
124
+ butacas que no ha retenido, y saltarse los topes de retención. Es lo que hace la tienda de ejemplo de `@seatsyoo/node`
125
+ (`ejemplo/tienda.ts`: `POST /caja` con `{ evento, holdToken }` y `POST /pagar` con `{ holdToken }`) y lo que cuenta la
126
+ guía de integración de la documentación (**/docs/integracion**).
127
+
128
+ ### Precios
129
+
130
+ Seatsyoo no guarda precios: los pone tu web. Pásalos con `pricing`, por clave de categoría (`{ platea: 30 }`), con
131
+ `currency` y `locale` para escribirlos: el visor los enseña en la ficha de cada butaca, en la leyenda y en el total,
132
+ y salen en `price` de cada objeto de `change`, `select`… (sin `pricing`, `price` es `null`). `price` es lo que cuesta
133
+ cada unidad de `quantity` (por plaza; una mesa entera, con todas sus sillas), así que `price × quantity` es lo que suma
134
+ el total del visor. Cambiar `pricing` vuelve a montar el visor (en el mismo evento, el comprador no pierde lo elegido).
135
+ Lo que se cobra lo calcula tu servidor con sus precios, no con lo que diga la página.
136
+
137
+ ### Eventos
138
+
139
+ | Evento | Con qué | Cuándo |
140
+ | --- | --- | --- |
141
+ | `ready` | el visor (`SeatingChart`) | Ha cargado el plano y los estados. |
142
+ | `change` | la selección (`SelectedObject[]`) | La selección ha cambiado. |
143
+ | `select`, `deselect` | el objeto (`SelectedObject`) | Se ha elegido o soltado un objeto. |
144
+ | `hold-expired` | — | Se acabó el tiempo de la retención. |
145
+ | `hold-token-change` | `{ token, expiresAt }` o `null` | El token de retención ha cambiado. |
146
+ | `floor-change` | `{ id, name }` | Se muestra otra planta. |
147
+ | `error` | `{ code, message }` | Algo ha fallado, o se ha llegado a un tope de retención (ver «Topes de retención»). Sin oyente de `@error`, se escribe en la consola. |
148
+
149
+ Los eventos llegan siempre a los oyentes de ese momento: cambiarlos no vuelve a montar el visor. En TypeScript (y
150
+ en las funciones `render`), los oyentes son `onReady`, `onChange`, `onHoldExpired`, `onHoldTokenChange`…
151
+
152
+ ### Lo que expone
153
+
154
+ Con una ref de plantilla, `chart` es el visor montado (un `SeatingChart`): `getSelection()`, `holdToken`,
155
+ `holdExpiresAt`, `clearSelection()`, `selectBestAvailable(n)`, `setFloor(id)`, `show3d()`, `zoomToFit()`,
156
+ `refresh()`… Vale `null` mientras no hay visor: en el servidor, antes de montarlo y después de desmontarlo. Si
157
+ cambia una opción que lo vuelve a montar, `chart` pasa a ser el visor nuevo.
158
+
159
+ ### Errores
160
+
161
+ Lo que falla al cargar o al retener llega a `@error` como `{ code, message }`. Si faltan opciones para montarlo
162
+ (qué enseñar, la clave pública…), el componente no lanza: emite `error` con `code: "invalid-options"` (sin oyente,
163
+ lo escribe en la consola) y lo monta en cuanto las opciones valen.
164
+
165
+ ### Topes de retención
166
+
167
+ Lo que el comprador retiene desde el visor tiene topes en el servidor. Al pasar uno, la butaca no se retiene y llega
168
+ el error a `@error`:
169
+
170
+ | Código | Qué tope |
171
+ | --- | --- |
172
+ | `hold-limit` (409) | Las «Plazas por comprador» de tu espacio (`maxHoldPerToken`, 10 si no se cambia; lo da `GET /workspace`), por token y evento. Una mesa o un palco que se venden enteros cuentan todas sus plazas. |
173
+ | `hold-limit-ip` (409) | Lo retenido a la vez desde una misma conexión (IP) en un evento, sumando todas sus compras en curso: el 10 % del aforo, hasta 200, y nunca menos que `maxHoldPerToken`. |
174
+ | `hold-rate-limited` (429) | Lo que retiene una misma IP en poco tiempo: como mucho 600 plazas cada 10 minutos (soltar no cuenta). |
175
+
176
+ - `max-selected` cuenta una mesa o un palco enteros como 1, y el servidor, con todas sus plazas: con
177
+ `max-selected` y `maxHoldPerToken` en 10, dos mesas de 8 dan `hold-limit`. No pongas `max-selected` por encima de
178
+ `maxHoldPerToken` y, si vendes mesas o palcos enteros, cuenta con ese error o sube el ajuste.
179
+ - `selectBestAvailable(n)` pide de 1 a 100 plazas, y nunca más que `maxHoldPerToken` (si no, `hold-limit` antes de
180
+ buscar).
181
+ - Un `holdToken` que crea tu servidor con la clave secreta (`POST /api/v1/hold-tokens`) no lleva IP: no cuenta para
182
+ `hold-limit-ip`. Lo que el comprador retiene con él desde el visor sí cuenta para `hold-limit` y `hold-rate-limited`.
183
+ - Lo que hace tu servidor con la clave secreta (retener, reservar) no tiene ninguno de estos topes.
184
+
185
+ ### Estados propios y marcas (modo «manager»)
186
+
187
+ En un panel de gestión (modo `"manager"`), `statusStyles` dice cómo se ve cada estado y `marks`, qué plazas libres
188
+ llevan una marca. Se sacan de la API con la clave secreta, así que los pide tu servidor y se los da a la página:
189
+
190
+ - `marks` es tal cual el `marks` de `GET /api/v1/events/{evento}/marks` (`{ "marks": { "Platea-A-1":
191
+ "visibilidad-reducida" } }`): objeto → clave del estado.
192
+ - `statusStyles` sale del catálogo de `GET /api/v1/statuses` (`{ statuses, system }`; con `?event={evento}`, solo los
193
+ que valen para ese evento), pero no es el mismo formato: por la clave de cada estado, `{ name, color, icon }`, y el
194
+ icono cambia de forma. `{ type: "text", text }` pasa a `{ text }` y `{ type: "image", src }`, a `{ image: src }`;
195
+ `{ type: "preset", name }` es de la colección del panel, que no se publica: sin icono, o con un trazado tuyo de 24×24
196
+ (`{ path }`). De `system` (los estados de base que tu espacio ha cambiado), solo los que traen `color`, y nunca
197
+ `free`. Un estado de base con `color: null` no pasa, así que el visor lo pinta como siempre y también pierde el
198
+ nombre y el icono propios que le hayas puesto, aunque el panel sí los enseñe (en el modo «manager», «Reservado» en
199
+ vez de tu «Vendida»). Si los quieres, ponle color propio en el panel. Pasar el catálogo tal cual no compila en
200
+ TypeScript y, en JavaScript, rompe el pintado.
201
+
202
+ ```ts
203
+ import type { StatusStyle } from "@seatsyoo/vue";
204
+
205
+ /** Un icono del catálogo de estados (GET /api/v1/statuses). */
206
+ type IconoDeEstado = { type: "preset"; name: string } | { type: "text"; text: string } | { type: "image"; src: string };
207
+ /** Lo que responde GET /api/v1/statuses (lo que hace falta aquí). */
208
+ interface Catalogo {
209
+ statuses: { key: string; name: string; color: string; icon: IconoDeEstado | null }[];
210
+ system: { key: string; name: string | null; color: string | null; icon: IconoDeEstado | null }[];
211
+ }
212
+
213
+ function estilo(name: string | null, color: string, icon: IconoDeEstado | null): StatusStyle {
214
+ // «preset»: sin icono (o tu trazado de 24×24, { path: "M4 12h16" }).
215
+ const forma = icon?.type === "text" ? { text: icon.text } : icon?.type === "image" ? { image: icon.src } : null;
216
+ return { ...(name ? { name } : {}), color, ...(forma ? { icon: forma } : {}) };
217
+ }
218
+
219
+ /** De GET /api/v1/statuses a la opción statusStyles. */
220
+ export function estilosDeEstados({ statuses, system }: Catalogo): Record<string, StatusStyle> {
221
+ const estilos: Record<string, StatusStyle> = {};
222
+ for (const s of statuses) estilos[s.key] = estilo(s.name, s.color, s.icon);
223
+ // De los de base, solo los que tienen color propio: sin él se quedan como los pinta el visor, también con su nombre
224
+ // y su icono de siempre (los tuyos se pierden). «free» no se pinta con estilo.
225
+ for (const s of system) if (s.key !== "free" && s.color) estilos[s.key] = estilo(s.name, s.color, s.icon);
226
+ return estilos;
227
+ }
228
+ ```
229
+
230
+ Después, `<SeatsyooChart mode="manager" … :status-styles="estilos" :marks="marcas" />`, con `estilos =
231
+ estilosDeEstados(catalogo)` y las marcas tal cual (también reactivas, cambiadas en el sitio).
232
+
233
+ ### Renderizado en el servidor
234
+
235
+ El visor se monta en `onMounted`: en el servidor el componente pinta solo un `<div>` vacío y en el navegador se
236
+ hidrata y monta el visor. En Nuxt funciona tal cual (también dentro de `<ClientOnly>`, si lo prefieres).
237
+
238
+ ### La vista 3D
239
+
240
+ No va en el paquete: el visor la descarga de `${server}/sdk/butacas-3d.js` cuando alguien la abre. Si tu web
241
+ tiene una política de seguridad de contenidos (CSP), añade esa dirección a `script-src`, o pasa `:view3d="false"`.
242
+
243
+ ## El diseñador de planos
244
+
245
+ `<SeatsyooDesigner>` abre en tu web el diseñador de planos de Seatsyoo, para que tus clientes dibujen sus recintos.
246
+ Tu servidor abre una sesión para un plano con su clave secreta (`POST /api/v1/charts/{plano}/designer-sessions`, o
247
+ `charts.designerSession` de `@seatsyoo/node`) y le da el token a tu web:
248
+
249
+ ```vue
250
+ <script setup lang="ts">
251
+ import { SeatsyooDesigner } from "@seatsyoo/vue";
252
+
253
+ defineProps<{ token: string }>();
254
+ const emit = defineEmits<{ cerrar: [] }>();
255
+ // Tu servidor abre otra sesión del mismo plano y te da el token (aquí, una ruta tuya).
256
+ const pedirToken = (): Promise<string> =>
257
+ fetch("/mi-api/disenador/teatro", { method: "POST" }).then((r) => r.json()).then((s: { token: string }) => s.token);
258
+ </script>
259
+
260
+ <template>
261
+ <SeatsyooDesigner
262
+ server="https://seatsyoo.example"
263
+ :token="token"
264
+ :refresh-token="pedirToken"
265
+ style="height: 80vh"
266
+ @publish="({ version }) => console.log('Publicada la versión', version)"
267
+ @close="emit('cerrar')"
268
+ />
269
+ </template>
270
+ ```
271
+
272
+ Props: `server`, `token`, `language` y `refreshToken` (pide a tu servidor otro token del mismo plano antes de que
273
+ caduque: sin ella, al caducar deja de guardar). Eventos: `ready`, `save`, `publish`, `close` («←» sale si hay quien
274
+ escuche `@close` al montarse) y `error`. `class`, `style` e `id` van al `<div>`, que necesita alto. Un `token` nuevo se le
275
+ manda sin volver a montarlo; cambiar `server` o `language`, o pasar o quitar `refreshToken`, lo vuelve a montar. Con
276
+ una ref de plantilla, `disenador.value.designer` es el diseñador (`setToken()`, `iframe`). `app.use(SeatsyooPlugin)`
277
+ también lo registra. En el servidor (SSR) pinta solo el `<div>`.
278
+
279
+ La referencia completa de opciones, avisos y métodos está en la documentación de Seatsyoo (**/docs/visor**, **/docs/disenador** y
280
+ **/docs/frameworks**).
281
+
282
+ ## Licencia
283
+
284
+ MIT (archivo `LICENSE`). El visor del que depende, `@seatsyoo/visor`, tiene su propia licencia: se usa gratis para
285
+ integrar Seatsyoo.
286
+
287
+ ---
288
+
289
+ ## English summary
290
+
291
+ `@seatsyoo/vue` is the **Seatsyoo** seating chart viewer as a Vue 3 component (Vue 3.3+, SSR-safe, no template
292
+ compiler needed). It wraps `@seatsyoo/visor` and ships TypeScript types.
293
+
294
+ ```sh
295
+ npm install @seatsyoo/vue
296
+ ```
297
+
298
+ ```vue
299
+ <SeatsyooChart
300
+ ref="chart"
301
+ server="https://seatsyoo.example"
302
+ public-key="pk_your_public_key"
303
+ event="hamlet-2026-11-20"
304
+ language="en"
305
+ style="height: 600px"
306
+ @change="(selection: SelectedObject[]) => (selected = selection)"
307
+ />
308
+ <!-- chart.value.chart is the mounted SeatingChart: at checkout, send only the event and its holdToken to your server
309
+ (never the selected keys). -->
310
+ ```
311
+
312
+ - **Embedded designer**: `<SeatsyooDesigner server :token :refresh-token @publish @close style="height: 80vh" />` opens
313
+ the Seatsyoo chart designer in your website, with a session token your server opens with its secret key
314
+ (`POST /api/v1/charts/{chart}/designer-sessions`).
315
+ - **Props**: every viewer option except `container` and the callbacks; `class`, `style`, `id` and other attributes
316
+ go to the wrapping `<div>`. `app.use(SeatsyooPlugin)` registers `<SeatsyooChart>` globally.
317
+ - **Events**: `ready`, `change`, `select`, `deselect`, `error`, `hold-expired`, `hold-token-change`,
318
+ `floor-change`. They always reach the current listeners; without an `@error` listener, errors go to the console.
319
+ - **Re-mounting**: changing `server`, `publicKey`, `event`, `link`, `document`, `season`, `seasonEvents`,
320
+ `channels`, `pricing`, `currency`, `locale`, `language`, `maxSelected`, `session`, `holdToken`, `mode`,
321
+ `showLegend`, `showSelectionBar`, `floor`, `sectors`, `view3d`, `entrance` or `texts` destroys the viewer and mounts a new one (arrays
322
+ and objects are compared by value). `theme` and `colorBy` are applied live (`setTheme`, `setColorBy`), and so
323
+ are `statusStyles` and `marks` (`"manager"` mode: `setStatusStyles`, `setMarks`), only when their value changes
324
+ (compared as data: the prototype doesn't matter and a key set to `undefined` counts as missing; a reactive object
325
+ changed in place counts too).
326
+ - **Prices**: Seatsyoo does not store prices. Pass yours with `pricing` (per category key, with `currency` and
327
+ `locale`): the viewer shows them and they come in `price` of each selected object (`null` without `pricing`).
328
+ `price` is what each unit of `quantity` costs (per place; a whole table, all its chairs), so `price × quantity` adds
329
+ up to the viewer's total.
330
+ - **Selling**: the page sends your server the event and the `holdToken`, nothing else. At checkout your server asks
331
+ Seatsyoo what that token holds (`GET /api/v1/events/{event}/statuses?holdToken=…`: `mine: true`, plus `mq` in a
332
+ standing area with the token's places, which only comes when more than one place is taken there, counting other
333
+ buyers'; when it is missing, it is 1), prices it (price × places: a whole table counts all its chairs, as in the
334
+ viewer's total) and stores that list with the order, each object with an explicit quantity
335
+ (`{ key, quantity: mq ?? 1 }`). Once paid, it books exactly that list (`POST …/actions/book` with
336
+ `holdToken` and your `orderId`). Never book a list sent by the page: `book` also books free objects, so anyone could
337
+ add seats they never held. The `@seatsyoo/node` example store (`ejemplo/tienda.ts`) does this; see
338
+ **/en/docs/integration**.
339
+ - **Hold limits** (errors in the `error` event): `hold-limit` (409, the workspace's "places per buyer",
340
+ `maxHoldPerToken`, 10 by default, per token and event; a whole table or box counts all its places), `hold-limit-ip` (409, per connection
341
+ and event: 10% of the capacity, up to 200, never below `maxHoldPerToken`) and `hold-rate-limited` (429, 600 places
342
+ held per IP every 10 minutes). `max-selected` counts a whole table or box as 1, so keep it at or below
343
+ `maxHoldPerToken` and expect `hold-limit` with whole tables. `selectBestAvailable(n)` takes 1 to 100 and never more
344
+ than `maxHoldPerToken`. A `holdToken` your server creates with the secret key has no IP, so it does not count for
345
+ `hold-limit-ip`, but what the buyer holds with it from the viewer still counts for `hold-limit` and
346
+ `hold-rate-limited`. Your server's own calls with the secret key have none of these limits.
347
+ - **Custom statuses and marks** (`"manager"` mode): `marks` is the `marks` of `GET /api/v1/events/{event}/marks` as
348
+ is. `statusStyles` is not the shape of `GET /api/v1/statuses`: per status key, `{ name, color, icon }` with the icon
349
+ converted (`text` → `{ text }`, `image` → `{ image: src }`, `preset` → no icon or your own 24×24 `{ path }`); from
350
+ `system`, only entries with a `color`, never `free` (one with `color: null` stays exactly as the viewer draws it:
351
+ its own name and icon are lost too, even if the panel shows them; give it its own colour if you want them). Both
352
+ need the secret key: your server fetches them. The `estilosDeEstados` function above does the conversion.
353
+ - **Holds on re-mount**: same event with `session: "continue"` (default) → the new viewer recovers the buyer's
354
+ held seats. If it can't (another event or season, `server`, `publicKey`, `session: "start"`, a `holdToken`,
355
+ `"manager"` mode), the component releases them first, like `clearSelection()` (`deselect` and an empty `change`
356
+ are emitted): switching shows is just changing `event`. Seats held with your own `holdToken` are never released
357
+ (your server books or releases them, e.g. a multi-show cart), and nothing is released on unmount.
358
+ - **`document`** is not copied: changing it in place goes unnoticed; pass a new object without mutating the old one.
359
+ - **Exposed**: `chart`, the mounted `SeatingChart` (or `null` on the server, before mounting and after unmounting).
360
+ - **Invalid options** don't throw: the `error` event gets `{ code: "invalid-options", message }`.
361
+ - **SSR**: renders an empty `<div>` on the server and mounts the viewer in `onMounted`.
362
+ - **3D view**: downloaded from `${server}/sdk/butacas-3d.js` when opened (allow it in your CSP `script-src`, or
363
+ pass `:view3d="false"`).
364
+ - Full reference: the Seatsyoo docs at **/en/docs/seat-map** and **/en/docs/frameworks**.
365
+
366
+ License: MIT. The viewer it depends on, `@seatsyoo/visor`, has its own license (free to use to integrate Seatsyoo).
@@ -0,0 +1,153 @@
1
+ import { type ChartDocument, type Language, type SeatingChart, type SelectedObject, type StatusStyle, type View3DModule } from "@seatsyoo/visor";
2
+ import { type ComponentOptionsMixin, type DefineComponent, type ExtractPublicPropTypes, type Plugin, type PropType } from "vue";
3
+ import { SeatsyooDesigner } from "./SeatsyooDesigner.js";
4
+ declare const props: {
5
+ /** URL base del servidor de Seatsyoo, p. ej. https://seatsyoo.example */
6
+ server: {
7
+ type: StringConstructor;
8
+ required: true;
9
+ };
10
+ /** Clave pública del espacio (obligatoria salvo con `link` o `document`). */
11
+ publicKey: StringConstructor;
12
+ /** Clave del evento. */
13
+ event: StringConstructor;
14
+ /** O la clave de un enlace público del plano: solo para ver. */
15
+ link: StringConstructor;
16
+ /** O el plano ya cargado: solo para ver. */
17
+ document: PropType<ChartDocument>;
18
+ /** O la clave de una temporada (abono). */
19
+ season: StringConstructor;
20
+ /** Abono parcial: los eventos de la temporada elegidos. */
21
+ seasonEvents: PropType<string[]>;
22
+ /** Canales de venta desde los que se compra. */
23
+ channels: PropType<string[]>;
24
+ /**
25
+ * Los precios que pone tu web, por clave de categoría (`{ platea: 30 }`): el visor los enseña en la ficha, la leyenda
26
+ * y el total, y salen en `price` de lo elegido. Seatsyoo no guarda precios.
27
+ */
28
+ pricing: PropType<Record<string, number>>;
29
+ /** Moneda de `pricing` (código ISO; si no, euros). */
30
+ currency: StringConstructor;
31
+ locale: StringConstructor;
32
+ language: PropType<Language>;
33
+ /** Máximo de plazas seleccionables (por defecto 10). */
34
+ maxSelected: NumberConstructor;
35
+ /** «continue» (por defecto) recupera la selección al recargar; «start» empieza de cero. */
36
+ session: PropType<"continue" | "start">;
37
+ /** Token de retención creado por tu servidor. */
38
+ holdToken: StringConstructor;
39
+ /** «buyer» (por defecto) o «manager». */
40
+ mode: PropType<"buyer" | "manager">;
41
+ /** Color base de los objetos (se aplica sin volver a montar). */
42
+ colorBy: PropType<"category" | "channel">;
43
+ /** Tema (se aplica sin volver a montar). */
44
+ theme: PropType<"light" | "dark" | "auto">;
45
+ /**
46
+ * Solo en el modo «manager»: nombre, color e icono de los estados, por su clave (los propios y los de base que se
47
+ * quieran cambiar). Se aplica sin volver a montar (setStatusStyles).
48
+ */
49
+ statusStyles: PropType<Record<string, StatusStyle>>;
50
+ /**
51
+ * Solo en el modo «manager»: marcas sobre plazas que siguen libres, objeto → estado (su aspecto, en `statusStyles`).
52
+ * Se aplica sin volver a montar (setMarks).
53
+ */
54
+ marks: PropType<Record<string, string>>;
55
+ showLegend: {
56
+ type: BooleanConstructor;
57
+ default: undefined;
58
+ };
59
+ showSelectionBar: {
60
+ type: BooleanConstructor;
61
+ default: undefined;
62
+ };
63
+ /** Planta que se muestra al abrir (id). */
64
+ floor: StringConstructor;
65
+ sectors: {
66
+ type: BooleanConstructor;
67
+ default: undefined;
68
+ };
69
+ /** Botón de vista 3D (por defecto sí). */
70
+ view3d: {
71
+ type: BooleanConstructor;
72
+ default: undefined;
73
+ };
74
+ /** Entrada animada al abrirse: el plano se descubre desde el escenario (por defecto, no). */
75
+ entrance: {
76
+ type: BooleanConstructor;
77
+ default: undefined;
78
+ };
79
+ /**
80
+ * Textos del plano que se enseñan cambiados (por ejemplo, traducidos), por el texto tal como está en el plano:
81
+ * `{ Platea: "Stalls" }`. Solo cambia lo que se ve: claves, selección y eventos siguen siendo los del plano.
82
+ */
83
+ texts: PropType<Record<string, string>>;
84
+ /** Cómo cargar el módulo 3D (por defecto se pide al servidor). */
85
+ load3d: PropType<() => Promise<View3DModule>>;
86
+ };
87
+ declare const emits: {
88
+ /** El visor ha cargado el plano y los estados. */
89
+ ready: (chart: SeatingChart) => boolean;
90
+ /** La selección ha cambiado. */
91
+ change: (selection: SelectedObject[]) => boolean;
92
+ select: (object: SelectedObject) => boolean;
93
+ deselect: (object: SelectedObject) => boolean;
94
+ /** Algo ha fallado: `{ code, message }`. Sin oyente, se escribe en la consola. */
95
+ error: (error: {
96
+ code: string;
97
+ message: string;
98
+ }) => boolean;
99
+ /** Se acabó el tiempo de la retención (`@hold-expired`). */
100
+ holdExpired: () => boolean;
101
+ /** El token de retención ha cambiado (`@hold-token-change`). */
102
+ holdTokenChange: (token: {
103
+ token: string;
104
+ expiresAt: Date;
105
+ } | null) => boolean;
106
+ /** Se muestra otra planta (`@floor-change`). */
107
+ floorChange: (floor: {
108
+ id: string;
109
+ name: string;
110
+ }) => boolean;
111
+ };
112
+ /** Props de SeatsyooChart: las opciones del visor, sin `container` ni los avisos (que son eventos). */
113
+ export type SeatsyooChartProps = ExtractPublicPropTypes<typeof props>;
114
+ /** Lo que expone el componente a una ref de plantilla. */
115
+ export interface SeatsyooChartExposed {
116
+ /** El visor montado (`getSelection()`, `holdToken`…), o null mientras no hay ninguno. */
117
+ chart: SeatingChart | null;
118
+ }
119
+ /**
120
+ * El tipo público del componente, escrito a mano con los parámetros de DefineComponent que no han cambiado desde
121
+ * Vue 3.3: el que deduce TypeScript lleva los de la versión con que se compila (ShallowRef<T, S>, más
122
+ * parámetros…), que no existen en las anteriores.
123
+ */
124
+ export type SeatsyooChartComponent = DefineComponent<typeof props, SeatsyooChartExposed, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, typeof emits>;
125
+ /**
126
+ * El visor de mapas de asientos de Seatsyoo.
127
+ *
128
+ * ```vue
129
+ * <SeatsyooChart
130
+ * ref="plano"
131
+ * server="https://seatsyoo.example"
132
+ * public-key="pk_tu_clave_publica"
133
+ * event="hamlet-2026-11-20"
134
+ * style="height: 600px"
135
+ * @change="(seleccion) => (elegidas = seleccion)"
136
+ * />
137
+ * ```
138
+ *
139
+ * Con una ref de plantilla, `plano.value.chart` es el visor montado (`getSelection()`, `holdToken`…), o `null`
140
+ * mientras no hay ninguno.
141
+ */
142
+ export declare const SeatsyooChart: SeatsyooChartComponent;
143
+ /** Plugin que registra <SeatsyooChart> y <SeatsyooDesigner> en toda la aplicación: `app.use(SeatsyooPlugin)`. */
144
+ export declare const SeatsyooPlugin: Plugin;
145
+ declare module "vue" {
146
+ interface GlobalComponents {
147
+ /** El visor de Seatsyoo (registrado con `app.use(SeatsyooPlugin)`). */
148
+ SeatsyooChart: typeof SeatsyooChart;
149
+ /** El diseñador de planos de Seatsyoo (registrado con `app.use(SeatsyooPlugin)`). */
150
+ SeatsyooDesigner: typeof SeatsyooDesigner;
151
+ }
152
+ }
153
+ export {};