@nex125/seatmap-editor 0.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/dist/index.js ADDED
@@ -0,0 +1,1958 @@
1
+ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
2
+ import { clampToPolygon, generateId, pointInPolygon, CommandHistory, serializeVenue, deserializeVenue, venueAABB } from '@nex125/seatmap-core';
3
+ import { SeatmapProvider, useSeatmapContext, SeatmapCanvas } from '@nex125/seatmap-react';
4
+ import { useStore } from 'zustand';
5
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
+
7
+ // src/SeatmapEditor.tsx
8
+
9
+ // src/tools/BaseTool.ts
10
+ var BaseTool = class {
11
+ onPointerDown(_e, _viewport, _store) {
12
+ }
13
+ onPointerMove(_e, _viewport, _store) {
14
+ }
15
+ onPointerUp(_e, _viewport, _store) {
16
+ }
17
+ onActivate(_viewport, _store) {
18
+ }
19
+ onDeactivate() {
20
+ }
21
+ };
22
+
23
+ // src/tools/PanTool.ts
24
+ var PanTool = class extends BaseTool {
25
+ name = "pan";
26
+ cursor = "grab";
27
+ isPanning = false;
28
+ lastX = 0;
29
+ lastY = 0;
30
+ onPointerDown(e) {
31
+ this.isPanning = true;
32
+ this.lastX = e.screenX;
33
+ this.lastY = e.screenY;
34
+ }
35
+ onPointerMove(e, viewport) {
36
+ if (!this.isPanning) return;
37
+ const dx = e.screenX - this.lastX;
38
+ const dy = e.screenY - this.lastY;
39
+ this.lastX = e.screenX;
40
+ this.lastY = e.screenY;
41
+ viewport.pan(dx, dy);
42
+ }
43
+ onPointerUp() {
44
+ this.isPanning = false;
45
+ }
46
+ onDeactivate() {
47
+ this.isPanning = false;
48
+ }
49
+ };
50
+ var GRID = 20;
51
+ function snapToGrid(v) {
52
+ return Math.round(v / GRID) * GRID;
53
+ }
54
+ var SelectTool = class extends BaseTool {
55
+ constructor(spatialIndex, history) {
56
+ super();
57
+ this.spatialIndex = spatialIndex;
58
+ this.history = history;
59
+ }
60
+ name = "select";
61
+ cursor = "default";
62
+ isDragging = false;
63
+ dragStartWorld = { x: 0, y: 0 };
64
+ hasDragged = false;
65
+ dragMode = { type: "none" };
66
+ selectionRect = null;
67
+ onPointerDown(e, _viewport, store) {
68
+ this.isDragging = true;
69
+ this.hasDragged = false;
70
+ this.dragStartWorld = { x: e.worldX, y: e.worldY };
71
+ this.selectionRect = null;
72
+ this.dragMode = { type: "none" };
73
+ const hits = this.spatialIndex.queryPoint({ x: e.worldX, y: e.worldY }, 12);
74
+ const seatHit = hits.find((h) => h.type === "seat" && h.seatId);
75
+ const sectionHit = hits.find((h) => h.type === "section");
76
+ const venue = store.getState().venue;
77
+ if (!venue) return;
78
+ if (seatHit?.seatId && store.getState().selectedSeatIds.has(seatHit.seatId)) {
79
+ const selectedIds = store.getState().selectedSeatIds;
80
+ const sectionId = seatHit.sectionId;
81
+ const originals = /* @__PURE__ */ new Map();
82
+ const section = venue.sections.find((s) => s.id === sectionId);
83
+ if (section) {
84
+ for (const row of section.rows) {
85
+ for (const seat of row.seats) {
86
+ if (selectedIds.has(seat.id)) {
87
+ originals.set(seat.id, { rowId: row.id, pos: { ...seat.position } });
88
+ }
89
+ }
90
+ }
91
+ }
92
+ if (originals.size > 0) {
93
+ this.dragMode = { type: "seats", sectionId, originals };
94
+ return;
95
+ }
96
+ }
97
+ if (sectionHit && !seatHit) {
98
+ const section = venue.sections.find((s) => s.id === sectionHit.sectionId);
99
+ if (section) {
100
+ this.dragMode = {
101
+ type: "section",
102
+ sectionId: section.id,
103
+ origPos: { ...section.position }
104
+ };
105
+ return;
106
+ }
107
+ }
108
+ }
109
+ onPointerMove(e, _viewport, store) {
110
+ if (!this.isDragging) return;
111
+ const dx = e.worldX - this.dragStartWorld.x;
112
+ const dy = e.worldY - this.dragStartWorld.y;
113
+ if (!this.hasDragged && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
114
+ this.hasDragged = true;
115
+ if (this.dragMode.type === "none") {
116
+ this.dragMode = { type: "rect" };
117
+ }
118
+ }
119
+ if (!this.hasDragged) return;
120
+ const venue = store.getState().venue;
121
+ if (!venue) return;
122
+ if (this.dragMode.type === "seats") {
123
+ const { sectionId, originals } = this.dragMode;
124
+ const section = venue.sections.find((s) => s.id === sectionId);
125
+ if (!section) return;
126
+ const c = Math.cos(-section.rotation);
127
+ const s2 = Math.sin(-section.rotation);
128
+ const localDx = dx * c - dy * s2;
129
+ const localDy = dx * s2 + dy * c;
130
+ const outline = section.outline;
131
+ store.getState().setVenue({
132
+ ...venue,
133
+ sections: venue.sections.map((sec) => {
134
+ if (sec.id !== sectionId) return sec;
135
+ return {
136
+ ...sec,
137
+ rows: sec.rows.map((r) => ({
138
+ ...r,
139
+ seats: r.seats.map((st) => {
140
+ const orig = originals.get(st.id);
141
+ if (!orig) return st;
142
+ let pos = {
143
+ x: orig.pos.x + localDx,
144
+ y: orig.pos.y + localDy
145
+ };
146
+ if (outline.length >= 3) {
147
+ pos = clampToPolygon(pos, outline);
148
+ }
149
+ return { ...st, position: pos };
150
+ })
151
+ }))
152
+ };
153
+ })
154
+ });
155
+ return;
156
+ }
157
+ if (this.dragMode.type === "section") {
158
+ const { sectionId, origPos } = this.dragMode;
159
+ store.getState().setVenue({
160
+ ...venue,
161
+ sections: venue.sections.map(
162
+ (sec) => sec.id === sectionId ? { ...sec, position: { x: origPos.x + dx, y: origPos.y + dy } } : sec
163
+ )
164
+ });
165
+ return;
166
+ }
167
+ if (this.dragMode.type === "rect") {
168
+ const x = Math.min(this.dragStartWorld.x, e.worldX);
169
+ const y = Math.min(this.dragStartWorld.y, e.worldY);
170
+ const width = Math.abs(dx);
171
+ const height = Math.abs(dy);
172
+ this.selectionRect = { x, y, width, height };
173
+ const items = this.spatialIndex.queryRect({
174
+ minX: x,
175
+ minY: y,
176
+ maxX: x + width,
177
+ maxY: y + height
178
+ });
179
+ const seatIds = items.filter((item) => item.type === "seat" && item.seatId).map((item) => item.seatId);
180
+ store.getState().setSelection(seatIds);
181
+ }
182
+ }
183
+ onPointerUp(e, _viewport, store) {
184
+ if (this.hasDragged) {
185
+ this.commitDrag(store);
186
+ } else {
187
+ const hits = this.spatialIndex.queryPoint({ x: e.worldX, y: e.worldY }, 12);
188
+ const seatHit = hits.find((h) => h.type === "seat" && h.seatId);
189
+ if (seatHit?.seatId) {
190
+ if (e.shiftKey || e.ctrlKey || e.metaKey) {
191
+ store.getState().toggleSeat(seatHit.seatId);
192
+ } else {
193
+ store.getState().setSelection([seatHit.seatId]);
194
+ }
195
+ } else if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
196
+ store.getState().clearSelection();
197
+ }
198
+ }
199
+ this.reset();
200
+ }
201
+ commitDrag(store) {
202
+ const venue = store.getState().venue;
203
+ if (!venue) return;
204
+ if (this.dragMode.type === "seats") {
205
+ const { sectionId, originals } = this.dragMode;
206
+ const section = venue.sections.find((s) => s.id === sectionId);
207
+ if (!section) return;
208
+ const finals = /* @__PURE__ */ new Map();
209
+ for (const row of section.rows) {
210
+ for (const seat of row.seats) {
211
+ if (originals.has(seat.id)) {
212
+ finals.set(seat.id, {
213
+ x: snapToGrid(seat.position.x),
214
+ y: snapToGrid(seat.position.y)
215
+ });
216
+ }
217
+ }
218
+ }
219
+ store.getState().setVenue({
220
+ ...venue,
221
+ sections: venue.sections.map(
222
+ (sec) => sec.id === sectionId ? {
223
+ ...sec,
224
+ rows: sec.rows.map((r) => ({
225
+ ...r,
226
+ seats: r.seats.map((st) => {
227
+ const fp = finals.get(st.id);
228
+ return fp ? { ...st, position: fp } : st;
229
+ })
230
+ }))
231
+ } : sec
232
+ )
233
+ });
234
+ this.history.execute({
235
+ description: `Move ${originals.size} seat(s)`,
236
+ execute: () => {
237
+ const v = store.getState().venue;
238
+ if (!v) return;
239
+ store.getState().setVenue({
240
+ ...v,
241
+ sections: v.sections.map(
242
+ (sec) => sec.id === sectionId ? {
243
+ ...sec,
244
+ rows: sec.rows.map((r) => ({
245
+ ...r,
246
+ seats: r.seats.map((st) => {
247
+ const fp = finals.get(st.id);
248
+ return fp ? { ...st, position: fp } : st;
249
+ })
250
+ }))
251
+ } : sec
252
+ )
253
+ });
254
+ },
255
+ undo: () => {
256
+ const v = store.getState().venue;
257
+ if (!v) return;
258
+ store.getState().setVenue({
259
+ ...v,
260
+ sections: v.sections.map(
261
+ (sec) => sec.id === sectionId ? {
262
+ ...sec,
263
+ rows: sec.rows.map((r) => ({
264
+ ...r,
265
+ seats: r.seats.map((st) => {
266
+ const op = originals.get(st.id);
267
+ return op ? { ...st, position: op.pos } : st;
268
+ })
269
+ }))
270
+ } : sec
271
+ )
272
+ });
273
+ }
274
+ });
275
+ }
276
+ if (this.dragMode.type === "section") {
277
+ const { sectionId, origPos } = this.dragMode;
278
+ const section = venue.sections.find((s) => s.id === sectionId);
279
+ if (!section) return;
280
+ const finalPos = { ...section.position };
281
+ this.history.execute({
282
+ description: `Move section`,
283
+ execute: () => {
284
+ const v = store.getState().venue;
285
+ if (!v) return;
286
+ store.getState().setVenue({
287
+ ...v,
288
+ sections: v.sections.map(
289
+ (s) => s.id === sectionId ? { ...s, position: finalPos } : s
290
+ )
291
+ });
292
+ },
293
+ undo: () => {
294
+ const v = store.getState().venue;
295
+ if (!v) return;
296
+ store.getState().setVenue({
297
+ ...v,
298
+ sections: v.sections.map(
299
+ (s) => s.id === sectionId ? { ...s, position: origPos } : s
300
+ )
301
+ });
302
+ }
303
+ });
304
+ }
305
+ }
306
+ reset() {
307
+ this.isDragging = false;
308
+ this.hasDragged = false;
309
+ this.selectionRect = null;
310
+ this.dragMode = { type: "none" };
311
+ }
312
+ onDeactivate() {
313
+ this.reset();
314
+ }
315
+ };
316
+ var CLOSE_THRESHOLD = 15;
317
+ var AddSectionTool = class extends BaseTool {
318
+ constructor(history, categoryId = "") {
319
+ super();
320
+ this.history = history;
321
+ this.categoryId = categoryId;
322
+ }
323
+ name = "add-section";
324
+ cursor = "crosshair";
325
+ points = [];
326
+ onPointsChange;
327
+ setCategoryId(id) {
328
+ this.categoryId = id;
329
+ }
330
+ onPointerDown(e, _viewport, store) {
331
+ if (this.points.length >= 3) {
332
+ const first = this.points[0];
333
+ const dist = Math.hypot(e.worldX - first.x, e.worldY - first.y);
334
+ if (dist < CLOSE_THRESHOLD) {
335
+ this.finishPolygon(store);
336
+ return;
337
+ }
338
+ }
339
+ this.points.push({ x: e.worldX, y: e.worldY });
340
+ this.notifyChange();
341
+ }
342
+ onPointerMove(e) {
343
+ if (this.points.length === 0) return;
344
+ const closeable = this.points.length >= 3 && Math.hypot(e.worldX - this.points[0].x, e.worldY - this.points[0].y) < CLOSE_THRESHOLD;
345
+ this.onPointsChange?.(this.points, closeable);
346
+ }
347
+ finishPolygon(store) {
348
+ if (this.points.length < 3) {
349
+ this.points = [];
350
+ this.notifyChange();
351
+ return;
352
+ }
353
+ let cx = 0, cy = 0;
354
+ for (const p of this.points) {
355
+ cx += p.x;
356
+ cy += p.y;
357
+ }
358
+ cx /= this.points.length;
359
+ cy /= this.points.length;
360
+ const outline = this.points.map((p) => ({
361
+ x: p.x - cx,
362
+ y: p.y - cy
363
+ }));
364
+ const newSection = {
365
+ id: generateId("sec"),
366
+ label: `Section ${Date.now().toString(36).slice(-3).toUpperCase()}`,
367
+ position: { x: cx, y: cy },
368
+ rotation: 0,
369
+ categoryId: this.categoryId,
370
+ rows: [],
371
+ outline
372
+ };
373
+ this.history.execute({
374
+ description: `Add section "${newSection.label}"`,
375
+ execute: () => {
376
+ const v = store.getState().venue;
377
+ if (!v) return;
378
+ store.getState().setVenue({ ...v, sections: [...v.sections, newSection] });
379
+ },
380
+ undo: () => {
381
+ const v = store.getState().venue;
382
+ if (!v) return;
383
+ store.getState().setVenue({
384
+ ...v,
385
+ sections: v.sections.filter((s) => s.id !== newSection.id)
386
+ });
387
+ }
388
+ });
389
+ this.points = [];
390
+ this.notifyChange();
391
+ }
392
+ notifyChange() {
393
+ this.onPointsChange?.(this.points, false);
394
+ }
395
+ onDeactivate() {
396
+ this.points = [];
397
+ this.notifyChange();
398
+ }
399
+ };
400
+ var ROW_GAP = 22;
401
+ var AddRowTool = class extends BaseTool {
402
+ constructor(history, spatialIndex) {
403
+ super();
404
+ this.history = history;
405
+ this.spatialIndex = spatialIndex;
406
+ }
407
+ name = "add-row";
408
+ cursor = "cell";
409
+ seatsPerRow = 10;
410
+ seatSpacing = 20;
411
+ onPointerDown(e, _viewport, store) {
412
+ const hits = this.spatialIndex.queryPoint({ x: e.worldX, y: e.worldY }, 50);
413
+ const sectionHit = hits.find((h) => h.type === "section");
414
+ if (!sectionHit) return;
415
+ const venue = store.getState().venue;
416
+ if (!venue) return;
417
+ const section = venue.sections.find((s) => s.id === sectionHit.sectionId);
418
+ if (!section) return;
419
+ const cos = Math.cos(-section.rotation);
420
+ const sin = Math.sin(-section.rotation);
421
+ const relX = e.worldX - section.position.x;
422
+ const relY = e.worldY - section.position.y;
423
+ let targetY = relX * sin + relY * cos;
424
+ const existingYs = section.rows.flatMap((r) => r.seats.map((s) => s.position.y)).filter((y, i, arr) => arr.indexOf(y) === i).sort((a, b) => a - b);
425
+ for (const ey of existingYs) {
426
+ if (Math.abs(targetY - ey) < ROW_GAP) {
427
+ targetY = ey + ROW_GAP;
428
+ }
429
+ }
430
+ const hasOutline = section.outline.length >= 3;
431
+ const allSeats = [];
432
+ const startX = -((this.seatsPerRow - 1) * this.seatSpacing) / 2;
433
+ for (let i = 0; i < this.seatsPerRow; i++) {
434
+ const pos = { x: startX + i * this.seatSpacing, y: targetY };
435
+ if (hasOutline && !pointInPolygon(pos, section.outline)) continue;
436
+ allSeats.push({
437
+ id: generateId("seat"),
438
+ label: `${allSeats.length + 1}`,
439
+ position: pos,
440
+ status: "available",
441
+ categoryId: section.categoryId
442
+ });
443
+ }
444
+ const seats = allSeats;
445
+ if (seats.length === 0) return;
446
+ const rowLabel = String.fromCharCode(65 + section.rows.length);
447
+ const newRow = {
448
+ id: generateId("row"),
449
+ label: rowLabel,
450
+ seats
451
+ };
452
+ const sectionId = section.id;
453
+ this.history.execute({
454
+ description: `Add row ${rowLabel} to "${section.label}"`,
455
+ execute: () => {
456
+ const v = store.getState().venue;
457
+ if (!v) return;
458
+ store.getState().setVenue({
459
+ ...v,
460
+ sections: v.sections.map(
461
+ (s) => s.id === sectionId ? { ...s, rows: [...s.rows, newRow] } : s
462
+ )
463
+ });
464
+ },
465
+ undo: () => {
466
+ const v = store.getState().venue;
467
+ if (!v) return;
468
+ store.getState().setVenue({
469
+ ...v,
470
+ sections: v.sections.map(
471
+ (s) => s.id === sectionId ? { ...s, rows: s.rows.filter((r) => r.id !== newRow.id) } : s
472
+ )
473
+ });
474
+ }
475
+ });
476
+ }
477
+ };
478
+ var GRID2 = 20;
479
+ var MIN_SEAT_DIST = 16;
480
+ function snapToGrid2(v) {
481
+ return Math.round(v / GRID2) * GRID2;
482
+ }
483
+ var AddSeatTool = class extends BaseTool {
484
+ constructor(history, spatialIndex) {
485
+ super();
486
+ this.history = history;
487
+ this.spatialIndex = spatialIndex;
488
+ }
489
+ name = "add-seat";
490
+ cursor = "crosshair";
491
+ onPointerDown(e, _viewport, store) {
492
+ const hits = this.spatialIndex.queryPoint({ x: e.worldX, y: e.worldY }, 50);
493
+ const sectionHit = hits.find((h) => h.type === "section");
494
+ if (!sectionHit) return;
495
+ const venue = store.getState().venue;
496
+ if (!venue) return;
497
+ const section = venue.sections.find((s) => s.id === sectionHit.sectionId);
498
+ if (!section) return;
499
+ const relX = e.worldX - section.position.x;
500
+ const relY = e.worldY - section.position.y;
501
+ const c = Math.cos(-section.rotation);
502
+ const s2 = Math.sin(-section.rotation);
503
+ let lx = snapToGrid2(relX * c - relY * s2);
504
+ let ly = snapToGrid2(relX * s2 + relY * c);
505
+ if (section.outline.length >= 3 && !pointInPolygon({ x: lx, y: ly }, section.outline)) {
506
+ return;
507
+ }
508
+ const existing = [];
509
+ for (const row of section.rows) {
510
+ for (const seat of row.seats) {
511
+ existing.push(seat.position);
512
+ }
513
+ }
514
+ lx = this.findNonOverlapping(lx, ly, existing);
515
+ let bestRow = null;
516
+ let bestDist = Infinity;
517
+ for (const row of section.rows) {
518
+ if (row.seats.length === 0) continue;
519
+ const rowY = row.seats[0].position.y;
520
+ const dist = Math.abs(ly - rowY);
521
+ if (dist < MIN_SEAT_DIST && dist < bestDist) {
522
+ bestDist = dist;
523
+ bestRow = row;
524
+ }
525
+ }
526
+ const sectionId = section.id;
527
+ if (bestRow) {
528
+ const rowId = bestRow.id;
529
+ const snappedY = bestRow.seats[0].position.y;
530
+ const existingInRow = bestRow.seats.map((s) => s.position);
531
+ const finalX = this.findNonOverlapping(lx, snappedY, existingInRow);
532
+ const newSeat = {
533
+ id: generateId("seat"),
534
+ label: `${bestRow.seats.length + 1}`,
535
+ position: { x: finalX, y: snappedY },
536
+ status: "available",
537
+ categoryId: section.categoryId
538
+ };
539
+ this.history.execute({
540
+ description: `Add seat to row "${bestRow.label}"`,
541
+ execute: () => {
542
+ const v = store.getState().venue;
543
+ if (!v) return;
544
+ store.getState().setVenue({
545
+ ...v,
546
+ sections: v.sections.map(
547
+ (sec) => sec.id === sectionId ? { ...sec, rows: sec.rows.map((r) => r.id === rowId ? { ...r, seats: [...r.seats, newSeat] } : r) } : sec
548
+ )
549
+ });
550
+ },
551
+ undo: () => {
552
+ const v = store.getState().venue;
553
+ if (!v) return;
554
+ store.getState().setVenue({
555
+ ...v,
556
+ sections: v.sections.map(
557
+ (sec) => sec.id === sectionId ? { ...sec, rows: sec.rows.map((r) => r.id === rowId ? { ...r, seats: r.seats.filter((st) => st.id !== newSeat.id) } : r) } : sec
558
+ )
559
+ });
560
+ }
561
+ });
562
+ } else {
563
+ const rowLabel = String.fromCharCode(65 + section.rows.length);
564
+ const newSeat = {
565
+ id: generateId("seat"),
566
+ label: "1",
567
+ position: { x: lx, y: ly },
568
+ status: "available",
569
+ categoryId: section.categoryId
570
+ };
571
+ const newRow = { id: generateId("row"), label: rowLabel, seats: [newSeat] };
572
+ this.history.execute({
573
+ description: `Add seat in new row ${rowLabel}`,
574
+ execute: () => {
575
+ const v = store.getState().venue;
576
+ if (!v) return;
577
+ store.getState().setVenue({
578
+ ...v,
579
+ sections: v.sections.map(
580
+ (sec) => sec.id === sectionId ? { ...sec, rows: [...sec.rows, newRow] } : sec
581
+ )
582
+ });
583
+ },
584
+ undo: () => {
585
+ const v = store.getState().venue;
586
+ if (!v) return;
587
+ store.getState().setVenue({
588
+ ...v,
589
+ sections: v.sections.map(
590
+ (sec) => sec.id === sectionId ? { ...sec, rows: sec.rows.filter((r) => r.id !== newRow.id) } : sec
591
+ )
592
+ });
593
+ }
594
+ });
595
+ }
596
+ }
597
+ findNonOverlapping(x, y, existing) {
598
+ let candidate = snapToGrid2(x);
599
+ for (let attempt = 0; attempt < 20; attempt++) {
600
+ const overlaps = existing.some(
601
+ (p) => Math.hypot(p.x - candidate, p.y - y) < MIN_SEAT_DIST
602
+ );
603
+ if (!overlaps) return candidate;
604
+ candidate += GRID2;
605
+ }
606
+ return candidate;
607
+ }
608
+ };
609
+ var tools = [
610
+ { id: "pan", label: "Pan", icon: "\u270B" },
611
+ { id: "select", label: "Select", icon: "\u2196" },
612
+ { id: "add-section", label: "Section", icon: "\u25A2" },
613
+ { id: "add-row", label: "Row", icon: "\u22EF" },
614
+ { id: "add-seat", label: "Seat", icon: "+" }
615
+ ];
616
+ var btnBase = {
617
+ padding: "6px 10px",
618
+ border: "1px solid #3a3a5a",
619
+ borderRadius: 6,
620
+ background: "#2a2a4a",
621
+ color: "#e0e0e0",
622
+ cursor: "pointer",
623
+ fontSize: 13,
624
+ fontFamily: "system-ui",
625
+ display: "flex",
626
+ alignItems: "center",
627
+ gap: 4
628
+ };
629
+ var activeBtnStyle = {
630
+ ...btnBase,
631
+ background: "#4a4a7a",
632
+ borderColor: "#6a6aaa"
633
+ };
634
+ function Toolbar({
635
+ activeTool,
636
+ onToolChange,
637
+ canUndo,
638
+ canRedo,
639
+ onUndo,
640
+ onRedo,
641
+ onFitView,
642
+ onSave,
643
+ onLoad,
644
+ seatsPerRow,
645
+ onSeatsPerRowChange,
646
+ style
647
+ }) {
648
+ return /* @__PURE__ */ jsxs(
649
+ "div",
650
+ {
651
+ style: {
652
+ display: "flex",
653
+ gap: 4,
654
+ padding: "8px 12px",
655
+ background: "#1a1a2e",
656
+ borderBottom: "1px solid #2a2a4a",
657
+ alignItems: "center",
658
+ flexWrap: "wrap",
659
+ ...style
660
+ },
661
+ children: [
662
+ tools.map((tool) => /* @__PURE__ */ jsxs(
663
+ "button",
664
+ {
665
+ onClick: () => onToolChange(tool.id),
666
+ style: activeTool === tool.id ? activeBtnStyle : btnBase,
667
+ title: tool.label,
668
+ children: [
669
+ /* @__PURE__ */ jsx("span", { children: tool.icon }),
670
+ /* @__PURE__ */ jsx("span", { children: tool.label })
671
+ ]
672
+ },
673
+ tool.id
674
+ )),
675
+ activeTool === "add-row" && /* @__PURE__ */ jsxs(Fragment, { children: [
676
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 24, background: "#3a3a5a", margin: "0 6px" } }),
677
+ /* @__PURE__ */ jsxs("label", { style: { color: "#9e9e9e", fontSize: 12, fontFamily: "system-ui", display: "flex", alignItems: "center", gap: 4 }, children: [
678
+ "Seats/row:",
679
+ /* @__PURE__ */ jsx(
680
+ "input",
681
+ {
682
+ type: "number",
683
+ min: 1,
684
+ max: 100,
685
+ value: seatsPerRow,
686
+ onChange: (e) => onSeatsPerRowChange(Math.max(1, Math.min(100, parseInt(e.target.value) || 1))),
687
+ style: {
688
+ width: 50,
689
+ padding: "3px 6px",
690
+ background: "#2a2a4a",
691
+ border: "1px solid #3a3a5a",
692
+ borderRadius: 4,
693
+ color: "#e0e0e0",
694
+ fontSize: 13,
695
+ fontFamily: "system-ui"
696
+ }
697
+ }
698
+ )
699
+ ] })
700
+ ] }),
701
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 24, background: "#3a3a5a", margin: "0 6px" } }),
702
+ /* @__PURE__ */ jsx("button", { onClick: onUndo, disabled: !canUndo, style: { ...btnBase, opacity: canUndo ? 1 : 0.4 }, title: "Undo (Ctrl+Z)", children: "\u21A9 Undo" }),
703
+ /* @__PURE__ */ jsx("button", { onClick: onRedo, disabled: !canRedo, style: { ...btnBase, opacity: canRedo ? 1 : 0.4 }, title: "Redo (Ctrl+Shift+Z)", children: "\u21AA Redo" }),
704
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 24, background: "#3a3a5a", margin: "0 6px" } }),
705
+ /* @__PURE__ */ jsx("button", { onClick: onFitView, style: btnBase, title: "Fit to view", children: "\u229E Fit" }),
706
+ /* @__PURE__ */ jsx("div", { style: { width: 1, height: 24, background: "#3a3a5a", margin: "0 6px" } }),
707
+ /* @__PURE__ */ jsx("button", { onClick: onSave, style: btnBase, title: "Export venue as JSON", children: "\u2193 Save" }),
708
+ /* @__PURE__ */ jsx("button", { onClick: onLoad, style: btnBase, title: "Import venue from JSON", children: "\u2191 Load" })
709
+ ]
710
+ }
711
+ );
712
+ }
713
+ var labelStyle = {
714
+ fontSize: 11,
715
+ color: "#9e9e9e",
716
+ marginBottom: 2,
717
+ fontFamily: "system-ui"
718
+ };
719
+ var inputStyle = {
720
+ width: "100%",
721
+ padding: "4px 8px",
722
+ background: "#2a2a4a",
723
+ border: "1px solid #3a3a5a",
724
+ borderRadius: 4,
725
+ color: "#e0e0e0",
726
+ fontSize: 13,
727
+ fontFamily: "system-ui",
728
+ boxSizing: "border-box"
729
+ };
730
+ var selectStyle = { ...inputStyle, cursor: "pointer" };
731
+ var btnDanger = {
732
+ padding: "3px 8px",
733
+ border: "1px solid #5a2a2a",
734
+ borderRadius: 4,
735
+ background: "#3a1a1a",
736
+ color: "#f48888",
737
+ cursor: "pointer",
738
+ fontSize: 12,
739
+ fontFamily: "system-ui"
740
+ };
741
+ var btnSmall = {
742
+ padding: "3px 8px",
743
+ border: "1px solid #3a3a5a",
744
+ borderRadius: 4,
745
+ background: "#2a2a4a",
746
+ color: "#e0e0e0",
747
+ cursor: "pointer",
748
+ fontSize: 12,
749
+ fontFamily: "system-ui"
750
+ };
751
+ function freshVenue(store) {
752
+ return store.getState().venue;
753
+ }
754
+ function setVenue(store, venue) {
755
+ store.getState().setVenue(venue);
756
+ }
757
+ function PropertyPanel({
758
+ venue,
759
+ selectedSeatIds,
760
+ history,
761
+ store,
762
+ onUploadBackground,
763
+ onRemoveBackground,
764
+ onBackgroundOpacityChange,
765
+ style
766
+ }) {
767
+ const [selectedSection, setSelectedSection] = useState(null);
768
+ useEffect(() => {
769
+ if (!venue || selectedSeatIds.size === 0) {
770
+ setSelectedSection(null);
771
+ return;
772
+ }
773
+ const firstSeatId = [...selectedSeatIds][0];
774
+ for (const section of venue.sections) {
775
+ for (const row of section.rows) {
776
+ if (row.seats.some((s) => s.id === firstSeatId)) {
777
+ setSelectedSection(section);
778
+ return;
779
+ }
780
+ }
781
+ }
782
+ setSelectedSection(null);
783
+ }, [venue, selectedSeatIds]);
784
+ const updateSectionLabel = (sectionId, newLabel) => {
785
+ const v = freshVenue(store);
786
+ if (!v) return;
787
+ const oldLabel = v.sections.find((s) => s.id === sectionId)?.label ?? "";
788
+ history.execute({
789
+ description: `Rename section to "${newLabel}"`,
790
+ execute: () => {
791
+ const cur = freshVenue(store);
792
+ if (!cur) return;
793
+ setVenue(store, {
794
+ ...cur,
795
+ sections: cur.sections.map(
796
+ (s) => s.id === sectionId ? { ...s, label: newLabel } : s
797
+ )
798
+ });
799
+ },
800
+ undo: () => {
801
+ const cur = freshVenue(store);
802
+ if (!cur) return;
803
+ setVenue(store, {
804
+ ...cur,
805
+ sections: cur.sections.map(
806
+ (s) => s.id === sectionId ? { ...s, label: oldLabel } : s
807
+ )
808
+ });
809
+ }
810
+ });
811
+ };
812
+ const updateSectionCategory = (sectionId, categoryId) => {
813
+ const v = freshVenue(store);
814
+ if (!v) return;
815
+ const oldCatId = v.sections.find((s) => s.id === sectionId)?.categoryId ?? "";
816
+ history.execute({
817
+ description: `Change section category`,
818
+ execute: () => {
819
+ const cur = freshVenue(store);
820
+ if (!cur) return;
821
+ setVenue(store, {
822
+ ...cur,
823
+ sections: cur.sections.map(
824
+ (s) => s.id === sectionId ? {
825
+ ...s,
826
+ categoryId,
827
+ rows: s.rows.map((r) => ({
828
+ ...r,
829
+ seats: r.seats.map((seat) => ({ ...seat, categoryId }))
830
+ }))
831
+ } : s
832
+ )
833
+ });
834
+ },
835
+ undo: () => {
836
+ const cur = freshVenue(store);
837
+ if (!cur) return;
838
+ setVenue(store, {
839
+ ...cur,
840
+ sections: cur.sections.map(
841
+ (s) => s.id === sectionId ? {
842
+ ...s,
843
+ categoryId: oldCatId,
844
+ rows: s.rows.map((r) => ({
845
+ ...r,
846
+ seats: r.seats.map((seat) => ({ ...seat, categoryId: oldCatId }))
847
+ }))
848
+ } : s
849
+ )
850
+ });
851
+ }
852
+ });
853
+ };
854
+ const deleteSection = (sectionId) => {
855
+ const v = freshVenue(store);
856
+ if (!v) return;
857
+ const removed = v.sections.find((s) => s.id === sectionId);
858
+ if (!removed) return;
859
+ history.execute({
860
+ description: `Delete section "${removed.label}"`,
861
+ execute: () => {
862
+ const cur = freshVenue(store);
863
+ if (!cur) return;
864
+ setVenue(store, { ...cur, sections: cur.sections.filter((s) => s.id !== sectionId) });
865
+ store.getState().clearSelection();
866
+ },
867
+ undo: () => {
868
+ const cur = freshVenue(store);
869
+ if (!cur) return;
870
+ setVenue(store, { ...cur, sections: [...cur.sections, removed] });
871
+ }
872
+ });
873
+ };
874
+ const deleteRow = (sectionId, rowId) => {
875
+ const v = freshVenue(store);
876
+ if (!v) return;
877
+ const sec = v.sections.find((s) => s.id === sectionId);
878
+ const removed = sec?.rows.find((r) => r.id === rowId);
879
+ if (!removed) return;
880
+ history.execute({
881
+ description: `Delete row "${removed.label}"`,
882
+ execute: () => {
883
+ const cur = freshVenue(store);
884
+ if (!cur) return;
885
+ setVenue(store, {
886
+ ...cur,
887
+ sections: cur.sections.map(
888
+ (s) => s.id === sectionId ? { ...s, rows: s.rows.filter((r) => r.id !== rowId) } : s
889
+ )
890
+ });
891
+ },
892
+ undo: () => {
893
+ const cur = freshVenue(store);
894
+ if (!cur) return;
895
+ setVenue(store, {
896
+ ...cur,
897
+ sections: cur.sections.map(
898
+ (s) => s.id === sectionId ? { ...s, rows: [...s.rows, removed] } : s
899
+ )
900
+ });
901
+ }
902
+ });
903
+ };
904
+ const deleteSeat = (sectionId, rowId, seatId) => {
905
+ const v = freshVenue(store);
906
+ if (!v) return;
907
+ const sec = v.sections.find((s) => s.id === sectionId);
908
+ const row = sec?.rows.find((r) => r.id === rowId);
909
+ const removed = row?.seats.find((s) => s.id === seatId);
910
+ if (!removed) return;
911
+ history.execute({
912
+ description: `Delete seat "${removed.label}"`,
913
+ execute: () => {
914
+ const cur = freshVenue(store);
915
+ if (!cur) return;
916
+ setVenue(store, {
917
+ ...cur,
918
+ sections: cur.sections.map(
919
+ (s) => s.id === sectionId ? {
920
+ ...s,
921
+ rows: s.rows.map(
922
+ (r) => r.id === rowId ? { ...r, seats: r.seats.filter((st) => st.id !== seatId) } : r
923
+ )
924
+ } : s
925
+ )
926
+ });
927
+ const sel = store.getState().selectedSeatIds;
928
+ if (sel.has(seatId)) store.getState().deselectSeat(seatId);
929
+ },
930
+ undo: () => {
931
+ const cur = freshVenue(store);
932
+ if (!cur) return;
933
+ setVenue(store, {
934
+ ...cur,
935
+ sections: cur.sections.map(
936
+ (s) => s.id === sectionId ? {
937
+ ...s,
938
+ rows: s.rows.map(
939
+ (r) => r.id === rowId ? { ...r, seats: [...r.seats, removed] } : r
940
+ )
941
+ } : s
942
+ )
943
+ });
944
+ }
945
+ });
946
+ };
947
+ const addSingleSeat = (sectionId) => {
948
+ const v = freshVenue(store);
949
+ if (!v) return;
950
+ const sec = v.sections.find((s) => s.id === sectionId);
951
+ if (!sec) return;
952
+ let targetRow = sec.rows[sec.rows.length - 1];
953
+ const newSeat = {
954
+ id: generateId("seat"),
955
+ label: targetRow ? `${targetRow.seats.length + 1}` : "1",
956
+ position: {
957
+ x: targetRow ? targetRow.seats.length > 0 ? targetRow.seats[targetRow.seats.length - 1].position.x + 20 : 0 : 0,
958
+ y: targetRow ? targetRow.seats[0]?.position.y ?? 0 : 0
959
+ },
960
+ status: "available",
961
+ categoryId: sec.categoryId
962
+ };
963
+ if (!targetRow) {
964
+ const newRow = { id: generateId("row"), label: "A", seats: [newSeat] };
965
+ history.execute({
966
+ description: `Add seat to new row`,
967
+ execute: () => {
968
+ const cur = freshVenue(store);
969
+ if (!cur) return;
970
+ setVenue(store, {
971
+ ...cur,
972
+ sections: cur.sections.map(
973
+ (s) => s.id === sectionId ? { ...s, rows: [...s.rows, newRow] } : s
974
+ )
975
+ });
976
+ },
977
+ undo: () => {
978
+ const cur = freshVenue(store);
979
+ if (!cur) return;
980
+ setVenue(store, {
981
+ ...cur,
982
+ sections: cur.sections.map(
983
+ (s) => s.id === sectionId ? { ...s, rows: s.rows.filter((r) => r.id !== newRow.id) } : s
984
+ )
985
+ });
986
+ }
987
+ });
988
+ return;
989
+ }
990
+ const rowId = targetRow.id;
991
+ history.execute({
992
+ description: `Add seat to row "${targetRow.label}"`,
993
+ execute: () => {
994
+ const cur = freshVenue(store);
995
+ if (!cur) return;
996
+ setVenue(store, {
997
+ ...cur,
998
+ sections: cur.sections.map(
999
+ (s) => s.id === sectionId ? {
1000
+ ...s,
1001
+ rows: s.rows.map(
1002
+ (r) => r.id === rowId ? { ...r, seats: [...r.seats, newSeat] } : r
1003
+ )
1004
+ } : s
1005
+ )
1006
+ });
1007
+ },
1008
+ undo: () => {
1009
+ const cur = freshVenue(store);
1010
+ if (!cur) return;
1011
+ setVenue(store, {
1012
+ ...cur,
1013
+ sections: cur.sections.map(
1014
+ (s) => s.id === sectionId ? {
1015
+ ...s,
1016
+ rows: s.rows.map(
1017
+ (r) => r.id === rowId ? { ...r, seats: r.seats.filter((st) => st.id !== newSeat.id) } : r
1018
+ )
1019
+ } : s
1020
+ )
1021
+ });
1022
+ }
1023
+ });
1024
+ };
1025
+ if (!venue) {
1026
+ return /* @__PURE__ */ jsx("div", { style: { padding: 16, color: "#9e9e9e", fontSize: 13, fontFamily: "system-ui", ...style }, children: "No venue loaded" });
1027
+ }
1028
+ if (!selectedSection) {
1029
+ return /* @__PURE__ */ jsxs("div", { style: { padding: 16, ...style }, children: [
1030
+ /* @__PURE__ */ jsx("div", { style: { color: "#9e9e9e", fontSize: 13, fontFamily: "system-ui", marginBottom: 12 }, children: selectedSeatIds.size === 0 ? "Select seats to edit section properties" : `${selectedSeatIds.size} seat(s) selected` }),
1031
+ /* @__PURE__ */ jsx("div", { style: labelStyle, children: "Venue" }),
1032
+ /* @__PURE__ */ jsx("div", { style: { color: "#e0e0e0", fontSize: 14, fontFamily: "system-ui" }, children: venue.name }),
1033
+ /* @__PURE__ */ jsxs("div", { style: { ...labelStyle, marginTop: 12 }, children: [
1034
+ "Sections: ",
1035
+ venue.sections.length
1036
+ ] }),
1037
+ /* @__PURE__ */ jsxs("div", { style: { ...labelStyle, marginTop: 4 }, children: [
1038
+ "Seats: ",
1039
+ venue.sections.reduce((t, s) => t + s.rows.reduce((rt, r) => rt + r.seats.length, 0), 0)
1040
+ ] }),
1041
+ /* @__PURE__ */ jsx("div", { style: { height: 1, background: "#2a2a4a", margin: "14px 0" } }),
1042
+ /* @__PURE__ */ jsx("div", { style: labelStyle, children: "Background Image" }),
1043
+ venue.backgroundImage ? /* @__PURE__ */ jsxs("div", { style: { marginTop: 6 }, children: [
1044
+ /* @__PURE__ */ jsx(
1045
+ "div",
1046
+ {
1047
+ style: {
1048
+ width: "100%",
1049
+ height: 80,
1050
+ borderRadius: 4,
1051
+ border: "1px solid #3a3a5a",
1052
+ overflow: "hidden",
1053
+ marginBottom: 8
1054
+ },
1055
+ children: /* @__PURE__ */ jsx(
1056
+ "img",
1057
+ {
1058
+ src: venue.backgroundImage,
1059
+ alt: "Background",
1060
+ style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
1061
+ }
1062
+ )
1063
+ }
1064
+ ),
1065
+ /* @__PURE__ */ jsxs("div", { style: { ...labelStyle, marginBottom: 4 }, children: [
1066
+ "Opacity: ",
1067
+ Math.round((venue.backgroundImageOpacity ?? 0.5) * 100),
1068
+ "%"
1069
+ ] }),
1070
+ /* @__PURE__ */ jsx(
1071
+ "input",
1072
+ {
1073
+ type: "range",
1074
+ min: 0,
1075
+ max: 100,
1076
+ value: Math.round((venue.backgroundImageOpacity ?? 0.5) * 100),
1077
+ onChange: (e) => onBackgroundOpacityChange?.(parseInt(e.target.value) / 100),
1078
+ style: { width: "100%", accentColor: "#6a6aaa", cursor: "pointer" }
1079
+ }
1080
+ ),
1081
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, marginTop: 8 }, children: [
1082
+ /* @__PURE__ */ jsx("button", { onClick: onUploadBackground, style: btnSmall, children: "Replace" }),
1083
+ /* @__PURE__ */ jsx("button", { onClick: onRemoveBackground, style: btnDanger, children: "Remove" })
1084
+ ] })
1085
+ ] }) : /* @__PURE__ */ jsx(
1086
+ "button",
1087
+ {
1088
+ onClick: onUploadBackground,
1089
+ style: { ...btnSmall, marginTop: 6, width: "100%" },
1090
+ children: "Upload Image"
1091
+ }
1092
+ )
1093
+ ] });
1094
+ }
1095
+ const selectedSeatList = [];
1096
+ for (const row of selectedSection.rows) {
1097
+ for (const seat of row.seats) {
1098
+ if (selectedSeatIds.has(seat.id)) {
1099
+ selectedSeatList.push({ seat, row });
1100
+ }
1101
+ }
1102
+ }
1103
+ return /* @__PURE__ */ jsxs("div", { style: { padding: 16, ...style }, children: [
1104
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
1105
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, color: "#e0e0e0", fontSize: 14, fontFamily: "system-ui" }, children: "Section" }),
1106
+ /* @__PURE__ */ jsx("button", { onClick: () => deleteSection(selectedSection.id), style: btnDanger, title: "Delete section", children: "Delete" })
1107
+ ] }),
1108
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 10 }, children: [
1109
+ /* @__PURE__ */ jsx("div", { style: labelStyle, children: "Label" }),
1110
+ /* @__PURE__ */ jsx(
1111
+ "input",
1112
+ {
1113
+ style: inputStyle,
1114
+ value: selectedSection.label,
1115
+ onChange: (e) => updateSectionLabel(selectedSection.id, e.target.value)
1116
+ }
1117
+ )
1118
+ ] }),
1119
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 10 }, children: [
1120
+ /* @__PURE__ */ jsx("div", { style: labelStyle, children: "Category" }),
1121
+ /* @__PURE__ */ jsx(
1122
+ "select",
1123
+ {
1124
+ style: selectStyle,
1125
+ value: selectedSection.categoryId,
1126
+ onChange: (e) => updateSectionCategory(selectedSection.id, e.target.value),
1127
+ children: venue.categories.map((cat) => /* @__PURE__ */ jsx("option", { value: cat.id, children: cat.name }, cat.id))
1128
+ }
1129
+ )
1130
+ ] }),
1131
+ /* @__PURE__ */ jsx("div", { style: { marginBottom: 10 }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" }, children: [
1132
+ /* @__PURE__ */ jsxs("div", { style: labelStyle, children: [
1133
+ "Rows (",
1134
+ selectedSection.rows.length,
1135
+ ") \xB7",
1136
+ " ",
1137
+ selectedSection.rows.reduce((t, r) => t + r.seats.length, 0),
1138
+ " seats"
1139
+ ] }),
1140
+ /* @__PURE__ */ jsx("button", { onClick: () => addSingleSeat(selectedSection.id), style: btnSmall, title: "Add a single seat to the last row", children: "+ Seat" })
1141
+ ] }) }),
1142
+ /* @__PURE__ */ jsx("div", { style: { maxHeight: 200, overflowY: "auto", marginBottom: 10 }, children: selectedSection.rows.map((row) => /* @__PURE__ */ jsxs(
1143
+ "div",
1144
+ {
1145
+ style: {
1146
+ display: "flex",
1147
+ alignItems: "center",
1148
+ gap: 6,
1149
+ padding: "3px 6px",
1150
+ borderRadius: 4,
1151
+ marginBottom: 2,
1152
+ background: "#2a2a4a",
1153
+ fontSize: 12,
1154
+ fontFamily: "system-ui",
1155
+ color: "#e0e0e0"
1156
+ },
1157
+ children: [
1158
+ /* @__PURE__ */ jsxs("span", { style: { fontWeight: 600, minWidth: 24 }, children: [
1159
+ "Row ",
1160
+ row.label
1161
+ ] }),
1162
+ /* @__PURE__ */ jsxs("span", { style: { flex: 1, color: "#9e9e9e" }, children: [
1163
+ row.seats.length,
1164
+ " seats"
1165
+ ] }),
1166
+ /* @__PURE__ */ jsx(
1167
+ "button",
1168
+ {
1169
+ onClick: () => deleteRow(selectedSection.id, row.id),
1170
+ style: { ...btnDanger, padding: "1px 5px", fontSize: 11 },
1171
+ title: `Delete row ${row.label}`,
1172
+ children: "\u2715"
1173
+ }
1174
+ )
1175
+ ]
1176
+ },
1177
+ row.id
1178
+ )) }),
1179
+ selectedSeatList.length > 0 && selectedSeatList.length <= 10 && /* @__PURE__ */ jsxs("div", { style: { marginBottom: 10 }, children: [
1180
+ /* @__PURE__ */ jsxs("div", { style: labelStyle, children: [
1181
+ "Selected Seats (",
1182
+ selectedSeatList.length,
1183
+ ")"
1184
+ ] }),
1185
+ /* @__PURE__ */ jsx("div", { style: { maxHeight: 120, overflowY: "auto" }, children: selectedSeatList.map(({ seat, row }) => /* @__PURE__ */ jsxs(
1186
+ "div",
1187
+ {
1188
+ style: {
1189
+ display: "flex",
1190
+ alignItems: "center",
1191
+ gap: 6,
1192
+ padding: "2px 6px",
1193
+ fontSize: 12,
1194
+ fontFamily: "system-ui",
1195
+ color: "#e0e0e0"
1196
+ },
1197
+ children: [
1198
+ /* @__PURE__ */ jsxs("span", { style: { flex: 1 }, children: [
1199
+ "Row ",
1200
+ row.label,
1201
+ ", Seat ",
1202
+ seat.label
1203
+ ] }),
1204
+ /* @__PURE__ */ jsx(
1205
+ "button",
1206
+ {
1207
+ onClick: () => deleteSeat(selectedSection.id, row.id, seat.id),
1208
+ style: { ...btnDanger, padding: "1px 5px", fontSize: 11 },
1209
+ title: "Delete seat",
1210
+ children: "\u2715"
1211
+ }
1212
+ )
1213
+ ]
1214
+ },
1215
+ seat.id
1216
+ )) })
1217
+ ] }),
1218
+ selectedSeatList.length > 10 && /* @__PURE__ */ jsxs("div", { style: { ...labelStyle, marginBottom: 10 }, children: [
1219
+ selectedSeatList.length,
1220
+ " seats selected"
1221
+ ] })
1222
+ ] });
1223
+ }
1224
+ var btnSmall2 = {
1225
+ padding: "3px 8px",
1226
+ border: "1px solid #3a3a5a",
1227
+ borderRadius: 4,
1228
+ background: "#2a2a4a",
1229
+ color: "#e0e0e0",
1230
+ cursor: "pointer",
1231
+ fontSize: 12,
1232
+ fontFamily: "system-ui"
1233
+ };
1234
+ function CategoryManager({
1235
+ venue,
1236
+ history,
1237
+ store,
1238
+ style
1239
+ }) {
1240
+ const [newName, setNewName] = useState("");
1241
+ const [newColor, setNewColor] = useState("#4caf50");
1242
+ if (!venue) return null;
1243
+ const addCategory = () => {
1244
+ if (!newName.trim()) return;
1245
+ const cat = {
1246
+ id: generateId("cat"),
1247
+ name: newName.trim(),
1248
+ color: newColor
1249
+ };
1250
+ history.execute({
1251
+ description: `Add category "${cat.name}"`,
1252
+ execute: () => {
1253
+ const cur = store.getState().venue;
1254
+ if (!cur) return;
1255
+ store.getState().setVenue({ ...cur, categories: [...cur.categories, cat] });
1256
+ },
1257
+ undo: () => {
1258
+ const cur = store.getState().venue;
1259
+ if (!cur) return;
1260
+ store.getState().setVenue({ ...cur, categories: cur.categories.filter((c) => c.id !== cat.id) });
1261
+ }
1262
+ });
1263
+ setNewName("");
1264
+ };
1265
+ const removeCategory = (catId) => {
1266
+ const cat = venue.categories.find((c) => c.id === catId);
1267
+ if (!cat) return;
1268
+ history.execute({
1269
+ description: `Remove category "${cat.name}"`,
1270
+ execute: () => {
1271
+ const cur = store.getState().venue;
1272
+ if (!cur) return;
1273
+ store.getState().setVenue({ ...cur, categories: cur.categories.filter((c) => c.id !== catId) });
1274
+ },
1275
+ undo: () => {
1276
+ const cur = store.getState().venue;
1277
+ if (!cur) return;
1278
+ store.getState().setVenue({ ...cur, categories: [...cur.categories, cat] });
1279
+ }
1280
+ });
1281
+ };
1282
+ return /* @__PURE__ */ jsxs("div", { style: { padding: 16, ...style }, children: [
1283
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, color: "#e0e0e0", fontSize: 14, fontFamily: "system-ui", marginBottom: 12 }, children: "Pricing Categories" }),
1284
+ venue.categories.map((cat) => /* @__PURE__ */ jsxs(
1285
+ "div",
1286
+ {
1287
+ style: {
1288
+ display: "flex",
1289
+ alignItems: "center",
1290
+ gap: 8,
1291
+ marginBottom: 6,
1292
+ padding: "4px 8px",
1293
+ borderRadius: 4,
1294
+ background: "#2a2a4a"
1295
+ },
1296
+ children: [
1297
+ /* @__PURE__ */ jsx("div", { style: { width: 14, height: 14, borderRadius: 3, background: cat.color, flexShrink: 0 } }),
1298
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, color: "#e0e0e0", fontSize: 13, fontFamily: "system-ui" }, children: cat.name }),
1299
+ /* @__PURE__ */ jsx(
1300
+ "button",
1301
+ {
1302
+ onClick: () => removeCategory(cat.id),
1303
+ style: { ...btnSmall2, padding: "1px 6px", fontSize: 11 },
1304
+ children: "\u2715"
1305
+ }
1306
+ )
1307
+ ]
1308
+ },
1309
+ cat.id
1310
+ )),
1311
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, marginTop: 10, alignItems: "center" }, children: [
1312
+ /* @__PURE__ */ jsx(
1313
+ "input",
1314
+ {
1315
+ type: "color",
1316
+ value: newColor,
1317
+ onChange: (e) => setNewColor(e.target.value),
1318
+ style: { width: 28, height: 28, border: "none", padding: 0, cursor: "pointer" }
1319
+ }
1320
+ ),
1321
+ /* @__PURE__ */ jsx(
1322
+ "input",
1323
+ {
1324
+ placeholder: "Category name",
1325
+ value: newName,
1326
+ onChange: (e) => setNewName(e.target.value),
1327
+ onKeyDown: (e) => e.key === "Enter" && addCategory(),
1328
+ style: {
1329
+ flex: 1,
1330
+ padding: "4px 8px",
1331
+ background: "#2a2a4a",
1332
+ border: "1px solid #3a3a5a",
1333
+ borderRadius: 4,
1334
+ color: "#e0e0e0",
1335
+ fontSize: 13,
1336
+ fontFamily: "system-ui"
1337
+ }
1338
+ }
1339
+ ),
1340
+ /* @__PURE__ */ jsx("button", { onClick: addCategory, style: btnSmall2, children: "Add" })
1341
+ ] })
1342
+ ] });
1343
+ }
1344
+ function LayerPanel({
1345
+ venue,
1346
+ selectedSeatIds,
1347
+ onSelectSection,
1348
+ style
1349
+ }) {
1350
+ if (!venue) return null;
1351
+ const findSectionForSeat = (seatId) => {
1352
+ for (const section of venue.sections) {
1353
+ for (const row of section.rows) {
1354
+ if (row.seats.some((s) => s.id === seatId)) return section.id;
1355
+ }
1356
+ }
1357
+ return null;
1358
+ };
1359
+ const selectedSectionId = selectedSeatIds.size > 0 ? findSectionForSeat([...selectedSeatIds][0]) : null;
1360
+ return /* @__PURE__ */ jsxs("div", { style: { padding: 16, ...style }, children: [
1361
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, color: "#e0e0e0", fontSize: 14, fontFamily: "system-ui", marginBottom: 12 }, children: "Layers" }),
1362
+ venue.sections.map((section) => {
1363
+ const seatCount = section.rows.reduce((t, r) => t + r.seats.length, 0);
1364
+ const isActive = section.id === selectedSectionId;
1365
+ const catColor = venue.categories.find((c) => c.id === section.categoryId)?.color ?? "#666";
1366
+ return /* @__PURE__ */ jsxs(
1367
+ "div",
1368
+ {
1369
+ onClick: () => onSelectSection(section.id),
1370
+ style: {
1371
+ display: "flex",
1372
+ alignItems: "center",
1373
+ gap: 8,
1374
+ padding: "6px 8px",
1375
+ borderRadius: 4,
1376
+ marginBottom: 2,
1377
+ cursor: "pointer",
1378
+ background: isActive ? "#3a3a5a" : "transparent"
1379
+ },
1380
+ children: [
1381
+ /* @__PURE__ */ jsx(
1382
+ "div",
1383
+ {
1384
+ style: {
1385
+ width: 10,
1386
+ height: 10,
1387
+ borderRadius: 2,
1388
+ background: catColor,
1389
+ flexShrink: 0
1390
+ }
1391
+ }
1392
+ ),
1393
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
1394
+ /* @__PURE__ */ jsx(
1395
+ "div",
1396
+ {
1397
+ style: {
1398
+ color: "#e0e0e0",
1399
+ fontSize: 13,
1400
+ fontFamily: "system-ui",
1401
+ whiteSpace: "nowrap",
1402
+ overflow: "hidden",
1403
+ textOverflow: "ellipsis"
1404
+ },
1405
+ children: section.label
1406
+ }
1407
+ ),
1408
+ /* @__PURE__ */ jsxs("div", { style: { color: "#9e9e9e", fontSize: 11, fontFamily: "system-ui" }, children: [
1409
+ section.rows.length,
1410
+ " rows, ",
1411
+ seatCount,
1412
+ " seats"
1413
+ ] })
1414
+ ] })
1415
+ ]
1416
+ },
1417
+ section.id
1418
+ );
1419
+ }),
1420
+ venue.sections.length === 0 && /* @__PURE__ */ jsx("div", { style: { color: "#9e9e9e", fontSize: 13, fontFamily: "system-ui" }, children: "No sections yet. Use the Add Section tool." })
1421
+ ] });
1422
+ }
1423
+ function PolygonPreviewOverlay({
1424
+ points,
1425
+ closeable,
1426
+ viewport
1427
+ }) {
1428
+ if (points.length === 0) return null;
1429
+ const screenPoints = points.map((p) => viewport.worldToScreen(p.x, p.y));
1430
+ const svgPoints = screenPoints.map((p) => `${p.x},${p.y}`).join(" ");
1431
+ const first = screenPoints[0];
1432
+ const last = screenPoints[screenPoints.length - 1];
1433
+ return /* @__PURE__ */ jsxs(
1434
+ "svg",
1435
+ {
1436
+ style: {
1437
+ position: "absolute",
1438
+ inset: 0,
1439
+ width: "100%",
1440
+ height: "100%",
1441
+ pointerEvents: "none",
1442
+ zIndex: 10
1443
+ },
1444
+ children: [
1445
+ screenPoints.length >= 2 && /* @__PURE__ */ jsx(
1446
+ "polyline",
1447
+ {
1448
+ points: svgPoints,
1449
+ fill: "rgba(100, 180, 255, 0.1)",
1450
+ stroke: "rgba(100, 180, 255, 0.8)",
1451
+ strokeWidth: 2,
1452
+ strokeDasharray: "6 4"
1453
+ }
1454
+ ),
1455
+ screenPoints.length >= 3 && /* @__PURE__ */ jsx(
1456
+ "line",
1457
+ {
1458
+ x1: last.x,
1459
+ y1: last.y,
1460
+ x2: first.x,
1461
+ y2: first.y,
1462
+ stroke: closeable ? "rgba(100, 255, 100, 0.8)" : "rgba(100, 180, 255, 0.3)",
1463
+ strokeWidth: closeable ? 2 : 1,
1464
+ strokeDasharray: "4 4"
1465
+ }
1466
+ ),
1467
+ screenPoints.map((p, i) => /* @__PURE__ */ jsx(
1468
+ "circle",
1469
+ {
1470
+ cx: p.x,
1471
+ cy: p.y,
1472
+ r: i === 0 && closeable ? 8 : 4,
1473
+ fill: i === 0 && closeable ? "rgba(100, 255, 100, 0.8)" : "rgba(100, 180, 255, 0.8)"
1474
+ },
1475
+ i
1476
+ )),
1477
+ points.length >= 2 && /* @__PURE__ */ jsxs(
1478
+ "text",
1479
+ {
1480
+ x: (first.x + last.x) / 2,
1481
+ y: (first.y + last.y) / 2 - 10,
1482
+ fill: "#e0e0e0",
1483
+ fontSize: 12,
1484
+ fontFamily: "system-ui",
1485
+ textAnchor: "middle",
1486
+ children: [
1487
+ points.length,
1488
+ " points ",
1489
+ closeable ? "(click first point to close)" : ""
1490
+ ]
1491
+ }
1492
+ )
1493
+ ]
1494
+ }
1495
+ );
1496
+ }
1497
+ function EditorInner({ onChange }) {
1498
+ const { store, viewport, spatialIndex } = useSeatmapContext();
1499
+ const venue = useStore(store, (s) => s.venue);
1500
+ const selectedSeatIds = useStore(store, (s) => s.selectedSeatIds);
1501
+ const historyRef = useRef(new CommandHistory());
1502
+ const [canUndo, setCanUndo] = useState(false);
1503
+ const [canRedo, setCanRedo] = useState(false);
1504
+ const panTool = useMemo(() => new PanTool(), []);
1505
+ const selectTool = useMemo(() => new SelectTool(spatialIndex, historyRef.current), [spatialIndex]);
1506
+ const [polygonPoints, setPolygonPoints] = useState([]);
1507
+ const [polygonCloseable, setPolygonCloseable] = useState(false);
1508
+ const addSectionTool = useMemo(
1509
+ () => {
1510
+ const tool = new AddSectionTool(historyRef.current);
1511
+ const v = store.getState().venue;
1512
+ if (v && v.categories.length > 0) tool.setCategoryId(v.categories[0].id);
1513
+ tool.onPointsChange = (pts, closeable) => {
1514
+ setPolygonPoints([...pts]);
1515
+ setPolygonCloseable(closeable);
1516
+ };
1517
+ return tool;
1518
+ },
1519
+ []
1520
+ );
1521
+ const addRowTool = useMemo(
1522
+ () => new AddRowTool(historyRef.current, spatialIndex),
1523
+ [spatialIndex]
1524
+ );
1525
+ const addSeatTool = useMemo(
1526
+ () => new AddSeatTool(historyRef.current, spatialIndex),
1527
+ [spatialIndex]
1528
+ );
1529
+ const toolMap = useMemo(
1530
+ () => ({
1531
+ pan: panTool,
1532
+ select: selectTool,
1533
+ "add-section": addSectionTool,
1534
+ "add-row": addRowTool,
1535
+ "add-seat": addSeatTool
1536
+ }),
1537
+ [panTool, selectTool, addSectionTool, addRowTool, addSeatTool]
1538
+ );
1539
+ const [activeToolName, setActiveToolName] = useState("pan");
1540
+ const activeToolRef = useRef(panTool);
1541
+ const [seatsPerRow, setSeatsPerRow] = useState(10);
1542
+ const handleSeatsPerRowChange = useCallback(
1543
+ (n) => {
1544
+ setSeatsPerRow(n);
1545
+ addRowTool.seatsPerRow = n;
1546
+ },
1547
+ [addRowTool]
1548
+ );
1549
+ const setActiveTool = useCallback(
1550
+ (name) => {
1551
+ activeToolRef.current.onDeactivate();
1552
+ const tool = toolMap[name] ?? selectTool;
1553
+ tool.onActivate(viewport, store);
1554
+ activeToolRef.current = tool;
1555
+ setActiveToolName(name);
1556
+ },
1557
+ [toolMap, selectTool, viewport, store]
1558
+ );
1559
+ useEffect(() => {
1560
+ const unsub = historyRef.current.subscribe(() => {
1561
+ setCanUndo(historyRef.current.canUndo);
1562
+ setCanRedo(historyRef.current.canRedo);
1563
+ });
1564
+ return unsub;
1565
+ }, []);
1566
+ useEffect(() => {
1567
+ if (venue) {
1568
+ spatialIndex.buildFromSections(venue.sections);
1569
+ onChange?.(venue);
1570
+ }
1571
+ }, [venue, spatialIndex, onChange]);
1572
+ const handleSave = useCallback(() => {
1573
+ const v = store.getState().venue;
1574
+ if (!v) return;
1575
+ const json = serializeVenue(v);
1576
+ const blob = new Blob([json], { type: "application/json" });
1577
+ const url = URL.createObjectURL(blob);
1578
+ const a = document.createElement("a");
1579
+ a.href = url;
1580
+ a.download = `${v.name.replace(/\s+/g, "_").toLowerCase()}.json`;
1581
+ a.click();
1582
+ URL.revokeObjectURL(url);
1583
+ }, [store]);
1584
+ const handleLoad = useCallback(() => {
1585
+ const input = document.createElement("input");
1586
+ input.type = "file";
1587
+ input.accept = ".json";
1588
+ input.onchange = () => {
1589
+ const file = input.files?.[0];
1590
+ if (!file) return;
1591
+ const reader = new FileReader();
1592
+ reader.onload = () => {
1593
+ try {
1594
+ const loaded = deserializeVenue(reader.result);
1595
+ store.getState().setVenue(loaded);
1596
+ spatialIndex.buildFromSections(loaded.sections);
1597
+ viewport.fitBounds(venueAABB(loaded));
1598
+ historyRef.current.clear();
1599
+ } catch {
1600
+ alert("Invalid venue JSON file");
1601
+ }
1602
+ };
1603
+ reader.readAsText(file);
1604
+ };
1605
+ input.click();
1606
+ }, [store, spatialIndex, viewport]);
1607
+ const handleFitView = useCallback(() => {
1608
+ if (!venue) return;
1609
+ viewport.fitBounds(venueAABB(venue));
1610
+ }, [venue, viewport]);
1611
+ const handleUploadBackground = useCallback(() => {
1612
+ const input = document.createElement("input");
1613
+ input.type = "file";
1614
+ input.accept = "image/*";
1615
+ input.onchange = () => {
1616
+ const file = input.files?.[0];
1617
+ if (!file) return;
1618
+ const reader = new FileReader();
1619
+ reader.onload = () => {
1620
+ const v = store.getState().venue;
1621
+ if (!v) return;
1622
+ const dataUrl = reader.result;
1623
+ store.getState().setVenue({
1624
+ ...v,
1625
+ backgroundImage: dataUrl,
1626
+ backgroundImageOpacity: v.backgroundImageOpacity ?? 0.5
1627
+ });
1628
+ };
1629
+ reader.readAsDataURL(file);
1630
+ };
1631
+ input.click();
1632
+ }, [store]);
1633
+ const handleRemoveBackground = useCallback(() => {
1634
+ const v = store.getState().venue;
1635
+ if (!v) return;
1636
+ store.getState().setVenue({
1637
+ ...v,
1638
+ backgroundImage: void 0,
1639
+ backgroundImageOpacity: void 0
1640
+ });
1641
+ }, [store]);
1642
+ const handleBackgroundOpacityChange = useCallback(
1643
+ (opacity) => {
1644
+ const v = store.getState().venue;
1645
+ if (!v) return;
1646
+ store.getState().setVenue({ ...v, backgroundImageOpacity: opacity });
1647
+ },
1648
+ [store]
1649
+ );
1650
+ const handleSelectSection = useCallback(
1651
+ (sectionId) => {
1652
+ if (!venue) return;
1653
+ const section = venue.sections.find((s) => s.id === sectionId);
1654
+ if (!section) return;
1655
+ const allSeatIds = section.rows.flatMap((r) => r.seats.map((s) => s.id));
1656
+ store.getState().setSelection(allSeatIds);
1657
+ },
1658
+ [venue, store]
1659
+ );
1660
+ useEffect(() => {
1661
+ const isTyping = () => {
1662
+ const tag = document.activeElement?.tagName;
1663
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
1664
+ };
1665
+ const handler = (e) => {
1666
+ if ((e.metaKey || e.ctrlKey) && e.key === "z") {
1667
+ e.preventDefault();
1668
+ if (e.shiftKey) historyRef.current.redo();
1669
+ else historyRef.current.undo();
1670
+ return;
1671
+ }
1672
+ if (isTyping()) return;
1673
+ if (e.key === "v" || e.key === "1") setActiveTool("select");
1674
+ if (e.key === "h" || e.key === "2") setActiveTool("pan");
1675
+ if (e.key === "s" || e.key === "3") setActiveTool("add-section");
1676
+ if (e.key === "r" || e.key === "4") setActiveTool("add-row");
1677
+ if (e.key === "a" || e.key === "5") setActiveTool("add-seat");
1678
+ if (e.key === " ") {
1679
+ e.preventDefault();
1680
+ setActiveTool("pan");
1681
+ }
1682
+ };
1683
+ const upHandler = (e) => {
1684
+ if (isTyping()) return;
1685
+ if (e.key === " ") {
1686
+ setActiveTool("select");
1687
+ }
1688
+ };
1689
+ window.addEventListener("keydown", handler);
1690
+ window.addEventListener("keyup", upHandler);
1691
+ return () => {
1692
+ window.removeEventListener("keydown", handler);
1693
+ window.removeEventListener("keyup", upHandler);
1694
+ };
1695
+ }, [setActiveTool]);
1696
+ const handleCanvasPointerDown = useCallback(
1697
+ (e) => {
1698
+ if (e.button !== 0) return;
1699
+ e.currentTarget.setPointerCapture(e.pointerId);
1700
+ const rect = e.currentTarget.getBoundingClientRect();
1701
+ const screenX = e.clientX - rect.left;
1702
+ const screenY = e.clientY - rect.top;
1703
+ const world = viewport.screenToWorld(screenX, screenY);
1704
+ const toolEvent = {
1705
+ worldX: world.x,
1706
+ worldY: world.y,
1707
+ screenX: e.clientX,
1708
+ screenY: e.clientY,
1709
+ shiftKey: e.shiftKey,
1710
+ ctrlKey: e.ctrlKey,
1711
+ metaKey: e.metaKey,
1712
+ button: e.button
1713
+ };
1714
+ activeToolRef.current.onPointerDown(toolEvent, viewport, store);
1715
+ },
1716
+ [viewport, store]
1717
+ );
1718
+ const handleCanvasPointerMove = useCallback(
1719
+ (e) => {
1720
+ const rect = e.currentTarget.getBoundingClientRect();
1721
+ const screenX = e.clientX - rect.left;
1722
+ const screenY = e.clientY - rect.top;
1723
+ const world = viewport.screenToWorld(screenX, screenY);
1724
+ const toolEvent = {
1725
+ worldX: world.x,
1726
+ worldY: world.y,
1727
+ screenX: e.clientX,
1728
+ screenY: e.clientY,
1729
+ shiftKey: e.shiftKey,
1730
+ ctrlKey: e.ctrlKey,
1731
+ metaKey: e.metaKey,
1732
+ button: e.button
1733
+ };
1734
+ activeToolRef.current.onPointerMove(toolEvent, viewport, store);
1735
+ },
1736
+ [viewport, store]
1737
+ );
1738
+ const handleCanvasPointerUp = useCallback(
1739
+ (e) => {
1740
+ const rect = e.currentTarget.getBoundingClientRect();
1741
+ const screenX = e.clientX - rect.left;
1742
+ const screenY = e.clientY - rect.top;
1743
+ const world = viewport.screenToWorld(screenX, screenY);
1744
+ const toolEvent = {
1745
+ worldX: world.x,
1746
+ worldY: world.y,
1747
+ screenX: e.clientX,
1748
+ screenY: e.clientY,
1749
+ shiftKey: e.shiftKey,
1750
+ ctrlKey: e.ctrlKey,
1751
+ metaKey: e.metaKey,
1752
+ button: e.button
1753
+ };
1754
+ activeToolRef.current.onPointerUp(toolEvent, viewport, store);
1755
+ },
1756
+ [viewport, store]
1757
+ );
1758
+ const sidebarStyle = {
1759
+ width: 260,
1760
+ background: "#1a1a2e",
1761
+ borderLeft: "1px solid #2a2a4a",
1762
+ overflowY: "auto",
1763
+ flexShrink: 0
1764
+ };
1765
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", width: "100%", height: "100%" }, children: [
1766
+ /* @__PURE__ */ jsx(
1767
+ Toolbar,
1768
+ {
1769
+ activeTool: activeToolName,
1770
+ onToolChange: setActiveTool,
1771
+ canUndo,
1772
+ canRedo,
1773
+ onUndo: () => historyRef.current.undo(),
1774
+ onRedo: () => historyRef.current.redo(),
1775
+ onFitView: handleFitView,
1776
+ onSave: handleSave,
1777
+ onLoad: handleLoad,
1778
+ seatsPerRow,
1779
+ onSeatsPerRowChange: handleSeatsPerRowChange
1780
+ }
1781
+ ),
1782
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flex: 1, overflow: "hidden" }, children: [
1783
+ /* @__PURE__ */ jsxs(
1784
+ "div",
1785
+ {
1786
+ style: { flex: 1, position: "relative", cursor: (toolMap[activeToolName] ?? selectTool).cursor },
1787
+ onPointerDown: handleCanvasPointerDown,
1788
+ onPointerMove: handleCanvasPointerMove,
1789
+ onPointerUp: handleCanvasPointerUp,
1790
+ children: [
1791
+ /* @__PURE__ */ jsx(SeatmapCanvas, { panOnLeftClick: false }),
1792
+ polygonPoints.length > 0 && /* @__PURE__ */ jsx(
1793
+ PolygonPreviewOverlay,
1794
+ {
1795
+ points: polygonPoints,
1796
+ closeable: polygonCloseable,
1797
+ viewport
1798
+ }
1799
+ )
1800
+ ]
1801
+ }
1802
+ ),
1803
+ /* @__PURE__ */ jsxs("div", { style: sidebarStyle, children: [
1804
+ /* @__PURE__ */ jsx(
1805
+ PropertyPanel,
1806
+ {
1807
+ venue,
1808
+ selectedSeatIds,
1809
+ history: historyRef.current,
1810
+ store,
1811
+ onUploadBackground: handleUploadBackground,
1812
+ onRemoveBackground: handleRemoveBackground,
1813
+ onBackgroundOpacityChange: handleBackgroundOpacityChange
1814
+ }
1815
+ ),
1816
+ /* @__PURE__ */ jsx("div", { style: { height: 1, background: "#2a2a4a", margin: "0 16px" } }),
1817
+ /* @__PURE__ */ jsx(
1818
+ LayerPanel,
1819
+ {
1820
+ venue,
1821
+ selectedSeatIds,
1822
+ onSelectSection: handleSelectSection
1823
+ }
1824
+ ),
1825
+ /* @__PURE__ */ jsx("div", { style: { height: 1, background: "#2a2a4a", margin: "0 16px" } }),
1826
+ /* @__PURE__ */ jsx(
1827
+ CategoryManager,
1828
+ {
1829
+ venue,
1830
+ history: historyRef.current,
1831
+ store
1832
+ }
1833
+ )
1834
+ ] })
1835
+ ] })
1836
+ ] });
1837
+ }
1838
+ function SeatmapEditor({ venue, onChange, className }) {
1839
+ return /* @__PURE__ */ jsx(SeatmapProvider, { venue, children: /* @__PURE__ */ jsx("div", { className, style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(EditorInner, { onChange }) }) });
1840
+ }
1841
+ var DrawGATool = class extends BaseTool {
1842
+ constructor(history) {
1843
+ super();
1844
+ this.history = history;
1845
+ }
1846
+ name = "draw-ga";
1847
+ cursor = "crosshair";
1848
+ points = [];
1849
+ capacity = 100;
1850
+ categoryId = "";
1851
+ onPointerDown(e, _viewport, store) {
1852
+ if (e.button !== 0) return;
1853
+ if (e.ctrlKey || e.metaKey) {
1854
+ this.finishPolygon(store);
1855
+ return;
1856
+ }
1857
+ this.points.push({ x: e.worldX, y: e.worldY });
1858
+ }
1859
+ finishPolygon(store) {
1860
+ if (this.points.length < 3) {
1861
+ this.points = [];
1862
+ return;
1863
+ }
1864
+ const venue = store.getState().venue;
1865
+ if (!venue) return;
1866
+ const area = {
1867
+ id: generateId("ga"),
1868
+ label: `GA ${Date.now().toString(36).slice(-3).toUpperCase()}`,
1869
+ shape: [...this.points],
1870
+ capacity: this.capacity,
1871
+ categoryId: this.categoryId
1872
+ };
1873
+ this.history.execute({
1874
+ description: `Add GA area "${area.label}"`,
1875
+ execute: () => {
1876
+ const v = store.getState().venue;
1877
+ if (!v) return;
1878
+ store.getState().setVenue({
1879
+ ...v,
1880
+ gaAreas: [...v.gaAreas, area]
1881
+ });
1882
+ },
1883
+ undo: () => {
1884
+ const v = store.getState().venue;
1885
+ if (!v) return;
1886
+ store.getState().setVenue({
1887
+ ...v,
1888
+ gaAreas: v.gaAreas.filter((a) => a.id !== area.id)
1889
+ });
1890
+ }
1891
+ });
1892
+ this.points = [];
1893
+ }
1894
+ onDeactivate() {
1895
+ this.points = [];
1896
+ }
1897
+ };
1898
+ var AddTableTool = class extends BaseTool {
1899
+ constructor(history) {
1900
+ super();
1901
+ this.history = history;
1902
+ }
1903
+ name = "add-table";
1904
+ cursor = "crosshair";
1905
+ shape = "round";
1906
+ seatsPerTable = 8;
1907
+ tableRadius = 40;
1908
+ categoryId = "";
1909
+ onPointerDown(e, _viewport, store) {
1910
+ const venue = store.getState().venue;
1911
+ if (!venue) return;
1912
+ const seats = [];
1913
+ for (let i = 0; i < this.seatsPerTable; i++) {
1914
+ const angle = Math.PI * 2 * i / this.seatsPerTable - Math.PI / 2;
1915
+ seats.push({
1916
+ id: generateId("seat"),
1917
+ label: `${i + 1}`,
1918
+ position: {
1919
+ x: Math.cos(angle) * this.tableRadius,
1920
+ y: Math.sin(angle) * this.tableRadius
1921
+ },
1922
+ status: "available",
1923
+ categoryId: this.categoryId
1924
+ });
1925
+ }
1926
+ const table = {
1927
+ id: generateId("tbl"),
1928
+ label: `Table ${Date.now().toString(36).slice(-3).toUpperCase()}`,
1929
+ position: { x: e.worldX, y: e.worldY },
1930
+ shape: this.shape,
1931
+ seats,
1932
+ categoryId: this.categoryId
1933
+ };
1934
+ this.history.execute({
1935
+ description: `Add table "${table.label}"`,
1936
+ execute: () => {
1937
+ const v = store.getState().venue;
1938
+ if (!v) return;
1939
+ store.getState().setVenue({
1940
+ ...v,
1941
+ tables: [...v.tables, table]
1942
+ });
1943
+ },
1944
+ undo: () => {
1945
+ const v = store.getState().venue;
1946
+ if (!v) return;
1947
+ store.getState().setVenue({
1948
+ ...v,
1949
+ tables: v.tables.filter((t) => t.id !== table.id)
1950
+ });
1951
+ }
1952
+ });
1953
+ }
1954
+ };
1955
+
1956
+ export { AddRowTool, AddSeatTool, AddSectionTool, AddTableTool, BaseTool, CategoryManager, DrawGATool, LayerPanel, PanTool, PropertyPanel, SeatmapEditor, SelectTool, Toolbar };
1957
+ //# sourceMappingURL=index.js.map
1958
+ //# sourceMappingURL=index.js.map