@zonetrix/viewer 2.1.1 → 2.3.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/README.md CHANGED
@@ -49,7 +49,8 @@ import type {
49
49
  SeatShape,
50
50
  SeatData,
51
51
  SeatMapConfig,
52
- ColorSettings
52
+ ColorSettings,
53
+ FloorConfig
53
54
  } from '@zonetrix/viewer';
54
55
 
55
56
  // Hooks
@@ -103,7 +104,7 @@ function BookingApp() {
103
104
  |------|------|----------|-------------|
104
105
  | `config` | `SeatMapConfig` | No* | Seat map configuration object |
105
106
  | `configUrl` | `string` | No* | URL to fetch configuration from |
106
- | `floorId` | `string` | No | Filter seats/stages by floor ID |
107
+ | `floorId` | `string` | No | Filter seats/stages by floor ID (controlled mode) |
107
108
  | `onFloorChange` | `(floorId: string) => void` | No | Callback when floor changes |
108
109
  | `reservedSeats` | `string[]` | No | Array of seat IDs/numbers to mark as reserved |
109
110
  | `unavailableSeats` | `string[]` | No | Array of seat IDs/numbers to mark as unavailable |
@@ -116,6 +117,11 @@ function BookingApp() {
116
117
  | `className` | `string` | No | Custom CSS class for the container |
117
118
  | `onConfigLoad` | `(config: SeatMapConfig) => void` | No | Callback when config is loaded |
118
119
  | `onError` | `(error: Error) => void` | No | Callback when an error occurs |
120
+ | `showFloorSelector` | `boolean` | No | Show/hide built-in floor selector (default: true when floors > 1) |
121
+ | `floorSelectorPosition` | `string` | No | Position: 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' |
122
+ | `floorSelectorClassName` | `string` | No | Custom CSS class for floor selector |
123
+ | `showAllFloorsOption` | `boolean` | No | Show "All" button in floor selector (default: true) |
124
+ | `allFloorsLabel` | `string` | No | Custom label for "All" button (default: 'All') |
119
125
 
120
126
  *Note: Either `config` or `configUrl` must be provided.
121
127
 
@@ -257,19 +263,43 @@ function MobileOptimized() {
257
263
  }
258
264
  ```
259
265
 
260
- ### 6. Multi-floor Venue
266
+ ### 6. Multi-floor Venue (Built-in Floor Selector)
267
+
268
+ The viewer includes a built-in floor selector that automatically appears when your config has multiple floors.
261
269
 
262
270
  ```tsx
263
- import { useState } from 'react';
264
271
  import { SeatMapViewer } from '@zonetrix/viewer';
265
272
 
266
273
  function MultiFloorVenue() {
267
- const [currentFloor, setCurrentFloor] = useState('floor_1');
274
+ return (
275
+ <SeatMapViewer
276
+ config={venueConfig}
277
+ onSeatSelect={(seat) => handleSelection(seat)}
278
+ // Floor selector auto-shows when config.floors.length > 1
279
+ // Customize position and labels:
280
+ floorSelectorPosition="top-right"
281
+ allFloorsLabel="All Floors"
282
+ />
283
+ );
284
+ }
285
+ ```
286
+
287
+ #### Custom Floor Selector (Controlled Mode)
288
+
289
+ For full control over the floor selector UI, use controlled mode:
290
+
291
+ ```tsx
292
+ import { useState } from 'react';
293
+ import { SeatMapViewer } from '@zonetrix/viewer';
294
+
295
+ function CustomFloorSelector() {
296
+ const [currentFloor, setCurrentFloor] = useState<string | null>(null);
268
297
 
269
298
  return (
270
299
  <div>
271
- {/* Floor selector */}
300
+ {/* Your custom floor selector */}
272
301
  <div className="floor-tabs">
302
+ <button onClick={() => setCurrentFloor(null)}>All</button>
273
303
  {venueConfig.floors?.map((floor) => (
274
304
  <button
275
305
  key={floor.id}
@@ -283,7 +313,8 @@ function MultiFloorVenue() {
283
313
 
284
314
  <SeatMapViewer
285
315
  config={venueConfig}
286
- floorId={currentFloor}
316
+ showFloorSelector={false} // Hide built-in selector
317
+ floorId={currentFloor || undefined}
287
318
  onFloorChange={setCurrentFloor}
288
319
  onSeatSelect={(seat) => handleSelection(seat)}
289
320
  />
@@ -364,7 +395,11 @@ The viewer accepts a `SeatMapConfig` object. You can create these configurations
364
395
  }
365
396
  ],
366
397
  "sections": [],
367
- "stages": []
398
+ "stages": [],
399
+ "floors": [
400
+ { "id": "floor_1", "name": "Ground Floor", "order": 0 },
401
+ { "id": "floor_2", "name": "First Floor", "order": 1 }
402
+ ]
368
403
  }
369
404
  ```
370
405
 
@@ -416,6 +451,17 @@ interface ColorSettings {
416
451
  }
417
452
  ```
418
453
 
454
+ ### FloorConfig
455
+
456
+ ```typescript
457
+ interface FloorConfig {
458
+ id: string; // Unique identifier (e.g., "floor_1")
459
+ name: string; // Display name (e.g., "Ground Floor")
460
+ order: number; // Sort order (0 = first)
461
+ color?: string; // Optional floor color
462
+ }
463
+ ```
464
+
419
465
  ## Seat States Explained
420
466
 
421
467
  | State | Description | User Can Select? | Visual |
@@ -16,5 +16,12 @@ export interface SeatMapViewerProps {
16
16
  className?: string;
17
17
  onConfigLoad?: (config: SeatMapConfig) => void;
18
18
  onError?: (error: Error) => void;
19
+ showFloorSelector?: boolean;
20
+ floorSelectorPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
21
+ floorSelectorClassName?: string;
22
+ showAllFloorsOption?: boolean;
23
+ allFloorsLabel?: string;
24
+ fitToView?: boolean;
25
+ fitPadding?: number;
19
26
  }
20
27
  export declare const SeatMapViewer: React.FC<SeatMapViewerProps>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { SeatMapViewer } from './components/SeatMapViewer';
2
2
  export type { SeatMapViewerProps } from './components/SeatMapViewer';
3
- export type { SeatState, SeatShape, SeatData, SerializedSeat, SerializedSection, SerializedStage, ColorSettings, SeatMapConfig, } from './types';
3
+ export type { SeatState, SeatShape, SeatData, SerializedSeat, SerializedSection, SerializedStage, ColorSettings, SeatMapConfig, FloorConfig, } from './types';
4
4
  export { DEFAULT_COLORS } from './types';
5
5
  export { useConfigFetcher } from './hooks/useConfigFetcher';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react/jsx-runtime"),t=require("react"),h=require("react-konva");function $(i){const[f,n]=t.useState(null),[v,x]=t.useState(!1),[p,d]=t.useState(null),u=async()=>{if(i){x(!0),d(null);try{const o=await fetch(i);if(!o.ok)throw new Error(`Failed to fetch config: ${o.statusText}`);const c=await o.json();n(c)}catch(o){const c=o instanceof Error?o:new Error("Unknown error occurred");d(c),console.error("Failed to fetch seat map config:",c)}finally{x(!1)}}};return t.useEffect(()=>{u()},[i]),{config:f,loading:v,error:p,refetch:u}}const z={canvasBackground:"#1a1a1a",stageColor:"#808080",seatAvailable:"#2C2B30",seatReserved:"#FCEA00",seatSelected:"#3A7DE5",seatUnavailable:"#6b7280",seatHidden:"#4a4a4a",gridLines:"#404040",currency:"KD"},V=t.memo(({seat:i,state:f,colors:n,onClick:v})=>{const d={available:n.seatAvailable,reserved:n.seatReserved,selected:n.seatSelected,unavailable:n.seatUnavailable,hidden:n.seatHidden}[f],u=f==="available"||f==="selected",o=t.useCallback(()=>{u&&v(i)},[i,v,u]),c={x:i.position.x,y:i.position.y,fill:d,stroke:"#ffffff",strokeWidth:1,onClick:o,onTap:o};return i.shape==="circle"?s.jsx(h.Circle,{...c,radius:12}):s.jsx(h.Rect,{...c,width:24,height:24,offsetX:12,offsetY:12,cornerRadius:i.shape==="square"?0:4})});V.displayName="ViewerSeat";const _=t.memo(({stage:i,stageColor:f})=>s.jsxs(h.Group,{x:i.position.x,y:i.position.y,children:[s.jsx(h.Rect,{width:i.config.width,height:i.config.height,fill:f+"80",stroke:"#ffffff",strokeWidth:2,cornerRadius:10}),s.jsx(h.Text,{text:i.config.label,x:0,y:0,width:i.config.width,height:i.config.height,fontSize:24,fontStyle:"bold",fill:"#ffffff",align:"center",verticalAlign:"middle"})]}));_.displayName="ViewerStage";const X=({config:i,configUrl:f,floorId:n,onFloorChange:v,reservedSeats:x=[],unavailableSeats:p=[],onSeatSelect:d,onSeatDeselect:u,onSelectionChange:o,colorOverrides:c,showTooltip:G=!0,zoomEnabled:A=!0,className:k="",onConfigLoad:N,onError:R})=>{const L=t.useRef(null),[S,q]=t.useState(new Set),[M,D]=t.useState(1),[y,O]=t.useState({x:0,y:0}),{config:W,loading:U,error:b}=$(f),r=i||W,E=t.useMemo(()=>r?{...r.colors,...c}:{...z,...c},[r,c]),F=t.useMemo(()=>{if(!r)return[];let e=r.seats.filter(a=>a.state!=="hidden");return n&&(e=e.filter(a=>a.floorId===n||!a.floorId&&n==="floor_default")),e},[r,n]),Y=t.useMemo(()=>r?.stages?n?r.stages.filter(e=>e.floorId===n||!e.floorId&&n==="floor_default"):r.stages:[],[r,n]),w=t.useMemo(()=>{const e=new Set(x),a=new Set(p);return{reserved:e,unavailable:a}},[x,p]),T=t.useCallback(e=>{const a=e.id,l=e.seatNumber||"";return w.unavailable.has(a)||w.unavailable.has(l)?"unavailable":w.reserved.has(a)||w.reserved.has(l)?"reserved":S.has(a)?"selected":e.state},[w,S]);t.useEffect(()=>{r&&N&&N(r)},[r,N]),t.useEffect(()=>{b&&R&&R(b)},[b,R]);const B=t.useCallback(e=>{const a=T(e);if(a!=="available"&&a!=="selected")return;const l=S.has(e.id);q(g=>{const m=new Set(g);return l?m.delete(e.id):m.add(e.id),m}),l?u?.(e):(d?.(e),d||console.log("Seat selected:",e))},[T,S,d,u]),j=t.useMemo(()=>r?F.filter(e=>S.has(e.id)):[],[F,S]);t.useEffect(()=>{o?.(j)},[j,o]);const H=t.useCallback(e=>{if(!A)return;e.evt.preventDefault();const a=L.current;if(!a)return;const l=M,g=a.getPointerPosition();if(!g)return;const m={x:(g.x-y.x)/l,y:(g.y-y.y)/l},K=e.evt.deltaY>0?-1:1,P=1.05;let C=K>0?l*P:l/P;C=Math.max(.5,Math.min(5,C)),D(C),O({x:g.x-m.x*C,y:g.y-m.y*C})},[A,M,y]);return U?s.jsx("div",{className:`flex items-center justify-center h-full ${k}`,children:s.jsx("p",{children:"Loading seat map..."})}):b?s.jsx("div",{className:`flex items-center justify-center h-full ${k}`,children:s.jsxs("p",{className:"text-red-500",children:["Error loading seat map: ",b.message]})}):r?s.jsxs("div",{className:`relative ${k}`,children:[s.jsxs(h.Stage,{ref:L,width:r.canvas.width,height:r.canvas.height,scaleX:M,scaleY:M,x:y.x,y:y.y,onWheel:H,style:{backgroundColor:r.canvas.backgroundColor},children:[s.jsx(h.Layer,{listening:!1,children:Y.map(e=>s.jsx(_,{stage:e,stageColor:E.stageColor},e.id))}),s.jsx(h.Layer,{children:F.map(e=>s.jsx(V,{seat:e,state:T(e),colors:E,onClick:B},e.id))})]}),j.length>0&&s.jsxs("div",{className:"absolute top-4 right-4 bg-white dark:bg-gray-800 p-4 rounded shadow-lg",children:[s.jsxs("h3",{className:"font-semibold mb-2",children:["Selected Seats (",j.length,")"]}),s.jsx("div",{className:"max-h-48 overflow-y-auto space-y-1",children:j.map(e=>s.jsxs("div",{className:"text-sm",children:[e.seatNumber,e.price&&` - ${E.currency} ${e.price.toFixed(2)}`]},e.id))})]})]}):s.jsx("div",{className:`flex items-center justify-center h-full ${k}`,children:s.jsx("p",{children:"No configuration provided"})})};exports.DEFAULT_COLORS=z;exports.SeatMapViewer=X;exports.useConfigFetcher=$;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("react/jsx-runtime"),t=require("react"),v=require("react-konva");function K(i){const[h,d]=t.useState(null),[S,y]=t.useState(!1),[w,x]=t.useState(null),g=async()=>{if(i){y(!0),x(null);try{const f=await fetch(i);if(!f.ok)throw new Error(`Failed to fetch config: ${f.statusText}`);const u=await f.json();d(u)}catch(f){const u=f instanceof Error?f:new Error("Unknown error occurred");x(u),console.error("Failed to fetch seat map config:",u)}finally{y(!1)}}};return t.useEffect(()=>{g()},[i]),{config:h,loading:S,error:w,refetch:g}}const G={canvasBackground:"#1a1a1a",stageColor:"#808080",seatAvailable:"#2C2B30",seatReserved:"#FCEA00",seatSelected:"#3A7DE5",seatUnavailable:"#6b7280",seatHidden:"#4a4a4a",gridLines:"#404040",currency:"KD"},J=t.memo(({seat:i,state:h,colors:d,onClick:S})=>{const x={available:d.seatAvailable,reserved:d.seatReserved,selected:d.seatSelected,unavailable:d.seatUnavailable,hidden:d.seatHidden}[h],g=h==="available"||h==="selected",f=t.useCallback(()=>{g&&S(i)},[i,S,g]),u={x:i.position.x,y:i.position.y,fill:x,stroke:"#ffffff",strokeWidth:1,onClick:f,onTap:f};return i.shape==="circle"?o.jsx(v.Circle,{...u,radius:12}):o.jsx(v.Rect,{...u,width:24,height:24,offsetX:12,offsetY:12,cornerRadius:i.shape==="square"?0:4})});J.displayName="ViewerSeat";const Q=t.memo(({stage:i,stageColor:h})=>o.jsxs(v.Group,{x:i.position.x,y:i.position.y,children:[o.jsx(v.Rect,{width:i.config.width,height:i.config.height,fill:h+"80",stroke:"#ffffff",strokeWidth:2,cornerRadius:10}),o.jsx(v.Text,{text:i.config.label,x:0,y:0,width:i.config.width,height:i.config.height,fontSize:24,fontStyle:"bold",fill:"#ffffff",align:"center",verticalAlign:"middle"})]}));Q.displayName="ViewerStage";const Z=t.memo(({floors:i,currentFloorId:h,onFloorChange:d,showAllOption:S,allLabel:y,position:w,className:x})=>{const g=t.useMemo(()=>[...i].sort((p,I)=>p.order-I.order),[i]),u={position:"absolute",display:"flex",alignItems:"center",gap:"8px",padding:"8px 12px",backgroundColor:"rgba(26, 26, 26, 0.95)",borderRadius:"8px",margin:"12px",zIndex:10,...{"top-left":{top:0,left:0},"top-right":{top:0,right:0},"bottom-left":{bottom:0,left:0},"bottom-right":{bottom:0,right:0}}[w]},Y={padding:"6px 14px",fontSize:"14px",fontWeight:500,border:"1px solid #444",borderRadius:"6px",backgroundColor:"transparent",color:"#fff",cursor:"pointer",transition:"all 0.2s ease"},E={...Y,backgroundColor:"#3A7DE5",borderColor:"#3A7DE5"};return o.jsxs("div",{className:x,style:u,children:[S&&o.jsx("button",{type:"button",onClick:()=>d(null),style:h===null?E:Y,children:y}),g.map(p=>o.jsx("button",{type:"button",onClick:()=>d(p.id),style:h===p.id?E:Y,children:p.name},p.id))]})});Z.displayName="FloorSelectorBar";const Se=({config:i,configUrl:h,floorId:d,onFloorChange:S,reservedSeats:y=[],unavailableSeats:w=[],onSeatSelect:x,onSeatDeselect:g,onSelectionChange:f,colorOverrides:u,showTooltip:Y=!0,zoomEnabled:E=!0,className:p="",onConfigLoad:I,onError:T,showFloorSelector:O,floorSelectorPosition:ee="top-left",floorSelectorClassName:te,showAllFloorsOption:ne=!0,allFloorsLabel:se="All",fitToView:z=!0,fitPadding:W=40})=>{const P=t.useRef(null),[j,oe]=t.useState(new Set),[X,V]=t.useState(1),[F,_]=t.useState({x:0,y:0}),[ie,re]=t.useState(null),[q,U]=t.useState(!1),{config:ae,loading:le,error:N}=K(h),s=i||ae,$=d!==void 0,m=$?d||null:ie,ce=t.useCallback(e=>{$||re(e),S?.(e)},[$,S]),B=s?.floors||[],de=O!==void 0?O:B.length>1,D=t.useMemo(()=>s?{...s.colors,...u}:{...G,...u},[s,u]),k=t.useMemo(()=>{if(!s)return[];let e=s.seats.filter(n=>n.state!=="hidden");return m&&(e=e.filter(n=>n.floorId===m||!n.floorId&&m==="floor_default")),e},[s,m]),L=t.useMemo(()=>s?.stages?m?s.stages.filter(e=>e.floorId===m||!e.floorId&&m==="floor_default"):s.stages:[],[s,m]),b=t.useMemo(()=>{if(!s||k.length===0&&L.length===0)return null;const e=12;let n=1/0,r=1/0,a=-1/0,l=-1/0;return k.forEach(c=>{n=Math.min(n,c.position.x-e),r=Math.min(r,c.position.y-e),a=Math.max(a,c.position.x+e),l=Math.max(l,c.position.y+e)}),L.forEach(c=>{n=Math.min(n,c.position.x),r=Math.min(r,c.position.y),a=Math.max(a,c.position.x+(c.config?.width||200)),l=Math.max(l,c.position.y+(c.config?.height||100))}),{minX:n,minY:r,maxX:a,maxY:l,width:a-n,height:l-r}},[s,k,L]);t.useEffect(()=>{if(!z||q||!s||!b)return;const e=s.canvas.width,n=s.canvas.height,r=e-W*2,a=n-W*2,l=r/b.width,c=a/b.height,M=Math.min(l,c,2),C=b.minX+b.width/2,he=b.minY+b.height/2,pe=e/2,xe=n/2,ge=pe-C*M,me=xe-he*M;V(M),_({x:ge,y:me}),U(!0)},[z,q,s,b,W]),t.useEffect(()=>{z&&U(!1)},[m,z]);const R=t.useMemo(()=>{const e=new Set(y),n=new Set(w);return{reserved:e,unavailable:n}},[y,w]),H=t.useCallback(e=>{const n=e.id,r=e.seatNumber||"";return R.unavailable.has(n)||R.unavailable.has(r)?"unavailable":R.reserved.has(n)||R.reserved.has(r)?"reserved":j.has(n)?"selected":e.state},[R,j]);t.useEffect(()=>{s&&I&&I(s)},[s,I]),t.useEffect(()=>{N&&T&&T(N)},[N,T]);const fe=t.useCallback(e=>{const n=H(e);if(n!=="available"&&n!=="selected")return;const r=j.has(e.id);oe(a=>{const l=new Set(a);return r?l.delete(e.id):l.add(e.id),l}),r?g?.(e):(x?.(e),x||console.log("Seat selected:",e))},[H,j,x,g]),A=t.useMemo(()=>s?k.filter(e=>j.has(e.id)):[],[k,j]);t.useEffect(()=>{f?.(A)},[A,f]);const ue=t.useCallback(e=>{if(!E)return;e.evt.preventDefault();const n=P.current;if(!n)return;const r=X,a=n.getPointerPosition();if(!a)return;const l={x:(a.x-F.x)/r,y:(a.y-F.y)/r},c=e.evt.deltaY>0?-1:1,M=1.05;let C=c>0?r*M:r/M;C=Math.max(.5,Math.min(5,C)),V(C),_({x:a.x-l.x*C,y:a.y-l.y*C})},[E,X,F]);return le?o.jsx("div",{className:`flex items-center justify-center h-full ${p}`,children:o.jsx("p",{children:"Loading seat map..."})}):N?o.jsx("div",{className:`flex items-center justify-center h-full ${p}`,children:o.jsxs("p",{className:"text-red-500",children:["Error loading seat map: ",N.message]})}):s?o.jsxs("div",{className:`relative ${p}`,children:[de&&B.length>0&&o.jsx(Z,{floors:B,currentFloorId:m,onFloorChange:ce,showAllOption:ne,allLabel:se,position:ee,className:te}),o.jsxs(v.Stage,{ref:P,width:s.canvas.width,height:s.canvas.height,scaleX:X,scaleY:X,x:F.x,y:F.y,onWheel:ue,style:{backgroundColor:s.canvas.backgroundColor},children:[o.jsx(v.Layer,{listening:!1,children:L.map(e=>o.jsx(Q,{stage:e,stageColor:D.stageColor},e.id))}),o.jsx(v.Layer,{children:k.map(e=>o.jsx(J,{seat:e,state:H(e),colors:D,onClick:fe},e.id))})]}),A.length>0&&o.jsxs("div",{className:"absolute top-4 right-4 bg-white dark:bg-gray-800 p-4 rounded shadow-lg",children:[o.jsxs("h3",{className:"font-semibold mb-2",children:["Selected Seats (",A.length,")"]}),o.jsx("div",{className:"max-h-48 overflow-y-auto space-y-1",children:A.map(e=>o.jsxs("div",{className:"text-sm",children:[e.seatNumber,e.price&&` - ${D.currency} ${e.price.toFixed(2)}`]},e.id))})]})]}):o.jsx("div",{className:`flex items-center justify-center h-full ${p}`,children:o.jsx("p",{children:"No configuration provided"})})};exports.DEFAULT_COLORS=G;exports.SeatMapViewer=Se;exports.useConfigFetcher=K;
package/dist/index.mjs CHANGED
@@ -1,34 +1,34 @@
1
- import { jsx as s, jsxs as u } from "react/jsx-runtime";
2
- import { useState as v, useEffect as T, useRef as Q, useMemo as N, useCallback as $, memo as W } from "react";
3
- import { Stage as Z, Layer as L, Group as ee, Rect as D, Text as te, Circle as ie } from "react-konva";
4
- function re(t) {
5
- const [c, r] = v(null), [S, g] = v(!1), [y, d] = v(null), f = async () => {
6
- if (t) {
7
- g(!0), d(null);
1
+ import { jsx as r, jsxs as v } from "react/jsx-runtime";
2
+ import { useState as S, useEffect as F, useRef as we, useCallback as W, useMemo as k, memo as O } from "react";
3
+ import { Stage as Ce, Layer as Z, Group as ke, Rect as ee, Text as Ie, Circle as Me } from "react-konva";
4
+ function Ne(i) {
5
+ const [h, c] = S(null), [y, x] = S(!1), [w, p] = S(null), g = async () => {
6
+ if (i) {
7
+ x(!0), p(null);
8
8
  try {
9
- const a = await fetch(t);
10
- if (!a.ok)
11
- throw new Error(`Failed to fetch config: ${a.statusText}`);
12
- const o = await a.json();
13
- r(o);
14
- } catch (a) {
15
- const o = a instanceof Error ? a : new Error("Unknown error occurred");
16
- d(o), console.error("Failed to fetch seat map config:", o);
9
+ const d = await fetch(i);
10
+ if (!d.ok)
11
+ throw new Error(`Failed to fetch config: ${d.statusText}`);
12
+ const f = await d.json();
13
+ c(f);
14
+ } catch (d) {
15
+ const f = d instanceof Error ? d : new Error("Unknown error occurred");
16
+ p(f), console.error("Failed to fetch seat map config:", f);
17
17
  } finally {
18
- g(!1);
18
+ x(!1);
19
19
  }
20
20
  }
21
21
  };
22
- return T(() => {
23
- f();
24
- }, [t]), {
25
- config: c,
26
- loading: S,
27
- error: y,
28
- refetch: f
22
+ return F(() => {
23
+ g();
24
+ }, [i]), {
25
+ config: h,
26
+ loading: y,
27
+ error: w,
28
+ refetch: g
29
29
  };
30
30
  }
31
- const ne = {
31
+ const Fe = {
32
32
  canvasBackground: "#1a1a1a",
33
33
  stageColor: "#808080",
34
34
  seatAvailable: "#2C2B30",
@@ -38,64 +38,64 @@ const ne = {
38
38
  seatHidden: "#4a4a4a",
39
39
  gridLines: "#404040",
40
40
  currency: "KD"
41
- }, Y = W(({ seat: t, state: c, colors: r, onClick: S }) => {
42
- const d = {
43
- available: r.seatAvailable,
44
- reserved: r.seatReserved,
45
- selected: r.seatSelected,
46
- unavailable: r.seatUnavailable,
47
- hidden: r.seatHidden
41
+ }, te = O(({ seat: i, state: h, colors: c, onClick: y }) => {
42
+ const p = {
43
+ available: c.seatAvailable,
44
+ reserved: c.seatReserved,
45
+ selected: c.seatSelected,
46
+ unavailable: c.seatUnavailable,
47
+ hidden: c.seatHidden
48
48
  // Hidden seats are filtered out, but included for type safety
49
- }[c], f = c === "available" || c === "selected", a = $(() => {
50
- f && S(t);
51
- }, [t, S, f]), o = {
52
- x: t.position.x,
53
- y: t.position.y,
54
- fill: d,
49
+ }[h], g = h === "available" || h === "selected", d = W(() => {
50
+ g && y(i);
51
+ }, [i, y, g]), f = {
52
+ x: i.position.x,
53
+ y: i.position.y,
54
+ fill: p,
55
55
  stroke: "#ffffff",
56
56
  strokeWidth: 1,
57
- onClick: a,
58
- onTap: a
57
+ onClick: d,
58
+ onTap: d
59
59
  };
60
- return t.shape === "circle" ? /* @__PURE__ */ s(
61
- ie,
60
+ return i.shape === "circle" ? /* @__PURE__ */ r(
61
+ Me,
62
62
  {
63
- ...o,
63
+ ...f,
64
64
  radius: 12
65
65
  }
66
- ) : /* @__PURE__ */ s(
67
- D,
66
+ ) : /* @__PURE__ */ r(
67
+ ee,
68
68
  {
69
- ...o,
69
+ ...f,
70
70
  width: 24,
71
71
  height: 24,
72
72
  offsetX: 12,
73
73
  offsetY: 12,
74
- cornerRadius: t.shape === "square" ? 0 : 4
74
+ cornerRadius: i.shape === "square" ? 0 : 4
75
75
  }
76
76
  );
77
77
  });
78
- Y.displayName = "ViewerSeat";
79
- const B = W(({ stage: t, stageColor: c }) => /* @__PURE__ */ u(ee, { x: t.position.x, y: t.position.y, children: [
80
- /* @__PURE__ */ s(
81
- D,
78
+ te.displayName = "ViewerSeat";
79
+ const ne = O(({ stage: i, stageColor: h }) => /* @__PURE__ */ v(ke, { x: i.position.x, y: i.position.y, children: [
80
+ /* @__PURE__ */ r(
81
+ ee,
82
82
  {
83
- width: t.config.width,
84
- height: t.config.height,
85
- fill: c + "80",
83
+ width: i.config.width,
84
+ height: i.config.height,
85
+ fill: h + "80",
86
86
  stroke: "#ffffff",
87
87
  strokeWidth: 2,
88
88
  cornerRadius: 10
89
89
  }
90
90
  ),
91
- /* @__PURE__ */ s(
92
- te,
91
+ /* @__PURE__ */ r(
92
+ Ie,
93
93
  {
94
- text: t.config.label,
94
+ text: i.config.label,
95
95
  x: 0,
96
96
  y: 0,
97
- width: t.config.width,
98
- height: t.config.height,
97
+ width: i.config.width,
98
+ height: i.config.height,
99
99
  fontSize: 24,
100
100
  fontStyle: "bold",
101
101
  fill: "#ffffff",
@@ -104,130 +104,236 @@ const B = W(({ stage: t, stageColor: c }) => /* @__PURE__ */ u(ee, { x: t.positi
104
104
  }
105
105
  )
106
106
  ] }));
107
- B.displayName = "ViewerStage";
108
- const ce = ({
109
- config: t,
110
- configUrl: c,
111
- floorId: r,
112
- onFloorChange: S,
113
- reservedSeats: g = [],
114
- unavailableSeats: y = [],
115
- onSeatSelect: d,
116
- onSeatDeselect: f,
117
- onSelectionChange: a,
118
- colorOverrides: o,
119
- showTooltip: se = !0,
120
- zoomEnabled: P = !0,
121
- className: F = "",
122
- onConfigLoad: j,
123
- onError: z
107
+ ne.displayName = "ViewerStage";
108
+ const ie = O(({
109
+ floors: i,
110
+ currentFloorId: h,
111
+ onFloorChange: c,
112
+ showAllOption: y,
113
+ allLabel: x,
114
+ position: w,
115
+ className: p
124
116
  }) => {
125
- const V = Q(null), [m, H] = v(/* @__PURE__ */ new Set()), [R, U] = v(1), [w, X] = v({ x: 0, y: 0 }), { config: q, loading: G, error: x } = re(c), i = t || q, A = N(
126
- () => i ? { ...i.colors, ...o } : { ...ne, ...o },
127
- [i, o]
128
- ), E = N(() => {
129
- if (!i) return [];
130
- let e = i.seats.filter((n) => n.state !== "hidden");
131
- return r && (e = e.filter(
132
- (n) => n.floorId === r || !n.floorId && r === "floor_default"
117
+ const g = k(
118
+ () => [...i].sort((u, E) => u.order - E.order),
119
+ [i]
120
+ ), f = {
121
+ position: "absolute",
122
+ display: "flex",
123
+ alignItems: "center",
124
+ gap: "8px",
125
+ padding: "8px 12px",
126
+ backgroundColor: "rgba(26, 26, 26, 0.95)",
127
+ borderRadius: "8px",
128
+ margin: "12px",
129
+ zIndex: 10,
130
+ ...{
131
+ "top-left": { top: 0, left: 0 },
132
+ "top-right": { top: 0, right: 0 },
133
+ "bottom-left": { bottom: 0, left: 0 },
134
+ "bottom-right": { bottom: 0, right: 0 }
135
+ }[w]
136
+ }, $ = {
137
+ padding: "6px 14px",
138
+ fontSize: "14px",
139
+ fontWeight: 500,
140
+ border: "1px solid #444",
141
+ borderRadius: "6px",
142
+ backgroundColor: "transparent",
143
+ color: "#fff",
144
+ cursor: "pointer",
145
+ transition: "all 0.2s ease"
146
+ }, A = {
147
+ ...$,
148
+ backgroundColor: "#3A7DE5",
149
+ borderColor: "#3A7DE5"
150
+ };
151
+ return /* @__PURE__ */ v("div", { className: p, style: f, children: [
152
+ y && /* @__PURE__ */ r(
153
+ "button",
154
+ {
155
+ type: "button",
156
+ onClick: () => c(null),
157
+ style: h === null ? A : $,
158
+ children: x
159
+ }
160
+ ),
161
+ g.map((u) => /* @__PURE__ */ r(
162
+ "button",
163
+ {
164
+ type: "button",
165
+ onClick: () => c(u.id),
166
+ style: h === u.id ? A : $,
167
+ children: u.name
168
+ },
169
+ u.id
170
+ ))
171
+ ] });
172
+ });
173
+ ie.displayName = "FloorSelectorBar";
174
+ const Ye = ({
175
+ config: i,
176
+ configUrl: h,
177
+ floorId: c,
178
+ onFloorChange: y,
179
+ reservedSeats: x = [],
180
+ unavailableSeats: w = [],
181
+ onSeatSelect: p,
182
+ onSeatDeselect: g,
183
+ onSelectionChange: d,
184
+ colorOverrides: f,
185
+ showTooltip: $ = !0,
186
+ zoomEnabled: A = !0,
187
+ className: u = "",
188
+ onConfigLoad: E,
189
+ onError: D,
190
+ // Floor selector props
191
+ showFloorSelector: U,
192
+ floorSelectorPosition: oe = "top-left",
193
+ floorSelectorClassName: re,
194
+ showAllFloorsOption: se = !0,
195
+ allFloorsLabel: ae = "All",
196
+ fitToView: j = !0,
197
+ fitPadding: H = 40
198
+ }) => {
199
+ const q = we(null), [I, le] = S(/* @__PURE__ */ new Set()), [B, G] = S(1), [R, K] = S({ x: 0, y: 0 }), [ce, de] = S(null), [J, Q] = S(!1), { config: fe, loading: he, error: Y } = Ne(h), n = i || fe, L = c !== void 0, m = L ? c || null : ce, ue = W((e) => {
200
+ L || de(e), y?.(e);
201
+ }, [L, y]), P = n?.floors || [], pe = U !== void 0 ? U : P.length > 1, V = k(
202
+ () => n ? { ...n.colors, ...f } : { ...Fe, ...f },
203
+ [n, f]
204
+ ), M = k(() => {
205
+ if (!n) return [];
206
+ let e = n.seats.filter((t) => t.state !== "hidden");
207
+ return m && (e = e.filter(
208
+ (t) => t.floorId === m || !t.floorId && m === "floor_default"
133
209
  )), e;
134
- }, [i, r]), K = N(() => i?.stages ? r ? i.stages.filter(
135
- (e) => e.floorId === r || !e.floorId && r === "floor_default"
136
- ) : i.stages : [], [i, r]), b = N(() => {
137
- const e = new Set(g), n = new Set(y);
138
- return { reserved: e, unavailable: n };
139
- }, [g, y]), M = $((e) => {
140
- const n = e.id, l = e.seatNumber || "";
141
- return b.unavailable.has(n) || b.unavailable.has(l) ? "unavailable" : b.reserved.has(n) || b.reserved.has(l) ? "reserved" : m.has(n) ? "selected" : e.state;
142
- }, [b, m]);
143
- T(() => {
144
- i && j && j(i);
145
- }, [i, j]), T(() => {
146
- x && z && z(x);
147
- }, [x, z]);
148
- const O = $((e) => {
149
- const n = M(e);
150
- if (n !== "available" && n !== "selected")
210
+ }, [n, m]), T = k(() => n?.stages ? m ? n.stages.filter(
211
+ (e) => e.floorId === m || !e.floorId && m === "floor_default"
212
+ ) : n.stages : [], [n, m]), b = k(() => {
213
+ if (!n || M.length === 0 && T.length === 0)
214
+ return null;
215
+ const e = 12;
216
+ let t = 1 / 0, o = 1 / 0, s = -1 / 0, a = -1 / 0;
217
+ return M.forEach((l) => {
218
+ t = Math.min(t, l.position.x - e), o = Math.min(o, l.position.y - e), s = Math.max(s, l.position.x + e), a = Math.max(a, l.position.y + e);
219
+ }), T.forEach((l) => {
220
+ t = Math.min(t, l.position.x), o = Math.min(o, l.position.y), s = Math.max(s, l.position.x + (l.config?.width || 200)), a = Math.max(a, l.position.y + (l.config?.height || 100));
221
+ }), { minX: t, minY: o, maxX: s, maxY: a, width: s - t, height: a - o };
222
+ }, [n, M, T]);
223
+ F(() => {
224
+ if (!j || J || !n || !b) return;
225
+ const e = n.canvas.width, t = n.canvas.height, o = e - H * 2, s = t - H * 2, a = o / b.width, l = s / b.height, N = Math.min(a, l, 2), C = b.minX + b.width / 2, ye = b.minY + b.height / 2, xe = e / 2, be = t / 2, ve = xe - C * N, Se = be - ye * N;
226
+ G(N), K({ x: ve, y: Se }), Q(!0);
227
+ }, [j, J, n, b, H]), F(() => {
228
+ j && Q(!1);
229
+ }, [m, j]);
230
+ const z = k(() => {
231
+ const e = new Set(x), t = new Set(w);
232
+ return { reserved: e, unavailable: t };
233
+ }, [x, w]), _ = W((e) => {
234
+ const t = e.id, o = e.seatNumber || "";
235
+ return z.unavailable.has(t) || z.unavailable.has(o) ? "unavailable" : z.reserved.has(t) || z.reserved.has(o) ? "reserved" : I.has(t) ? "selected" : e.state;
236
+ }, [z, I]);
237
+ F(() => {
238
+ n && E && E(n);
239
+ }, [n, E]), F(() => {
240
+ Y && D && D(Y);
241
+ }, [Y, D]);
242
+ const ge = W((e) => {
243
+ const t = _(e);
244
+ if (t !== "available" && t !== "selected")
151
245
  return;
152
- const l = m.has(e.id);
153
- H((h) => {
154
- const p = new Set(h);
155
- return l ? p.delete(e.id) : p.add(e.id), p;
156
- }), l ? f?.(e) : (d?.(e), d || console.log("Seat selected:", e));
157
- }, [M, m, d, f]), C = N(() => i ? E.filter((e) => m.has(e.id)) : [], [E, m]);
158
- T(() => {
159
- a?.(C);
160
- }, [C, a]);
161
- const I = $((e) => {
162
- if (!P) return;
246
+ const o = I.has(e.id);
247
+ le((s) => {
248
+ const a = new Set(s);
249
+ return o ? a.delete(e.id) : a.add(e.id), a;
250
+ }), o ? g?.(e) : (p?.(e), p || console.log("Seat selected:", e));
251
+ }, [_, I, p, g]), X = k(() => n ? M.filter((e) => I.has(e.id)) : [], [M, I]);
252
+ F(() => {
253
+ d?.(X);
254
+ }, [X, d]);
255
+ const me = W((e) => {
256
+ if (!A) return;
163
257
  e.evt.preventDefault();
164
- const n = V.current;
165
- if (!n) return;
166
- const l = R, h = n.getPointerPosition();
167
- if (!h) return;
168
- const p = {
169
- x: (h.x - w.x) / l,
170
- y: (h.y - w.y) / l
171
- }, J = e.evt.deltaY > 0 ? -1 : 1, _ = 1.05;
172
- let k = J > 0 ? l * _ : l / _;
173
- k = Math.max(0.5, Math.min(5, k)), U(k), X({
174
- x: h.x - p.x * k,
175
- y: h.y - p.y * k
258
+ const t = q.current;
259
+ if (!t) return;
260
+ const o = B, s = t.getPointerPosition();
261
+ if (!s) return;
262
+ const a = {
263
+ x: (s.x - R.x) / o,
264
+ y: (s.y - R.y) / o
265
+ }, l = e.evt.deltaY > 0 ? -1 : 1, N = 1.05;
266
+ let C = l > 0 ? o * N : o / N;
267
+ C = Math.max(0.5, Math.min(5, C)), G(C), K({
268
+ x: s.x - a.x * C,
269
+ y: s.y - a.y * C
176
270
  });
177
- }, [P, R, w]);
178
- return G ? /* @__PURE__ */ s("div", { className: `flex items-center justify-center h-full ${F}`, children: /* @__PURE__ */ s("p", { children: "Loading seat map..." }) }) : x ? /* @__PURE__ */ s("div", { className: `flex items-center justify-center h-full ${F}`, children: /* @__PURE__ */ u("p", { className: "text-red-500", children: [
271
+ }, [A, B, R]);
272
+ return he ? /* @__PURE__ */ r("div", { className: `flex items-center justify-center h-full ${u}`, children: /* @__PURE__ */ r("p", { children: "Loading seat map..." }) }) : Y ? /* @__PURE__ */ r("div", { className: `flex items-center justify-center h-full ${u}`, children: /* @__PURE__ */ v("p", { className: "text-red-500", children: [
179
273
  "Error loading seat map: ",
180
- x.message
181
- ] }) }) : i ? /* @__PURE__ */ u("div", { className: `relative ${F}`, children: [
182
- /* @__PURE__ */ u(
183
- Z,
274
+ Y.message
275
+ ] }) }) : n ? /* @__PURE__ */ v("div", { className: `relative ${u}`, children: [
276
+ pe && P.length > 0 && /* @__PURE__ */ r(
277
+ ie,
278
+ {
279
+ floors: P,
280
+ currentFloorId: m,
281
+ onFloorChange: ue,
282
+ showAllOption: se,
283
+ allLabel: ae,
284
+ position: oe,
285
+ className: re
286
+ }
287
+ ),
288
+ /* @__PURE__ */ v(
289
+ Ce,
184
290
  {
185
- ref: V,
186
- width: i.canvas.width,
187
- height: i.canvas.height,
188
- scaleX: R,
189
- scaleY: R,
190
- x: w.x,
191
- y: w.y,
192
- onWheel: I,
193
- style: { backgroundColor: i.canvas.backgroundColor },
291
+ ref: q,
292
+ width: n.canvas.width,
293
+ height: n.canvas.height,
294
+ scaleX: B,
295
+ scaleY: B,
296
+ x: R.x,
297
+ y: R.y,
298
+ onWheel: me,
299
+ style: { backgroundColor: n.canvas.backgroundColor },
194
300
  children: [
195
- /* @__PURE__ */ s(L, { listening: !1, children: K.map((e) => /* @__PURE__ */ s(
196
- B,
301
+ /* @__PURE__ */ r(Z, { listening: !1, children: T.map((e) => /* @__PURE__ */ r(
302
+ ne,
197
303
  {
198
304
  stage: e,
199
- stageColor: A.stageColor
305
+ stageColor: V.stageColor
200
306
  },
201
307
  e.id
202
308
  )) }),
203
- /* @__PURE__ */ s(L, { children: E.map((e) => /* @__PURE__ */ s(
204
- Y,
309
+ /* @__PURE__ */ r(Z, { children: M.map((e) => /* @__PURE__ */ r(
310
+ te,
205
311
  {
206
312
  seat: e,
207
- state: M(e),
208
- colors: A,
209
- onClick: O
313
+ state: _(e),
314
+ colors: V,
315
+ onClick: ge
210
316
  },
211
317
  e.id
212
318
  )) })
213
319
  ]
214
320
  }
215
321
  ),
216
- C.length > 0 && /* @__PURE__ */ u("div", { className: "absolute top-4 right-4 bg-white dark:bg-gray-800 p-4 rounded shadow-lg", children: [
217
- /* @__PURE__ */ u("h3", { className: "font-semibold mb-2", children: [
322
+ X.length > 0 && /* @__PURE__ */ v("div", { className: "absolute top-4 right-4 bg-white dark:bg-gray-800 p-4 rounded shadow-lg", children: [
323
+ /* @__PURE__ */ v("h3", { className: "font-semibold mb-2", children: [
218
324
  "Selected Seats (",
219
- C.length,
325
+ X.length,
220
326
  ")"
221
327
  ] }),
222
- /* @__PURE__ */ s("div", { className: "max-h-48 overflow-y-auto space-y-1", children: C.map((e) => /* @__PURE__ */ u("div", { className: "text-sm", children: [
328
+ /* @__PURE__ */ r("div", { className: "max-h-48 overflow-y-auto space-y-1", children: X.map((e) => /* @__PURE__ */ v("div", { className: "text-sm", children: [
223
329
  e.seatNumber,
224
- e.price && ` - ${A.currency} ${e.price.toFixed(2)}`
330
+ e.price && ` - ${V.currency} ${e.price.toFixed(2)}`
225
331
  ] }, e.id)) })
226
332
  ] })
227
- ] }) : /* @__PURE__ */ s("div", { className: `flex items-center justify-center h-full ${F}`, children: /* @__PURE__ */ s("p", { children: "No configuration provided" }) });
333
+ ] }) : /* @__PURE__ */ r("div", { className: `flex items-center justify-center h-full ${u}`, children: /* @__PURE__ */ r("p", { children: "No configuration provided" }) });
228
334
  };
229
335
  export {
230
- ne as DEFAULT_COLORS,
231
- ce as SeatMapViewer,
232
- re as useConfigFetcher
336
+ Fe as DEFAULT_COLORS,
337
+ Ye as SeatMapViewer,
338
+ Ne as useConfigFetcher
233
339
  };
@@ -44,6 +44,12 @@ export interface SerializedStage {
44
44
  config: any;
45
45
  floorId?: string;
46
46
  }
47
+ export interface FloorConfig {
48
+ id: string;
49
+ name: string;
50
+ order: number;
51
+ color?: string;
52
+ }
47
53
  export interface ColorSettings {
48
54
  canvasBackground: string;
49
55
  stageColor: string;
@@ -74,5 +80,6 @@ export interface SeatMapConfig {
74
80
  seats: SerializedSeat[];
75
81
  sections?: SerializedSection[];
76
82
  stages?: SerializedStage[];
83
+ floors?: FloorConfig[];
77
84
  }
78
85
  export declare const DEFAULT_COLORS: ColorSettings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zonetrix/viewer",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "Lightweight React component for rendering interactive seat maps",
6
6
  "main": "./dist/index.js",