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