venuekit 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Juan Cruz Fernández
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,332 @@
1
- # Temporary Holding Version
1
+ # VenueKit
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
+ [![npm](https://img.shields.io/npm/v/venuekit.svg)](https://www.npmjs.com/package/venuekit)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./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
32
+
33
+ ```bash
34
+ npm install venuekit
35
+ ```
36
+
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`:
55
+
56
+ ```ts
57
+ import type { PlaceTypeOption } from "venuekit"
58
+
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
+ ]
64
+ ```
65
+
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.
73
+
74
+ ### 2. Let organizers draw the plan
75
+
76
+ ```tsx
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
+ }
105
+ ```
106
+
107
+ ### 3. Let buyers pick their seats
108
+
109
+ ```tsx
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
+ }
135
+ ```
136
+
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).
139
+
140
+ ### 4. Validate on the server
141
+
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
+ }
162
+ ```
163
+
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.
179
+
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.
183
+
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.
188
+
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
264
+
265
+ ```tsx
266
+ {focus === null ? (
267
+ <VenueOverview
268
+ layout={layout}
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}
276
+ />
277
+ ) : (
278
+ <SeatMap
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}
289
+ />
290
+ )}
291
+ ```
292
+
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.
315
+
316
+ ```bash
317
+ npm install
318
+ npm test # unit tests (Vitest)
319
+ npm run typecheck
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)
324
+ ```
325
+
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