@seatsyoo/vue 0.0.0-stage → 1.0.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,327 @@
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
+ La referencia completa de opciones, avisos y métodos está en la documentación de Seatsyoo (**/docs/visor** y
244
+ **/docs/frameworks**).
245
+
246
+ ## Licencia
247
+
248
+ MIT (archivo `LICENSE`). El visor del que depende, `@seatsyoo/visor`, tiene su propia licencia: se usa gratis para
249
+ integrar Seatsyoo.
250
+
251
+ ---
252
+
253
+ ## English summary
254
+
255
+ `@seatsyoo/vue` is the **Seatsyoo** seating chart viewer as a Vue 3 component (Vue 3.3+, SSR-safe, no template
256
+ compiler needed). It wraps `@seatsyoo/visor` and ships TypeScript types.
257
+
258
+ ```sh
259
+ npm install @seatsyoo/vue
260
+ ```
261
+
262
+ ```vue
263
+ <SeatsyooChart
264
+ ref="chart"
265
+ server="https://seatsyoo.example"
266
+ public-key="pk_your_public_key"
267
+ event="hamlet-2026-11-20"
268
+ language="en"
269
+ style="height: 600px"
270
+ @change="(selection: SelectedObject[]) => (selected = selection)"
271
+ />
272
+ <!-- chart.value.chart is the mounted SeatingChart: at checkout, send only the event and its holdToken to your server
273
+ (never the selected keys). -->
274
+ ```
275
+
276
+ - **Props**: every viewer option except `container` and the callbacks; `class`, `style`, `id` and other attributes
277
+ go to the wrapping `<div>`. `app.use(SeatsyooPlugin)` registers `<SeatsyooChart>` globally.
278
+ - **Events**: `ready`, `change`, `select`, `deselect`, `error`, `hold-expired`, `hold-token-change`,
279
+ `floor-change`. They always reach the current listeners; without an `@error` listener, errors go to the console.
280
+ - **Re-mounting**: changing `server`, `publicKey`, `event`, `link`, `document`, `season`, `seasonEvents`,
281
+ `channels`, `pricing`, `currency`, `locale`, `language`, `maxSelected`, `session`, `holdToken`, `mode`,
282
+ `showLegend`, `showSelectionBar`, `floor`, `sectors`, `view3d`, `entrance` or `texts` destroys the viewer and mounts a new one (arrays
283
+ and objects are compared by value). `theme` and `colorBy` are applied live (`setTheme`, `setColorBy`), and so
284
+ are `statusStyles` and `marks` (`"manager"` mode: `setStatusStyles`, `setMarks`), only when their value changes
285
+ (compared as data: the prototype doesn't matter and a key set to `undefined` counts as missing; a reactive object
286
+ changed in place counts too).
287
+ - **Prices**: Seatsyoo does not store prices. Pass yours with `pricing` (per category key, with `currency` and
288
+ `locale`): the viewer shows them and they come in `price` of each selected object (`null` without `pricing`).
289
+ `price` is what each unit of `quantity` costs (per place; a whole table, all its chairs), so `price × quantity` adds
290
+ up to the viewer's total.
291
+ - **Selling**: the page sends your server the event and the `holdToken`, nothing else. At checkout your server asks
292
+ Seatsyoo what that token holds (`GET /api/v1/events/{event}/statuses?holdToken=…`: `mine: true`, plus `mq` in a
293
+ standing area with the token's places, which only comes when more than one place is taken there, counting other
294
+ buyers'; when it is missing, it is 1), prices it (price × places: a whole table counts all its chairs, as in the
295
+ viewer's total) and stores that list with the order, each object with an explicit quantity
296
+ (`{ key, quantity: mq ?? 1 }`). Once paid, it books exactly that list (`POST …/actions/book` with
297
+ `holdToken` and your `orderId`). Never book a list sent by the page: `book` also books free objects, so anyone could
298
+ add seats they never held. The `@seatsyoo/node` example store (`ejemplo/tienda.ts`) does this; see
299
+ **/en/docs/integration**.
300
+ - **Hold limits** (errors in the `error` event): `hold-limit` (409, the workspace's "places per buyer",
301
+ `maxHoldPerToken`, 10 by default, per token and event; a whole table or box counts all its places), `hold-limit-ip` (409, per connection
302
+ and event: 10% of the capacity, up to 200, never below `maxHoldPerToken`) and `hold-rate-limited` (429, 600 places
303
+ held per IP every 10 minutes). `max-selected` counts a whole table or box as 1, so keep it at or below
304
+ `maxHoldPerToken` and expect `hold-limit` with whole tables. `selectBestAvailable(n)` takes 1 to 100 and never more
305
+ than `maxHoldPerToken`. A `holdToken` your server creates with the secret key has no IP, so it does not count for
306
+ `hold-limit-ip`, but what the buyer holds with it from the viewer still counts for `hold-limit` and
307
+ `hold-rate-limited`. Your server's own calls with the secret key have none of these limits.
308
+ - **Custom statuses and marks** (`"manager"` mode): `marks` is the `marks` of `GET /api/v1/events/{event}/marks` as
309
+ is. `statusStyles` is not the shape of `GET /api/v1/statuses`: per status key, `{ name, color, icon }` with the icon
310
+ converted (`text` → `{ text }`, `image` → `{ image: src }`, `preset` → no icon or your own 24×24 `{ path }`); from
311
+ `system`, only entries with a `color`, never `free` (one with `color: null` stays exactly as the viewer draws it:
312
+ its own name and icon are lost too, even if the panel shows them; give it its own colour if you want them). Both
313
+ need the secret key: your server fetches them. The `estilosDeEstados` function above does the conversion.
314
+ - **Holds on re-mount**: same event with `session: "continue"` (default) → the new viewer recovers the buyer's
315
+ held seats. If it can't (another event or season, `server`, `publicKey`, `session: "start"`, a `holdToken`,
316
+ `"manager"` mode), the component releases them first, like `clearSelection()` (`deselect` and an empty `change`
317
+ are emitted): switching shows is just changing `event`. Seats held with your own `holdToken` are never released
318
+ (your server books or releases them, e.g. a multi-show cart), and nothing is released on unmount.
319
+ - **`document`** is not copied: changing it in place goes unnoticed; pass a new object without mutating the old one.
320
+ - **Exposed**: `chart`, the mounted `SeatingChart` (or `null` on the server, before mounting and after unmounting).
321
+ - **Invalid options** don't throw: the `error` event gets `{ code: "invalid-options", message }`.
322
+ - **SSR**: renders an empty `<div>` on the server and mounts the viewer in `onMounted`.
323
+ - **3D view**: downloaded from `${server}/sdk/butacas-3d.js` when opened (allow it in your CSP `script-src`, or
324
+ pass `:view3d="false"`).
325
+ - Full reference: the Seatsyoo docs at **/en/docs/seat-map** and **/en/docs/frameworks**.
326
+
327
+ License: MIT. The viewer it depends on, `@seatsyoo/visor`, has its own license (free to use to integrate Seatsyoo).
@@ -0,0 +1,150 @@
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
+ declare const props: {
4
+ /** URL base del servidor de Seatsyoo, p. ej. https://seatsyoo.example */
5
+ server: {
6
+ type: StringConstructor;
7
+ required: true;
8
+ };
9
+ /** Clave pública del espacio (obligatoria salvo con `link` o `document`). */
10
+ publicKey: StringConstructor;
11
+ /** Clave del evento. */
12
+ event: StringConstructor;
13
+ /** O la clave de un enlace público del plano: solo para ver. */
14
+ link: StringConstructor;
15
+ /** O el plano ya cargado: solo para ver. */
16
+ document: PropType<ChartDocument>;
17
+ /** O la clave de una temporada (abono). */
18
+ season: StringConstructor;
19
+ /** Abono parcial: los eventos de la temporada elegidos. */
20
+ seasonEvents: PropType<string[]>;
21
+ /** Canales de venta desde los que se compra. */
22
+ channels: PropType<string[]>;
23
+ /**
24
+ * Los precios que pone tu web, por clave de categoría (`{ platea: 30 }`): el visor los enseña en la ficha, la leyenda
25
+ * y el total, y salen en `price` de lo elegido. Seatsyoo no guarda precios.
26
+ */
27
+ pricing: PropType<Record<string, number>>;
28
+ /** Moneda de `pricing` (código ISO; si no, euros). */
29
+ currency: StringConstructor;
30
+ locale: StringConstructor;
31
+ language: PropType<Language>;
32
+ /** Máximo de plazas seleccionables (por defecto 10). */
33
+ maxSelected: NumberConstructor;
34
+ /** «continue» (por defecto) recupera la selección al recargar; «start» empieza de cero. */
35
+ session: PropType<"continue" | "start">;
36
+ /** Token de retención creado por tu servidor. */
37
+ holdToken: StringConstructor;
38
+ /** «buyer» (por defecto) o «manager». */
39
+ mode: PropType<"buyer" | "manager">;
40
+ /** Color base de los objetos (se aplica sin volver a montar). */
41
+ colorBy: PropType<"category" | "channel">;
42
+ /** Tema (se aplica sin volver a montar). */
43
+ theme: PropType<"light" | "dark" | "auto">;
44
+ /**
45
+ * Solo en el modo «manager»: nombre, color e icono de los estados, por su clave (los propios y los de base que se
46
+ * quieran cambiar). Se aplica sin volver a montar (setStatusStyles).
47
+ */
48
+ statusStyles: PropType<Record<string, StatusStyle>>;
49
+ /**
50
+ * Solo en el modo «manager»: marcas sobre plazas que siguen libres, objeto → estado (su aspecto, en `statusStyles`).
51
+ * Se aplica sin volver a montar (setMarks).
52
+ */
53
+ marks: PropType<Record<string, string>>;
54
+ showLegend: {
55
+ type: BooleanConstructor;
56
+ default: undefined;
57
+ };
58
+ showSelectionBar: {
59
+ type: BooleanConstructor;
60
+ default: undefined;
61
+ };
62
+ /** Planta que se muestra al abrir (id). */
63
+ floor: StringConstructor;
64
+ sectors: {
65
+ type: BooleanConstructor;
66
+ default: undefined;
67
+ };
68
+ /** Botón de vista 3D (por defecto sí). */
69
+ view3d: {
70
+ type: BooleanConstructor;
71
+ default: undefined;
72
+ };
73
+ /** Entrada animada al abrirse: el plano se descubre desde el escenario (por defecto, no). */
74
+ entrance: {
75
+ type: BooleanConstructor;
76
+ default: undefined;
77
+ };
78
+ /**
79
+ * Textos del plano que se enseñan cambiados (por ejemplo, traducidos), por el texto tal como está en el plano:
80
+ * `{ Platea: "Stalls" }`. Solo cambia lo que se ve: claves, selección y eventos siguen siendo los del plano.
81
+ */
82
+ texts: PropType<Record<string, string>>;
83
+ /** Cómo cargar el módulo 3D (por defecto se pide al servidor). */
84
+ load3d: PropType<() => Promise<View3DModule>>;
85
+ };
86
+ declare const emits: {
87
+ /** El visor ha cargado el plano y los estados. */
88
+ ready: (chart: SeatingChart) => boolean;
89
+ /** La selección ha cambiado. */
90
+ change: (selection: SelectedObject[]) => boolean;
91
+ select: (object: SelectedObject) => boolean;
92
+ deselect: (object: SelectedObject) => boolean;
93
+ /** Algo ha fallado: `{ code, message }`. Sin oyente, se escribe en la consola. */
94
+ error: (error: {
95
+ code: string;
96
+ message: string;
97
+ }) => boolean;
98
+ /** Se acabó el tiempo de la retención (`@hold-expired`). */
99
+ holdExpired: () => boolean;
100
+ /** El token de retención ha cambiado (`@hold-token-change`). */
101
+ holdTokenChange: (token: {
102
+ token: string;
103
+ expiresAt: Date;
104
+ } | null) => boolean;
105
+ /** Se muestra otra planta (`@floor-change`). */
106
+ floorChange: (floor: {
107
+ id: string;
108
+ name: string;
109
+ }) => boolean;
110
+ };
111
+ /** Props de SeatsyooChart: las opciones del visor, sin `container` ni los avisos (que son eventos). */
112
+ export type SeatsyooChartProps = ExtractPublicPropTypes<typeof props>;
113
+ /** Lo que expone el componente a una ref de plantilla. */
114
+ export interface SeatsyooChartExposed {
115
+ /** El visor montado (`getSelection()`, `holdToken`…), o null mientras no hay ninguno. */
116
+ chart: SeatingChart | null;
117
+ }
118
+ /**
119
+ * El tipo público del componente, escrito a mano con los parámetros de DefineComponent que no han cambiado desde
120
+ * Vue 3.3: el que deduce TypeScript lleva los de la versión con que se compila (ShallowRef<T, S>, más
121
+ * parámetros…), que no existen en las anteriores.
122
+ */
123
+ export type SeatsyooChartComponent = DefineComponent<typeof props, SeatsyooChartExposed, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, typeof emits>;
124
+ /**
125
+ * El visor de mapas de asientos de Seatsyoo.
126
+ *
127
+ * ```vue
128
+ * <SeatsyooChart
129
+ * ref="plano"
130
+ * server="https://seatsyoo.example"
131
+ * public-key="pk_tu_clave_publica"
132
+ * event="hamlet-2026-11-20"
133
+ * style="height: 600px"
134
+ * @change="(seleccion) => (elegidas = seleccion)"
135
+ * />
136
+ * ```
137
+ *
138
+ * Con una ref de plantilla, `plano.value.chart` es el visor montado (`getSelection()`, `holdToken`…), o `null`
139
+ * mientras no hay ninguno.
140
+ */
141
+ export declare const SeatsyooChart: SeatsyooChartComponent;
142
+ /** Plugin que registra <SeatsyooChart> en toda la aplicación: `app.use(SeatsyooPlugin)`. */
143
+ export declare const SeatsyooPlugin: Plugin;
144
+ declare module "vue" {
145
+ interface GlobalComponents {
146
+ /** El visor de Seatsyoo (registrado con `app.use(SeatsyooPlugin)`). */
147
+ SeatsyooChart: typeof SeatsyooChart;
148
+ }
149
+ }
150
+ export {};
@@ -0,0 +1,224 @@
1
+ // SeatsyooChart: el visor de Seatsyoo como componente de Vue 3 (3.3 o posterior), sin SFC (defineComponent y una
2
+ // función render), para que se use sin compilar plantillas.
3
+ //
4
+ // - Se monta en onMounted: en el servidor (SSR) solo se pinta el <div> vacío.
5
+ // - Antes de desmontar, destroy().
6
+ // - Si cambia una opción de identidad (OPCIONES_DE_IDENTIDAD, por valor), se destruye y se monta otro. Si el nuevo
7
+ // no va a recuperar lo que el comprador tenía retenido (otro evento…), antes se suelta (sueltaAlCambiar).
8
+ // - theme y colorBy se aplican al visor montado (setTheme, setColorBy); statusStyles y marks (modo «manager»),
9
+ // también, comparadas por valor (setStatusStyles, setMarks), aunque se cambien en el sitio.
10
+ // - Los avisos del visor salen como eventos de Vue (ready, change, select…): llegan siempre a los oyentes de
11
+ // ese momento, sin volver a montar.
12
+ import { render, } from "@seatsyoo/visor";
13
+ import { defineComponent, getCurrentInstance, h, onBeforeUnmount, onMounted, shallowRef, toRaw, watch, } from "vue";
14
+ import { aplicarEnVivo, avisarEnConsola, copiaDeDatos, enVivoDe, errorDeOpciones, identidadDe, igual, opcionesEnVivo, sinIndefinidos, sueltaAlCambiar, } from "./opciones.js";
15
+ // Los booleanos con `default: undefined`: sin él, Vue los pone a false si no se pasan, y el visor los quiere
16
+ // sin indicar (su valor por defecto es «sí»).
17
+ const props = {
18
+ /** URL base del servidor de Seatsyoo, p. ej. https://seatsyoo.example */
19
+ server: { type: String, required: true },
20
+ /** Clave pública del espacio (obligatoria salvo con `link` o `document`). */
21
+ publicKey: String,
22
+ /** Clave del evento. */
23
+ event: String,
24
+ /** O la clave de un enlace público del plano: solo para ver. */
25
+ link: String,
26
+ /** O el plano ya cargado: solo para ver. */
27
+ document: Object,
28
+ /** O la clave de una temporada (abono). */
29
+ season: String,
30
+ /** Abono parcial: los eventos de la temporada elegidos. */
31
+ seasonEvents: Array,
32
+ /** Canales de venta desde los que se compra. */
33
+ channels: Array,
34
+ /**
35
+ * Los precios que pone tu web, por clave de categoría (`{ platea: 30 }`): el visor los enseña en la ficha, la leyenda
36
+ * y el total, y salen en `price` de lo elegido. Seatsyoo no guarda precios.
37
+ */
38
+ pricing: Object,
39
+ /** Moneda de `pricing` (código ISO; si no, euros). */
40
+ currency: String,
41
+ locale: String,
42
+ language: String,
43
+ /** Máximo de plazas seleccionables (por defecto 10). */
44
+ maxSelected: Number,
45
+ /** «continue» (por defecto) recupera la selección al recargar; «start» empieza de cero. */
46
+ session: String,
47
+ /** Token de retención creado por tu servidor. */
48
+ holdToken: String,
49
+ /** «buyer» (por defecto) o «manager». */
50
+ mode: String,
51
+ /** Color base de los objetos (se aplica sin volver a montar). */
52
+ colorBy: String,
53
+ /** Tema (se aplica sin volver a montar). */
54
+ theme: String,
55
+ /**
56
+ * Solo en el modo «manager»: nombre, color e icono de los estados, por su clave (los propios y los de base que se
57
+ * quieran cambiar). Se aplica sin volver a montar (setStatusStyles).
58
+ */
59
+ statusStyles: Object,
60
+ /**
61
+ * Solo en el modo «manager»: marcas sobre plazas que siguen libres, objeto → estado (su aspecto, en `statusStyles`).
62
+ * Se aplica sin volver a montar (setMarks).
63
+ */
64
+ marks: Object,
65
+ showLegend: { type: Boolean, default: undefined },
66
+ showSelectionBar: { type: Boolean, default: undefined },
67
+ /** Planta que se muestra al abrir (id). */
68
+ floor: String,
69
+ sectors: { type: Boolean, default: undefined },
70
+ /** Botón de vista 3D (por defecto sí). */
71
+ view3d: { type: Boolean, default: undefined },
72
+ /** Entrada animada al abrirse: el plano se descubre desde el escenario (por defecto, no). */
73
+ entrance: { type: Boolean, default: undefined },
74
+ /**
75
+ * Textos del plano que se enseñan cambiados (por ejemplo, traducidos), por el texto tal como está en el plano:
76
+ * `{ Platea: "Stalls" }`. Solo cambia lo que se ve: claves, selección y eventos siguen siendo los del plano.
77
+ */
78
+ texts: Object,
79
+ /** Cómo cargar el módulo 3D (por defecto se pide al servidor). */
80
+ load3d: Function,
81
+ };
82
+ // Los parámetros solo dan el tipo de cada evento (Vue valida con lo que devuelven: siempre sí).
83
+ const emits = {
84
+ /** El visor ha cargado el plano y los estados. */
85
+ ready: (chart) => true,
86
+ /** La selección ha cambiado. */
87
+ change: (selection) => true,
88
+ select: (object) => true,
89
+ deselect: (object) => true,
90
+ /** Algo ha fallado: `{ code, message }`. Sin oyente, se escribe en la consola. */
91
+ error: (error) => true,
92
+ /** Se acabó el tiempo de la retención (`@hold-expired`). */
93
+ holdExpired: () => true,
94
+ /** El token de retención ha cambiado (`@hold-token-change`). */
95
+ holdTokenChange: (token) => true,
96
+ /** Se muestra otra planta (`@floor-change`). */
97
+ floorChange: (floor) => true,
98
+ };
99
+ // El componente; su tipo público es SeatsyooChartComponent (abajo).
100
+ const componente = defineComponent({
101
+ name: "SeatsyooChart",
102
+ props,
103
+ emits,
104
+ expose: ["chart"],
105
+ setup(props, { emit }) {
106
+ /** El visor montado, o null (en el servidor, antes de montar, tras desmontar o si las opciones no valen). */
107
+ const chart = shallowRef(null);
108
+ /** El <div> donde se monta (ref de plantilla «el»). */
109
+ const el = shallowRef(null);
110
+ const instancia = getCurrentInstance();
111
+ let vivo = false;
112
+ /** La identidad con que se montó el visor actual (o con que falló el último intento). */
113
+ let montada = null;
114
+ /** Las opciones en vivo (tema, color, estilos y marcas) que tiene el visor actual. */
115
+ let aplicadas = enVivoDe({});
116
+ /** ¿Hay alguien escuchando @error ahora mismo? (Si no, el error va a la consola, como hace el visor.) */
117
+ function escuchanError() {
118
+ const p = instancia?.vnode.props;
119
+ return !!p && (typeof p.onError === "function" || Array.isArray(p.onError) || typeof p.onErrorOnce === "function");
120
+ }
121
+ /** La identidad de las props: lee solo esas props (así el watch solo depende de ellas). */
122
+ function identidad() {
123
+ const id = identidadDe(props);
124
+ // El plano no se copia ni se recorre: si llega reactivo, el visor lo recibe sin proxy.
125
+ id.document = id.document && toRaw(id.document);
126
+ return id;
127
+ }
128
+ function desmontar() {
129
+ const anterior = chart.value;
130
+ chart.value = null;
131
+ montada = null;
132
+ anterior?.destroy();
133
+ }
134
+ function montar(nueva) {
135
+ // Al cambiar de evento (o de lo que sea que el visor nuevo no recupere), se suelta lo que tenía retenido el
136
+ // que se va, con sus avisos (deselect y change con la selección vacía). Al desmontar el componente, no.
137
+ if (chart.value && montada && sueltaAlCambiar(montada, nueva))
138
+ void chart.value.clearSelection();
139
+ desmontar();
140
+ const div = el.value;
141
+ if (!div)
142
+ return;
143
+ montada = nueva;
144
+ const { load3d: hayLoad3d, ...opciones } = nueva;
145
+ const load3d = props.load3d;
146
+ // Copias sin proxy de statusStyles y marks: lo que se compara después y lo que recibe el visor.
147
+ const enVivo = enVivoDe(props);
148
+ try {
149
+ chart.value = render({
150
+ ...sinIndefinidos(opciones),
151
+ ...opcionesEnVivo(props, enVivo),
152
+ container: div,
153
+ onReady: (c) => emit("ready", c),
154
+ onChange: (s) => emit("change", s),
155
+ onSelect: (o) => emit("select", o),
156
+ onDeselect: (o) => emit("deselect", o),
157
+ onHoldExpired: () => emit("holdExpired"),
158
+ onHoldTokenChange: (t) => emit("holdTokenChange", t),
159
+ onFloorChange: (f) => emit("floorChange", f),
160
+ onError: (e) => (escuchanError() ? emit("error", e) : avisarEnConsola(e)),
161
+ // Solo si hay load3d (si no, el visor pide la 3D al servidor); ponerlo o quitarlo cambia la identidad.
162
+ ...(hayLoad3d && load3d ? { load3d: () => (props.load3d ?? load3d)() } : {}),
163
+ });
164
+ }
165
+ catch (err) {
166
+ const error = errorDeOpciones(err);
167
+ if (escuchanError())
168
+ emit("error", error);
169
+ else
170
+ console.error("[seatsyoo]", error.message);
171
+ return;
172
+ }
173
+ aplicadas = enVivo;
174
+ }
175
+ onMounted(() => {
176
+ vivo = true;
177
+ montar(identidad());
178
+ });
179
+ onBeforeUnmount(() => {
180
+ vivo = false;
181
+ desmontar();
182
+ });
183
+ // Antes del de las opciones en vivo: si en el mismo cambio se monta otro visor, ya lleva el tema nuevo.
184
+ watch(identidad, (nueva) => {
185
+ if (vivo && !(montada && igual(nueva, montada)))
186
+ montar(nueva);
187
+ });
188
+ // Copiar statusStyles y marks lee todo lo que tienen: así también avisa si se cambian en el sitio (reactivos).
189
+ watch(() => ({ theme: props.theme, colorBy: props.colorBy, statusStyles: copiaDeDatos(props.statusStyles), marks: copiaDeDatos(props.marks) }), (vivas) => {
190
+ const c = chart.value;
191
+ if (c)
192
+ aplicarEnVivo(c, aplicadas, vivas);
193
+ });
194
+ return { chart, el };
195
+ },
196
+ render() {
197
+ // class, style, id y demás atributos pasan solos a este <div> (inheritAttrs).
198
+ return h("div", { ref: "el" });
199
+ },
200
+ });
201
+ /**
202
+ * El visor de mapas de asientos de Seatsyoo.
203
+ *
204
+ * ```vue
205
+ * <SeatsyooChart
206
+ * ref="plano"
207
+ * server="https://seatsyoo.example"
208
+ * public-key="pk_tu_clave_publica"
209
+ * event="hamlet-2026-11-20"
210
+ * style="height: 600px"
211
+ * @change="(seleccion) => (elegidas = seleccion)"
212
+ * />
213
+ * ```
214
+ *
215
+ * Con una ref de plantilla, `plano.value.chart` es el visor montado (`getSelection()`, `holdToken`…), o `null`
216
+ * mientras no hay ninguno.
217
+ */
218
+ export const SeatsyooChart = componente;
219
+ /** Plugin que registra <SeatsyooChart> en toda la aplicación: `app.use(SeatsyooPlugin)`. */
220
+ export const SeatsyooPlugin = {
221
+ install(app) {
222
+ app.component("SeatsyooChart", SeatsyooChart);
223
+ },
224
+ };
@@ -0,0 +1,2 @@
1
+ export { SeatsyooChart, type SeatsyooChartExposed, type SeatsyooChartProps, SeatsyooPlugin } from "./SeatsyooChart.js";
2
+ export type { ChartDocument, Language, RenderOptions, SeatingChart, SelectedObject, StatusStyle } from "@seatsyoo/visor";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // @seatsyoo/vue: el visor de mapas de asientos de Seatsyoo como componente de Vue 3.
2
+ export { SeatsyooChart, SeatsyooPlugin } from "./SeatsyooChart.js";
@@ -0,0 +1,99 @@
1
+ import type { RenderOptions, SeatingChart } from "@seatsyoo/visor";
2
+ /**
3
+ * Opciones que, si cambian, vuelven a montar el visor: deciden qué se carga y cómo se vende. Los arrays y los
4
+ * objetos se comparan por su valor, no por la referencia: `:channels="['web']"` en cada render no remonta.
5
+ */
6
+ export declare const OPCIONES_DE_IDENTIDAD: readonly ["server", "publicKey", "event", "link", "document", "season", "seasonEvents", "channels", "pricing", "currency", "locale", "language", "maxSelected", "session", "holdToken", "mode", "showLegend", "showSelectionBar", "floor", "sectors", "view3d", "entrance", "texts"];
7
+ /**
8
+ * Opciones que se aplican al visor ya montado, sin volver a montarlo: theme y colorBy (setTheme y setColorBy) y, para
9
+ * el modo «manager», los estilos de los estados y las marcas sobre plazas libres (setStatusStyles y setMarks). Se
10
+ * comparan por valor, como las de identidad: un objeto nuevo con lo mismo no llama a nada.
11
+ */
12
+ export declare const OPCIONES_EN_VIVO: readonly ["theme", "colorBy", "statusStyles", "marks"];
13
+ /** Avisos del visor: siempre llaman a la última función recibida, sin volver a montar. */
14
+ export declare const AVISOS: readonly ["onReady", "onChange", "onSelect", "onDeselect", "onError", "onHoldExpired", "onHoldTokenChange", "onFloorChange"];
15
+ type ClaveDeIdentidad = (typeof OPCIONES_DE_IDENTIDAD)[number];
16
+ /**
17
+ * Cada opción del visor tiene aquí su regla (vuelve a montar, se aplica en vivo o es un aviso). Si el visor gana una
18
+ * opción nueva, esto deja de compilar hasta decidir qué hace el componente con ella: pasó con `entrance` y `texts`,
19
+ * que llegaron al visor sin que el componente las pasara, y con `statusStyles` y `marks` (en vivo: el visor tiene
20
+ * setStatusStyles y setMarks).
21
+ */
22
+ type SinRegla = Exclude<keyof RenderOptions, ClaveDeIdentidad | (typeof OPCIONES_EN_VIVO)[number] | (typeof AVISOS)[number] | "container" | "load3d">;
23
+ type DebeSerNever<T extends never> = T;
24
+ export type TodasLasOpcionesTienenRegla = DebeSerNever<SinRegla>;
25
+ /**
26
+ * Lo que identifica a un visor montado: las opciones de identidad (copiando arrays y objetos pequeños, para que
27
+ * cambiarlos en el sitio también cuente) y si hay `load3d` (la función en sí puede cambiar sin remontar: se
28
+ * llama siempre a la última).
29
+ */
30
+ export type Identidad = {
31
+ [K in ClaveDeIdentidad]: RenderOptions[K];
32
+ } & {
33
+ load3d: boolean;
34
+ };
35
+ export declare function identidadDe(opciones: Partial<RenderOptions>): Identidad;
36
+ /** Igualdad por valor de datos tipo JSON (cadenas, números, booleanos, null, undefined, arrays y objetos). */
37
+ export declare function igual(a: unknown, b: unknown): boolean;
38
+ /**
39
+ * Copia normalizada de datos tipo JSON (listas y objetos, a cualquier profundidad): los objetos salen con
40
+ * Object.prototype (también un diccionario hecho con `Object.create(null)`) y sin las claves que valen `undefined`
41
+ * (`{ color, icon: undefined }` es lo mismo que `{ color }`: el visor no pinta icono en los dos casos).
42
+ *
43
+ * Es la regla de statusStyles y marks, la misma en React, Vue y Angular: lo aplicado en vivo se guarda así copiado, lo
44
+ * que llega se copia igual antes de compararlo, y el visor recibe esa copia. Así cambiarlo en el sitio también cuenta y
45
+ * el visor no comparte el objeto de la página (setMarks repinta lo que difiere de lo que tenía, y si fuera el mismo
46
+ * objeto ya cambiado no vería nada).
47
+ */
48
+ export declare function copiaDeDatos<T>(valor: T): T;
49
+ /** Con qué valor de cada opción en vivo se montó el visor o se le cambió después (sin indicar, el de por defecto). */
50
+ export interface EnVivo {
51
+ theme: NonNullable<RenderOptions["theme"]>;
52
+ colorBy: NonNullable<RenderOptions["colorBy"]>;
53
+ statusStyles: NonNullable<RenderOptions["statusStyles"]>;
54
+ marks: NonNullable<RenderOptions["marks"]>;
55
+ }
56
+ /**
57
+ * Las opciones en vivo con que se monta un visor. Sin theme ni colorBy, los del visor por defecto; sin statusStyles ni
58
+ * marks, ninguno (`{}`, que para el visor es lo mismo que no pasarlas).
59
+ */
60
+ export declare function enVivoDe(opciones: Partial<RenderOptions>): EnVivo;
61
+ /** Lo que hay que añadir a las opciones de `render` para montar con estas opciones en vivo (las copias guardadas). */
62
+ export declare function opcionesEnVivo(opciones: Partial<RenderOptions>, enVivo: EnVivo): Partial<RenderOptions>;
63
+ /** Los métodos del visor con que se aplican las opciones en vivo. */
64
+ export type VisorEnVivo = Pick<SeatingChart, "setTheme" | "setColorBy" | "setStatusStyles" | "setMarks">;
65
+ /**
66
+ * Lleva al visor montado las opciones en vivo que han cambiado (por valor) desde `aplicadas`, y las apunta en
67
+ * `aplicadas`. Quitar una es volver a la de por defecto: el tema claro, el color por categoría, sin estilos ni marcas.
68
+ */
69
+ export declare function aplicarEnVivo(visor: VisorEnVivo, aplicadas: EnVivo, opciones: Partial<RenderOptions>): void;
70
+ /** Las opciones sin las que valen `undefined` (el visor usa entonces su valor por defecto). */
71
+ export declare function sinIndefinidos<T extends object>(opciones: T): T;
72
+ /** Lo que hace el visor con un error si nadie escucha `onError`: escribirlo en la consola. */
73
+ export declare function avisarEnConsola(error: {
74
+ code: string;
75
+ message: string;
76
+ }): void;
77
+ /**
78
+ * `render` lanza si faltan opciones (qué enseñar, la clave pública…). En un componente no hay quien lo
79
+ * recoja, así que se convierte en un error con el código `invalid-options`.
80
+ */
81
+ export declare function errorDeOpciones(err: unknown): {
82
+ code: string;
83
+ message: string;
84
+ };
85
+ /**
86
+ * Al volver a montar (cambia una opción de identidad), ¿hay que soltar lo que el comprador tiene retenido en el
87
+ * visor que se va?
88
+ *
89
+ * destroy() no suelta lo retenido: así el visor que se monte después en el mismo evento lo recupera (con
90
+ * `session: "continue"`, guarda su token en sessionStorage). Pero si el nuevo no lo va a recuperar (otro evento u
91
+ * otra temporada, otro servidor o clave pública, `session: "start"`, un `holdToken` nuevo, modo «manager»), esas
92
+ * plazas se quedarían bloqueadas para los demás hasta que caduque la retención, sin que la página pueda venderlas
93
+ * ni soltarlas: el comprador ya no las ve y el token del visor nuevo es otro. Entonces se sueltan.
94
+ *
95
+ * Lo retenido con un `holdToken` de la página no se suelta nunca: es de su token y lo lleva su servidor (por
96
+ * ejemplo, un carrito con varias funciones). Tampoco hay nada que soltar en el modo «manager» ni al solo ver.
97
+ */
98
+ export declare function sueltaAlCambiar(antes: Identidad, despues: Identidad): boolean;
99
+ export {};
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Opciones que, si cambian, vuelven a montar el visor: deciden qué se carga y cómo se vende. Los arrays y los
3
+ * objetos se comparan por su valor, no por la referencia: `:channels="['web']"` en cada render no remonta.
4
+ */
5
+ export const OPCIONES_DE_IDENTIDAD = [
6
+ "server",
7
+ "publicKey",
8
+ "event",
9
+ "link",
10
+ "document",
11
+ "season",
12
+ "seasonEvents",
13
+ "channels",
14
+ "pricing",
15
+ "currency",
16
+ "locale",
17
+ "language",
18
+ "maxSelected",
19
+ "session",
20
+ "holdToken",
21
+ "mode",
22
+ "showLegend",
23
+ "showSelectionBar",
24
+ "floor",
25
+ "sectors",
26
+ "view3d",
27
+ "entrance",
28
+ "texts",
29
+ ];
30
+ /**
31
+ * Opciones que se aplican al visor ya montado, sin volver a montarlo: theme y colorBy (setTheme y setColorBy) y, para
32
+ * el modo «manager», los estilos de los estados y las marcas sobre plazas libres (setStatusStyles y setMarks). Se
33
+ * comparan por valor, como las de identidad: un objeto nuevo con lo mismo no llama a nada.
34
+ */
35
+ export const OPCIONES_EN_VIVO = ["theme", "colorBy", "statusStyles", "marks"];
36
+ /** Avisos del visor: siempre llaman a la última función recibida, sin volver a montar. */
37
+ export const AVISOS = [
38
+ "onReady",
39
+ "onChange",
40
+ "onSelect",
41
+ "onDeselect",
42
+ "onError",
43
+ "onHoldExpired",
44
+ "onHoldTokenChange",
45
+ "onFloorChange",
46
+ ];
47
+ export function identidadDe(opciones) {
48
+ const identidad = { load3d: typeof opciones.load3d === "function" };
49
+ for (const clave of OPCIONES_DE_IDENTIDAD) {
50
+ const valor = opciones[clave];
51
+ // El plano puede ser muy grande: no se copia (se compara por valor solo si llega otro objeto).
52
+ identidad[clave] = clave === "document" ? valor : copiaSuperficial(valor);
53
+ }
54
+ return identidad;
55
+ }
56
+ function copiaSuperficial(valor) {
57
+ if (Array.isArray(valor))
58
+ return [...valor];
59
+ if (valor !== null && typeof valor === "object")
60
+ return { ...valor };
61
+ return valor;
62
+ }
63
+ /** Igualdad por valor de datos tipo JSON (cadenas, números, booleanos, null, undefined, arrays y objetos). */
64
+ export function igual(a, b) {
65
+ if (Object.is(a, b))
66
+ return true;
67
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
68
+ return false;
69
+ if (Array.isArray(a)) {
70
+ if (!Array.isArray(b) || a.length !== b.length)
71
+ return false;
72
+ for (let i = 0; i < a.length; i++)
73
+ if (!igual(a[i], b[i]))
74
+ return false;
75
+ return true;
76
+ }
77
+ if (Array.isArray(b))
78
+ return false;
79
+ const x = a;
80
+ const y = b;
81
+ const claves = Object.keys(x);
82
+ if (claves.length !== Object.keys(y).length)
83
+ return false;
84
+ for (const k of claves) {
85
+ if (!Object.prototype.hasOwnProperty.call(y, k) || !igual(x[k], y[k]))
86
+ return false;
87
+ }
88
+ return true;
89
+ }
90
+ /**
91
+ * Copia normalizada de datos tipo JSON (listas y objetos, a cualquier profundidad): los objetos salen con
92
+ * Object.prototype (también un diccionario hecho con `Object.create(null)`) y sin las claves que valen `undefined`
93
+ * (`{ color, icon: undefined }` es lo mismo que `{ color }`: el visor no pinta icono en los dos casos).
94
+ *
95
+ * Es la regla de statusStyles y marks, la misma en React, Vue y Angular: lo aplicado en vivo se guarda así copiado, lo
96
+ * que llega se copia igual antes de compararlo, y el visor recibe esa copia. Así cambiarlo en el sitio también cuenta y
97
+ * el visor no comparte el objeto de la página (setMarks repinta lo que difiere de lo que tenía, y si fuera el mismo
98
+ * objeto ya cambiado no vería nada).
99
+ */
100
+ export function copiaDeDatos(valor) {
101
+ if (Array.isArray(valor))
102
+ return valor.map((v) => copiaDeDatos(v));
103
+ if (valor !== null && typeof valor === "object") {
104
+ const entradas = Object.entries(valor).filter(([, v]) => v !== undefined);
105
+ return Object.fromEntries(entradas.map(([k, v]) => [k, copiaDeDatos(v)]));
106
+ }
107
+ return valor;
108
+ }
109
+ /**
110
+ * Las opciones en vivo con que se monta un visor. Sin theme ni colorBy, los del visor por defecto; sin statusStyles ni
111
+ * marks, ninguno (`{}`, que para el visor es lo mismo que no pasarlas).
112
+ */
113
+ export function enVivoDe(opciones) {
114
+ return {
115
+ theme: opciones.theme ?? "light",
116
+ colorBy: opciones.colorBy ?? "category",
117
+ statusStyles: copiaDeDatos(opciones.statusStyles ?? {}),
118
+ marks: copiaDeDatos(opciones.marks ?? {}),
119
+ };
120
+ }
121
+ /** Lo que hay que añadir a las opciones de `render` para montar con estas opciones en vivo (las copias guardadas). */
122
+ export function opcionesEnVivo(opciones, enVivo) {
123
+ return sinIndefinidos({
124
+ theme: opciones.theme,
125
+ colorBy: opciones.colorBy,
126
+ statusStyles: opciones.statusStyles && enVivo.statusStyles,
127
+ marks: opciones.marks && enVivo.marks,
128
+ });
129
+ }
130
+ /**
131
+ * Lleva al visor montado las opciones en vivo que han cambiado (por valor) desde `aplicadas`, y las apunta en
132
+ * `aplicadas`. Quitar una es volver a la de por defecto: el tema claro, el color por categoría, sin estilos ni marcas.
133
+ */
134
+ export function aplicarEnVivo(visor, aplicadas, opciones) {
135
+ const tema = opciones.theme ?? "light";
136
+ if (tema !== aplicadas.theme) {
137
+ visor.setTheme(tema);
138
+ aplicadas.theme = tema;
139
+ }
140
+ const color = opciones.colorBy ?? "category";
141
+ if (color !== aplicadas.colorBy) {
142
+ visor.setColorBy(color);
143
+ aplicadas.colorBy = color;
144
+ }
145
+ // statusStyles y marks: la copia normalizada de lo que llega (copiaDeDatos) contra la de lo aplicado. Un
146
+ // diccionario sin prototipo o con claves que valen undefined, con lo mismo, no llama a nada (como en Angular).
147
+ const estilos = copiaDeDatos(opciones.statusStyles ?? {});
148
+ if (!igual(estilos, aplicadas.statusStyles)) {
149
+ aplicadas.statusStyles = estilos;
150
+ visor.setStatusStyles(estilos);
151
+ }
152
+ const marcas = copiaDeDatos(opciones.marks ?? {});
153
+ if (!igual(marcas, aplicadas.marks)) {
154
+ aplicadas.marks = marcas;
155
+ visor.setMarks(marcas);
156
+ }
157
+ }
158
+ /** Las opciones sin las que valen `undefined` (el visor usa entonces su valor por defecto). */
159
+ export function sinIndefinidos(opciones) {
160
+ return Object.fromEntries(Object.entries(opciones).filter(([, v]) => v !== undefined));
161
+ }
162
+ /** Lo que hace el visor con un error si nadie escucha `onError`: escribirlo en la consola. */
163
+ export function avisarEnConsola(error) {
164
+ console.warn("[seatsyoo]", error.code, error.message);
165
+ }
166
+ /**
167
+ * `render` lanza si faltan opciones (qué enseñar, la clave pública…). En un componente no hay quien lo
168
+ * recoja, así que se convierte en un error con el código `invalid-options`.
169
+ */
170
+ export function errorDeOpciones(err) {
171
+ return { code: "invalid-options", message: err instanceof Error ? err.message : String(err) };
172
+ }
173
+ /**
174
+ * Qué compra el visor montado con estas opciones: el evento, o la temporada (con los eventos del abono parcial, en
175
+ * cualquier orden, como los guarda el visor); `null` si solo enseña el plano (`link`, `document`: no retiene nada).
176
+ */
177
+ function destino(i) {
178
+ // Con el mismo orden que el visor: document, link, season y event.
179
+ if (i.document || i.link)
180
+ return null;
181
+ if (!i.season)
182
+ return `evento:${i.event ?? ""}`;
183
+ const abono = i.seasonEvents?.length ? `@${[...i.seasonEvents].sort().join(",")}` : "";
184
+ return `temporada:${i.season}${abono}`;
185
+ }
186
+ /**
187
+ * Al volver a montar (cambia una opción de identidad), ¿hay que soltar lo que el comprador tiene retenido en el
188
+ * visor que se va?
189
+ *
190
+ * destroy() no suelta lo retenido: así el visor que se monte después en el mismo evento lo recupera (con
191
+ * `session: "continue"`, guarda su token en sessionStorage). Pero si el nuevo no lo va a recuperar (otro evento u
192
+ * otra temporada, otro servidor o clave pública, `session: "start"`, un `holdToken` nuevo, modo «manager»), esas
193
+ * plazas se quedarían bloqueadas para los demás hasta que caduque la retención, sin que la página pueda venderlas
194
+ * ni soltarlas: el comprador ya no las ve y el token del visor nuevo es otro. Entonces se sueltan.
195
+ *
196
+ * Lo retenido con un `holdToken` de la página no se suelta nunca: es de su token y lo lleva su servidor (por
197
+ * ejemplo, un carrito con varias funciones). Tampoco hay nada que soltar en el modo «manager» ni al solo ver.
198
+ */
199
+ export function sueltaAlCambiar(antes, despues) {
200
+ if (antes.holdToken !== undefined || antes.mode === "manager" || destino(antes) === null)
201
+ return false;
202
+ const recupera = despues.holdToken === undefined &&
203
+ despues.mode !== "manager" &&
204
+ (antes.session ?? "continue") === "continue" &&
205
+ (despues.session ?? "continue") === "continue" &&
206
+ antes.server === despues.server &&
207
+ antes.publicKey === despues.publicKey &&
208
+ destino(antes) === destino(despues);
209
+ return !recupera;
210
+ }
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@seatsyoo/vue",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "Componente de Vue para el visor de mapas de asientos de Seatsyoo: elegir butacas, retenerlas y venderlas en tiempo real.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "seatsyoo",
8
+ "vue",
9
+ "mapa de asientos",
10
+ "butacas",
11
+ "entradas",
12
+ "seating chart",
13
+ "seat map",
14
+ "tickets"
15
+ ],
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "dist/*.js",
27
+ "dist/*.d.ts"
28
+ ],
29
+ "dependencies": {
30
+ "@seatsyoo/visor": "^1.0.0"
31
+ },
32
+ "peerDependencies": {
33
+ "vue": "^3.3.0"
34
+ },
35
+ "main": "./dist/index.js",
36
+ "module": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
6
41
  }