@seatlayer/js 0.2.0 → 0.4.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 +56 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -109,6 +109,9 @@ var SeatingChart = class {
|
|
|
109
109
|
this.mount = null;
|
|
110
110
|
this.hostEl = null;
|
|
111
111
|
this.rendered = false;
|
|
112
|
+
this.tipEl = null;
|
|
113
|
+
this.tipPos = { x: 0, y: 0 };
|
|
114
|
+
this.onTipMove = null;
|
|
112
115
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
113
116
|
if (!options.container) throw new Error("seatmap: `container` is required");
|
|
114
117
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
@@ -130,6 +133,13 @@ var SeatingChart = class {
|
|
|
130
133
|
onError: (err) => this.opts.onError?.(err),
|
|
131
134
|
onDeckTap: (floorId) => this.opts.onDeckTap?.(floorId),
|
|
132
135
|
onHint: (message) => this.opts.onHint?.(message),
|
|
136
|
+
// Live-activity cue: pulse seats that other buyers take while the map is
|
|
137
|
+
// open — the WS feed already streams the status change, this makes it felt.
|
|
138
|
+
flashOnLiveChange: true,
|
|
139
|
+
onSeatHover: (details) => {
|
|
140
|
+
this.opts.onSeatHover?.(details);
|
|
141
|
+
if (this.opts.seatTooltip !== false) this.updateTooltip(details);
|
|
142
|
+
},
|
|
133
143
|
colorblindSafe: options.colorblindSafe
|
|
134
144
|
});
|
|
135
145
|
}
|
|
@@ -151,6 +161,19 @@ var SeatingChart = class {
|
|
|
151
161
|
this.rendered = false;
|
|
152
162
|
return this;
|
|
153
163
|
}
|
|
164
|
+
if (this.opts.seatTooltip !== false) {
|
|
165
|
+
const tip = document.createElement("div");
|
|
166
|
+
tip.setAttribute("role", "tooltip");
|
|
167
|
+
tip.style.cssText = 'position:absolute;z-index:7;pointer-events:none;display:none;max-width:240px;background:#10162a;color:#fff;border-radius:10px;padding:9px 12px;font:500 12px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;box-shadow:0 10px 30px -10px rgba(0,0,0,.5);';
|
|
168
|
+
host.appendChild(tip);
|
|
169
|
+
this.tipEl = tip;
|
|
170
|
+
this.onTipMove = (e) => {
|
|
171
|
+
const r = host.getBoundingClientRect();
|
|
172
|
+
this.tipPos = { x: e.clientX - r.left, y: e.clientY - r.top };
|
|
173
|
+
if (this.tipEl && this.tipEl.style.display !== "none") this.placeTooltip();
|
|
174
|
+
};
|
|
175
|
+
host.addEventListener("mousemove", this.onTipMove);
|
|
176
|
+
}
|
|
154
177
|
if (info.mode === "test") {
|
|
155
178
|
host.style.overflow = "hidden";
|
|
156
179
|
const ribbon = document.createElement("div");
|
|
@@ -161,6 +184,36 @@ var SeatingChart = class {
|
|
|
161
184
|
}
|
|
162
185
|
return this;
|
|
163
186
|
}
|
|
187
|
+
placeTooltip() {
|
|
188
|
+
if (!this.tipEl || !this.hostEl) return;
|
|
189
|
+
const hw = this.hostEl.clientWidth;
|
|
190
|
+
const tw = this.tipEl.offsetWidth;
|
|
191
|
+
const th = this.tipEl.offsetHeight;
|
|
192
|
+
let x = this.tipPos.x + 14;
|
|
193
|
+
let y = this.tipPos.y - th - 12;
|
|
194
|
+
if (x + tw > hw - 8) x = this.tipPos.x - tw - 14;
|
|
195
|
+
if (y < 8) y = this.tipPos.y + 18;
|
|
196
|
+
this.tipEl.style.left = `${Math.max(8, x)}px`;
|
|
197
|
+
this.tipEl.style.top = `${Math.max(8, y)}px`;
|
|
198
|
+
}
|
|
199
|
+
updateTooltip(details) {
|
|
200
|
+
if (!this.tipEl) return;
|
|
201
|
+
if (!details) {
|
|
202
|
+
this.tipEl.style.display = "none";
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const money = (() => {
|
|
206
|
+
try {
|
|
207
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency: details.currency }).format(details.price);
|
|
208
|
+
} catch {
|
|
209
|
+
return `${details.price} ${details.currency}`;
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
const statusLine = details.status === "free" ? "" : `<div style="margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:#fca5a5;font-weight:700">${details.status === "held" ? (0, import_core.t)("map.statusHeld") : (0, import_core.t)("map.statusTaken")}</div>`;
|
|
213
|
+
this.tipEl.innerHTML = `<div style="font-weight:700;font-size:13px">${details.label}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px;color:#c7cddc"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span>${details.categoryLabel}</span><span style="margin-left:auto;font-weight:700;color:#fff">${money}</span></div>` + statusLine;
|
|
214
|
+
this.tipEl.style.display = "block";
|
|
215
|
+
this.placeTooltip();
|
|
216
|
+
}
|
|
164
217
|
/** Current selection with prices resolved from the chart categories. */
|
|
165
218
|
getSelection() {
|
|
166
219
|
return this.controller.getSelection();
|
|
@@ -233,6 +286,9 @@ var SeatingChart = class {
|
|
|
233
286
|
}
|
|
234
287
|
/** Tear everything down: close the socket, stop timers, drop the canvas. */
|
|
235
288
|
destroy() {
|
|
289
|
+
if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
|
|
290
|
+
this.tipEl = null;
|
|
291
|
+
this.onTipMove = null;
|
|
236
292
|
this.controller.destroy();
|
|
237
293
|
if (this.hostEl && this.hostEl.parentNode) this.hostEl.parentNode.removeChild(this.hostEl);
|
|
238
294
|
this.hostEl = null;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/SeatingChart.ts","../src/api.ts"],"sourcesContent":["/**\n * @seatlayer/js — the framework-agnostic SeatLayer embed SDK.\n *\n * Works in any JS environment (plain HTML, React, Vue, Svelte, Angular, …).\n * Framework wrappers (@seatlayer/react, …) build on top of this.\n */\nexport { SeatingChart } from './SeatingChart';\nexport type { SeatingChartOptions, SelectedSeat, GAAreaAvailability } from './SeatingChart';\nexport { ApiError } from './api';\nexport type { HoldResult, HoldConflict, HoldLineItem, BestAvailableResult } from './api';\n","/**\n * SeatingChart — the embeddable buyer picker.\n *\n * A thin wrapper over the shared PickerController (src/picker/PickerController):\n * it owns the mount <div> + the public embed contract (hold-only — the SDK hands\n * the holdId to the host page for a server-side book) and delegates all transport\n * + booking to the controller, so the SDK inherits every fix made for the live\n * buyer page and the demo picker.\n */\nimport { PickerController, loadLocale, setStringOverrides, t, type PickerSeat } from '@seatlayer/core';\nimport { PubApi, type BestAvailableResult, type HoldResult } from './api';\n\nconst DEFAULT_API_BASE = 'https://api.seatlayer.io';\nconst DEFAULT_MAX_SELECTION = 10;\n\n/** A seat as surfaced to the host page (prices resolved from the chart's categories). */\nexport type SelectedSeat = PickerSeat;\nexport interface GAAreaAvailability {\n id: string; label: string; capacity: number; available: number; categoryKey: string; price: number; currency: string;\n tiers?: Array<{ id: string; name: string; price: number }>;\n}\n\nexport interface SeatingChartOptions {\n /** CSS selector or an HTMLElement to render into. */\n container: string | HTMLElement;\n /** Event key, e.g. `ev_xxx`. */\n event: string;\n /** API origin. Defaults to https://api.seatlayer.io. */\n apiBase?: string;\n /** Reserved for future authenticated rendering — accepted + stored, not yet sent. */\n publicKey?: string;\n /** Max seats selectable at once (default 10). */\n maxSelection?: number;\n /**\n * BCP 47 language for the widget UI — `'de'`, `'es-MX'`, etc. Falls back to\n * the browser language, then English. Built-in: en, es, de, fr. The German\n * bundle (etc.) is fetched on demand so unused languages cost nothing.\n */\n locale?: string;\n /**\n * Per-key string overrides layered over the active locale — white-label copy\n * without shipping a whole bundle, e.g. `{ 'map.fromPrice': 'ab {price}' }`.\n */\n messages?: Record<string, string>;\n /** ISO 4217 currency for on-map prices (default USD). */\n currency?: string;\n /**\n * Colorblind-safe rendering: category hues switch to an Okabe-Ito palette\n * and booked seats render hollow, so state never relies on hue alone.\n * Toggleable later with setColorblindSafe().\n */\n colorblindSafe?: boolean;\n onSelectionChange?: (seats: SelectedSeat[]) => void;\n onHold?: (result: HoldResult) => void;\n onHoldExpired?: () => void;\n onGAClick?: (area: GAAreaAvailability) => void;\n onError?: (err: unknown) => void;\n /**\n * Multi-floor charts only: fires when the buyer taps a deck in the stacked\n * 3D view, after the picker switches to that floor — lets the host page sync\n * its own floor UI (tabs, labels) with the map.\n */\n onDeckTap?: (floorId: string) => void;\n /**\n * Non-blocking, localized selection advice — currently the orphan-seat hint\n * (the selection would strand a single free seat between taken neighbors).\n * `null` clears it. Purely informational; nothing is ever prevented.\n */\n onHint?: (message: string | null) => void;\n}\n\nfunction resolveContainer(container: string | HTMLElement): HTMLElement {\n if (typeof container === 'string') {\n const el = document.querySelector(container);\n if (!el) throw new Error(`seatmap: container \"${container}\" not found`);\n return el as HTMLElement;\n }\n if (!(container instanceof HTMLElement)) {\n throw new Error('seatmap: container must be a CSS selector or an HTMLElement');\n }\n return container;\n}\n\nexport class SeatingChart {\n private readonly opts: SeatingChartOptions;\n private readonly controller: PickerController;\n /** Reserved for future authenticated rendering — stored, not yet sent on any request. */\n readonly publicKey?: string;\n\n private mount: HTMLElement | null = null;\n private hostEl: HTMLDivElement | null = null;\n private rendered = false;\n\n constructor(options: SeatingChartOptions) {\n if (!options || typeof options !== 'object') throw new Error('seatmap: options object is required');\n if (!options.container) throw new Error('seatmap: `container` is required');\n if (!options.event || typeof options.event !== 'string') throw new Error('seatmap: `event` key is required');\n\n this.opts = options;\n this.publicKey = options.publicKey;\n const api = new PubApi((options.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, ''));\n this.controller = new PickerController({\n transport: api,\n eventKey: options.event,\n maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION,\n currency: options.currency,\n onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),\n onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),\n onHoldExpired: () => this.opts.onHoldExpired?.(),\n onGAClick: (areaId) => {\n const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);\n if (area) this.opts.onGAClick?.(area);\n },\n onError: (err) => this.opts.onError?.(err),\n onDeckTap: (floorId) => this.opts.onDeckTap?.(floorId),\n onHint: (message) => this.opts.onHint?.(message),\n colorblindSafe: options.colorblindSafe,\n });\n }\n\n /** Fetch the chart, mount the renderer, seed statuses and go live. Idempotent. */\n async render(): Promise<this> {\n if (this.rendered) return this;\n this.rendered = true;\n\n // Resolve + load the UI language before the first paint so on-map labels\n // (\"N LEFT\", \"FROM …\", the map aria-label) render translated. English and\n // already-loaded locales resolve synchronously; others fetch one small chunk.\n await loadLocale(this.opts.locale);\n if (this.opts.messages) setStringOverrides(this.opts.messages);\n\n // Mount an owned <div> inside the caller's container so we never fight their\n // layout and can cleanly remove it on destroy().\n this.mount = resolveContainer(this.opts.container);\n const host = document.createElement('div');\n host.style.width = '100%';\n host.style.height = '100%';\n host.style.position = 'relative';\n this.mount.appendChild(host);\n this.hostEl = host;\n\n const info = await this.controller.render(host);\n if (!info) {\n this.rendered = false;\n return this;\n }\n if (info.mode === 'test') {\n host.style.overflow = 'hidden';\n const ribbon = document.createElement('div');\n ribbon.textContent = t('picker.testMode');\n ribbon.setAttribute('aria-label', t('picker.testMode'));\n ribbon.style.cssText =\n 'position:absolute;top:18px;right:-34px;z-index:6;transform:rotate(45deg);' +\n 'width:140px;text-align:center;padding:4px 0;background:#f4b740;color:#1a1200;' +\n 'font:800 10.5px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;letter-spacing:.12em;' +\n 'box-shadow:0 2px 8px rgba(0,0,0,.25);pointer-events:none;';\n host.appendChild(ribbon);\n }\n return this;\n }\n\n /** Current selection with prices resolved from the chart categories. */\n getSelection(): SelectedSeat[] {\n return this.controller.getSelection();\n }\n\n /** Hold the current selection. Resolves the hold, or null on a 409 conflict. */\n async hold(options: { ttlMs?: number } = {}): Promise<HoldResult | null> {\n try {\n const h = await this.controller.hold(undefined, options.ttlMs);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n getGAAreas(): GAAreaAvailability[] {\n return this.controller.getGAAreas();\n }\n\n async holdGA(\n areaId: string,\n qty: number,\n options: { tierId?: string | null; ttlMs?: number } = {},\n ): Promise<HoldResult | null> {\n try {\n const h = await this.controller.holdGA(areaId, qty, options);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n /** Ask the server for the `qty` best free seats and hold them atomically. */\n async bestAvailable(qty: number, categoryKey?: string): Promise<BestAvailableResult | null> {\n try {\n const h = await this.controller.bestAvailable(qty, categoryKey);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, labels: h.labels, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n /**\n * Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's\n * available `tiers` are on each `SelectedSeat` from `getSelection()` /\n * `onSelectionChange`. Re-emits the selection with the new tier + price, and\n * the tier rides along in the next `hold()` / `onHold` per seat. `tierId=null`\n * reverts to the default tier.\n */\n setSeatTier(seatId: string, tierId: string | null): void {\n this.controller.setSeatTier(seatId, tierId);\n }\n\n /**\n * Floors of a multi-floor chart — `[{ id, name }]` (single-floor charts\n * return one entry; empty before render()). Pair with setFloor() to build a\n * host-side floor switcher.\n */\n getFloors(): { id: string; name: string }[] {\n return this.controller.getFloors();\n }\n\n /** Switch the shown floor (2D). Warns + no-ops on single-floor charts. */\n setFloor(floorId: string): void {\n if (this.controller.getFloors().length <= 1) {\n console.warn('seatmap: setFloor() ignored — this chart has a single floor');\n return;\n }\n this.controller.setFloor(floorId);\n }\n\n /** Toggle colorblind-safe rendering at runtime (see options.colorblindSafe). */\n setColorblindSafe(on: boolean): void {\n this.controller.setColorblindSafe(on);\n }\n\n /** Release the current hold (if any). No-op when nothing is held. */\n async release(): Promise<void> {\n await this.controller.release();\n }\n\n /** Tear everything down: close the socket, stop timers, drop the canvas. */\n destroy(): void {\n this.controller.destroy();\n if (this.hostEl && this.hostEl.parentNode) this.hostEl.parentNode.removeChild(this.hostEl);\n this.hostEl = null;\n this.mount = null;\n this.rendered = false;\n }\n}\n","/**\n * Minimal client for the public embed surface of workers/api (the `/pub/*`\n * routes). Deliberately self-contained — it does NOT reuse src/lib/api.ts,\n * which bakes in a build-time API base and dashboard session credentials. The\n * SDK runs cross-origin on a third-party ticketing page, so:\n * - apiBase is per-instance (constructor option), not a build constant;\n * - credentials are omitted (no cookie to send, avoids CORS-credential setup);\n * - no custom headers on mutating calls (keeps the CORS preflight trivial).\n */\nimport type { ChartDoc, PickerSeat as SelectedSeat } from '@seatlayer/core';\n\nexport interface HoldConflict {\n label: string;\n status: string;\n}\n\nexport interface HoldLineItem {\n label: string; objectId: string; objectType: 'seat' | 'booth' | 'ga'; categoryKey: string;\n tierId: string | null;\n /** Price in major currency units (for example 45 means $45.00). */\n unitPrice: number;\n currency: string;\n quantity?: number;\n}\n\nexport class ApiError extends Error {\n status: number;\n code?: string;\n /** Present when a hold 409s because seats were just taken/held. */\n conflicts?: HoldConflict[];\n /** Present when best-available 409s ('not_enough_together' | 'sold_out'). */\n reason?: string;\n\n constructor(status: number, message: string, code?: string, conflicts?: HoldConflict[], reason?: string) {\n super(message);\n this.name = 'ApiError';\n this.status = status;\n this.code = code;\n this.conflicts = conflicts;\n this.reason = reason;\n }\n}\n\nexport interface PubChartResult {\n event: { key: string; name: string };\n doc: ChartDoc;\n}\n\nexport interface PubObjectsResult {\n /** Every non-free seat's status, keyed by seat label. */\n seats: Record<string, string>;\n updatedAt: number;\n}\n\nexport interface HoldResult {\n holdId: string;\n expiresAt: number;\n /** The held seats with the buyer's chosen ticket tier per seat (present on hold). */\n seats?: SelectedSeat[];\n items?: HoldLineItem[];\n}\n\n/** Best-available response — the server-picked seats plus the hold they landed in. */\nexport interface BestAvailableResult {\n holdId: string;\n expiresAt: number;\n labels: string[];\n seats?: SelectedSeat[];\n items?: HoldResult['items'];\n}\n\nasync function request<T>(\n base: string,\n path: string,\n init: { method?: 'GET' | 'POST'; body?: unknown } = {},\n): Promise<T> {\n const method = init.method ?? 'GET';\n const headers: Record<string, string> = {};\n let body: string | undefined;\n if (init.body !== undefined) {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(init.body);\n }\n\n const res = await fetch(`${base}${path}`, { method, headers, body, credentials: 'omit' });\n\n const isJson = (res.headers.get('content-type') ?? '').includes('application/json');\n const data = isJson ? await res.json().catch(() => null) : null;\n\n if (!res.ok) {\n const err = data as\n | { error?: string; code?: string; conflicts?: HoldConflict[]; reason?: string }\n | null;\n throw new ApiError(res.status, err?.error ?? `request_failed_${res.status}`, err?.code, err?.conflicts, err?.reason);\n }\n return data as T;\n}\n\n/** Public-surface client bound to one apiBase (e.g. https://api.seatlayer.io). */\nexport class PubApi {\n constructor(private readonly base: string) {}\n\n chart(key: string): Promise<PubChartResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/chart`);\n }\n\n objects(key: string): Promise<PubObjectsResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/objects`);\n }\n\n hold(key: string, selections: Array<{ label: string; tierId?: string | null }>, ttlMs?: number, replaceHoldId?: string): Promise<HoldResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold`, {\n method: 'POST',\n body: { selections, ...(ttlMs ? { ttlMs } : {}), ...(replaceHoldId ? { replaceHoldId } : {}) },\n });\n }\n\n bestAvailable(key: string, qty: number, categoryKey?: string): Promise<BestAvailableResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/best-available`, {\n method: 'POST',\n body: { qty, ...(categoryKey ? { categoryKey } : {}) },\n });\n }\n\n release(key: string, labels: string[], holdId: string): Promise<{ ok: true }> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {\n method: 'POST',\n body: { labels, holdId },\n });\n }\n\n socketUrl(key: string): string {\n const wsBase = this.base.replace(/^http/, 'ws');\n return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/SeatingChart.ts","../src/api.ts"],"sourcesContent":["/**\n * @seatlayer/js — the framework-agnostic SeatLayer embed SDK.\n *\n * Works in any JS environment (plain HTML, React, Vue, Svelte, Angular, …).\n * Framework wrappers (@seatlayer/react, …) build on top of this.\n */\nexport { SeatingChart } from './SeatingChart';\nexport type { SeatingChartOptions, SelectedSeat, GAAreaAvailability } from './SeatingChart';\nexport { ApiError } from './api';\nexport type { HoldResult, HoldConflict, HoldLineItem, BestAvailableResult } from './api';\nexport type { SeatHoverDetails } from '@seatlayer/core';\n","/**\n * SeatingChart — the embeddable buyer picker.\n *\n * A thin wrapper over the shared PickerController (src/picker/PickerController):\n * it owns the mount <div> + the public embed contract (hold-only — the SDK hands\n * the holdId to the host page for a server-side book) and delegates all transport\n * + booking to the controller, so the SDK inherits every fix made for the live\n * buyer page and the demo picker.\n */\nimport { PickerController, loadLocale, setStringOverrides, t, type PickerSeat, type SeatHoverDetails } from '@seatlayer/core';\nimport { PubApi, type BestAvailableResult, type HoldResult } from './api';\n\nconst DEFAULT_API_BASE = 'https://api.seatlayer.io';\nconst DEFAULT_MAX_SELECTION = 10;\n\n/** A seat as surfaced to the host page (prices resolved from the chart's categories). */\nexport type SelectedSeat = PickerSeat;\nexport interface GAAreaAvailability {\n id: string; label: string; capacity: number; available: number; categoryKey: string; price: number; currency: string;\n tiers?: Array<{ id: string; name: string; price: number }>;\n}\n\nexport interface SeatingChartOptions {\n /** CSS selector or an HTMLElement to render into. */\n container: string | HTMLElement;\n /** Event key, e.g. `ev_xxx`. */\n event: string;\n /** API origin. Defaults to https://api.seatlayer.io. */\n apiBase?: string;\n /** Reserved for future authenticated rendering — accepted + stored, not yet sent. */\n publicKey?: string;\n /** Max seats selectable at once (default 10). */\n maxSelection?: number;\n /**\n * BCP 47 language for the widget UI — `'de'`, `'es-MX'`, etc. Falls back to\n * the browser language, then English. Built-in: en, es, de, fr. The German\n * bundle (etc.) is fetched on demand so unused languages cost nothing.\n */\n locale?: string;\n /**\n * Per-key string overrides layered over the active locale — white-label copy\n * without shipping a whole bundle, e.g. `{ 'map.fromPrice': 'ab {price}' }`.\n */\n messages?: Record<string, string>;\n /** ISO 4217 currency for on-map prices (default USD). */\n currency?: string;\n /**\n * Colorblind-safe rendering: category hues switch to an Okabe-Ito palette\n * and booked seats render hollow, so state never relies on hue alone.\n * Toggleable later with setColorblindSafe().\n */\n colorblindSafe?: boolean;\n /**\n * Built-in seat tooltip on mouse hover (seat · category · price · status).\n * Rendered inside the widget so every host gets it; default true. Turn off\n * to draw your own popover from onSeatHover.\n */\n seatTooltip?: boolean;\n /**\n * Seat hover with everything a popover needs (category label/color, resolved\n * tier-aware price, live status, currency); null on hover-out. Fires whether\n * or not the built-in tooltip is enabled.\n */\n onSeatHover?: (details: SeatHoverDetails | null) => void;\n onSelectionChange?: (seats: SelectedSeat[]) => void;\n onHold?: (result: HoldResult) => void;\n onHoldExpired?: () => void;\n onGAClick?: (area: GAAreaAvailability) => void;\n onError?: (err: unknown) => void;\n /**\n * Multi-floor charts only: fires when the buyer taps a deck in the stacked\n * 3D view, after the picker switches to that floor — lets the host page sync\n * its own floor UI (tabs, labels) with the map.\n */\n onDeckTap?: (floorId: string) => void;\n /**\n * Non-blocking, localized selection advice — currently the orphan-seat hint\n * (the selection would strand a single free seat between taken neighbors).\n * `null` clears it. Purely informational; nothing is ever prevented.\n */\n onHint?: (message: string | null) => void;\n}\n\nfunction resolveContainer(container: string | HTMLElement): HTMLElement {\n if (typeof container === 'string') {\n const el = document.querySelector(container);\n if (!el) throw new Error(`seatmap: container \"${container}\" not found`);\n return el as HTMLElement;\n }\n if (!(container instanceof HTMLElement)) {\n throw new Error('seatmap: container must be a CSS selector or an HTMLElement');\n }\n return container;\n}\n\nexport class SeatingChart {\n private readonly opts: SeatingChartOptions;\n private readonly controller: PickerController;\n /** Reserved for future authenticated rendering — stored, not yet sent on any request. */\n readonly publicKey?: string;\n\n private mount: HTMLElement | null = null;\n private hostEl: HTMLDivElement | null = null;\n private rendered = false;\n private tipEl: HTMLDivElement | null = null;\n private tipPos = { x: 0, y: 0 };\n private onTipMove: ((e: MouseEvent) => void) | null = null;\n\n constructor(options: SeatingChartOptions) {\n if (!options || typeof options !== 'object') throw new Error('seatmap: options object is required');\n if (!options.container) throw new Error('seatmap: `container` is required');\n if (!options.event || typeof options.event !== 'string') throw new Error('seatmap: `event` key is required');\n\n this.opts = options;\n this.publicKey = options.publicKey;\n const api = new PubApi((options.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, ''));\n this.controller = new PickerController({\n transport: api,\n eventKey: options.event,\n maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION,\n currency: options.currency,\n onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),\n onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),\n onHoldExpired: () => this.opts.onHoldExpired?.(),\n onGAClick: (areaId) => {\n const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);\n if (area) this.opts.onGAClick?.(area);\n },\n onError: (err) => this.opts.onError?.(err),\n onDeckTap: (floorId) => this.opts.onDeckTap?.(floorId),\n onHint: (message) => this.opts.onHint?.(message),\n // Live-activity cue: pulse seats that other buyers take while the map is\n // open — the WS feed already streams the status change, this makes it felt.\n flashOnLiveChange: true,\n onSeatHover: (details) => {\n this.opts.onSeatHover?.(details);\n if (this.opts.seatTooltip !== false) this.updateTooltip(details);\n },\n colorblindSafe: options.colorblindSafe,\n });\n }\n\n /** Fetch the chart, mount the renderer, seed statuses and go live. Idempotent. */\n async render(): Promise<this> {\n if (this.rendered) return this;\n this.rendered = true;\n\n // Resolve + load the UI language before the first paint so on-map labels\n // (\"N LEFT\", \"FROM …\", the map aria-label) render translated. English and\n // already-loaded locales resolve synchronously; others fetch one small chunk.\n await loadLocale(this.opts.locale);\n if (this.opts.messages) setStringOverrides(this.opts.messages);\n\n // Mount an owned <div> inside the caller's container so we never fight their\n // layout and can cleanly remove it on destroy().\n this.mount = resolveContainer(this.opts.container);\n const host = document.createElement('div');\n host.style.width = '100%';\n host.style.height = '100%';\n host.style.position = 'relative';\n this.mount.appendChild(host);\n this.hostEl = host;\n\n const info = await this.controller.render(host);\n if (!info) {\n this.rendered = false;\n return this;\n }\n\n // Tooltip element + cursor tracking (mouse only — touch selects directly and\n // reviews seats in the host tray). Positioned at the cursor, flipped at edges.\n // Appended AFTER controller.render — mounting the canvas replaces the host's\n // prior children, so anything added earlier would be wiped.\n if (this.opts.seatTooltip !== false) {\n const tip = document.createElement('div');\n tip.setAttribute('role', 'tooltip');\n tip.style.cssText =\n 'position:absolute;z-index:7;pointer-events:none;display:none;max-width:240px;' +\n 'background:#10162a;color:#fff;border-radius:10px;padding:9px 12px;' +\n 'font:500 12px/1.45 -apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;' +\n 'box-shadow:0 10px 30px -10px rgba(0,0,0,.5);';\n host.appendChild(tip);\n this.tipEl = tip;\n this.onTipMove = (e: MouseEvent) => {\n const r = host.getBoundingClientRect();\n this.tipPos = { x: e.clientX - r.left, y: e.clientY - r.top };\n if (this.tipEl && this.tipEl.style.display !== 'none') this.placeTooltip();\n };\n host.addEventListener('mousemove', this.onTipMove);\n }\n if (info.mode === 'test') {\n host.style.overflow = 'hidden';\n const ribbon = document.createElement('div');\n ribbon.textContent = t('picker.testMode');\n ribbon.setAttribute('aria-label', t('picker.testMode'));\n ribbon.style.cssText =\n 'position:absolute;top:18px;right:-34px;z-index:6;transform:rotate(45deg);' +\n 'width:140px;text-align:center;padding:4px 0;background:#f4b740;color:#1a1200;' +\n 'font:800 10.5px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;letter-spacing:.12em;' +\n 'box-shadow:0 2px 8px rgba(0,0,0,.25);pointer-events:none;';\n host.appendChild(ribbon);\n }\n return this;\n }\n\n private placeTooltip(): void {\n if (!this.tipEl || !this.hostEl) return;\n const hw = this.hostEl.clientWidth;\n const tw = this.tipEl.offsetWidth;\n const th = this.tipEl.offsetHeight;\n let x = this.tipPos.x + 14;\n let y = this.tipPos.y - th - 12;\n if (x + tw > hw - 8) x = this.tipPos.x - tw - 14;\n if (y < 8) y = this.tipPos.y + 18;\n this.tipEl.style.left = `${Math.max(8, x)}px`;\n this.tipEl.style.top = `${Math.max(8, y)}px`;\n }\n\n private updateTooltip(details: SeatHoverDetails | null): void {\n if (!this.tipEl) return;\n if (!details) {\n this.tipEl.style.display = 'none';\n return;\n }\n const money = (() => {\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency: details.currency }).format(details.price);\n } catch {\n return `${details.price} ${details.currency}`;\n }\n })();\n const statusLine =\n details.status === 'free'\n ? ''\n : `<div style=\"margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:#fca5a5;font-weight:700\">${\n details.status === 'held' ? t('map.statusHeld') : t('map.statusTaken')\n }</div>`;\n this.tipEl.innerHTML =\n `<div style=\"font-weight:700;font-size:13px\">${details.label}</div>` +\n `<div style=\"display:flex;align-items:center;gap:6px;margin-top:4px;color:#c7cddc\">` +\n `<span style=\"width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}\"></span>` +\n `<span>${details.categoryLabel}</span>` +\n `<span style=\"margin-left:auto;font-weight:700;color:#fff\">${money}</span></div>` +\n statusLine;\n this.tipEl.style.display = 'block';\n this.placeTooltip();\n }\n\n /** Current selection with prices resolved from the chart categories. */\n getSelection(): SelectedSeat[] {\n return this.controller.getSelection();\n }\n\n /** Hold the current selection. Resolves the hold, or null on a 409 conflict. */\n async hold(options: { ttlMs?: number } = {}): Promise<HoldResult | null> {\n try {\n const h = await this.controller.hold(undefined, options.ttlMs);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n getGAAreas(): GAAreaAvailability[] {\n return this.controller.getGAAreas();\n }\n\n async holdGA(\n areaId: string,\n qty: number,\n options: { tierId?: string | null; ttlMs?: number } = {},\n ): Promise<HoldResult | null> {\n try {\n const h = await this.controller.holdGA(areaId, qty, options);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n /** Ask the server for the `qty` best free seats and hold them atomically. */\n async bestAvailable(qty: number, categoryKey?: string): Promise<BestAvailableResult | null> {\n try {\n const h = await this.controller.bestAvailable(qty, categoryKey);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, labels: h.labels, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n /**\n * Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's\n * available `tiers` are on each `SelectedSeat` from `getSelection()` /\n * `onSelectionChange`. Re-emits the selection with the new tier + price, and\n * the tier rides along in the next `hold()` / `onHold` per seat. `tierId=null`\n * reverts to the default tier.\n */\n setSeatTier(seatId: string, tierId: string | null): void {\n this.controller.setSeatTier(seatId, tierId);\n }\n\n /**\n * Floors of a multi-floor chart — `[{ id, name }]` (single-floor charts\n * return one entry; empty before render()). Pair with setFloor() to build a\n * host-side floor switcher.\n */\n getFloors(): { id: string; name: string }[] {\n return this.controller.getFloors();\n }\n\n /** Switch the shown floor (2D). Warns + no-ops on single-floor charts. */\n setFloor(floorId: string): void {\n if (this.controller.getFloors().length <= 1) {\n console.warn('seatmap: setFloor() ignored — this chart has a single floor');\n return;\n }\n this.controller.setFloor(floorId);\n }\n\n /** Toggle colorblind-safe rendering at runtime (see options.colorblindSafe). */\n setColorblindSafe(on: boolean): void {\n this.controller.setColorblindSafe(on);\n }\n\n /** Release the current hold (if any). No-op when nothing is held. */\n async release(): Promise<void> {\n await this.controller.release();\n }\n\n /** Tear everything down: close the socket, stop timers, drop the canvas. */\n destroy(): void {\n if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener('mousemove', this.onTipMove);\n this.tipEl = null;\n this.onTipMove = null;\n this.controller.destroy();\n if (this.hostEl && this.hostEl.parentNode) this.hostEl.parentNode.removeChild(this.hostEl);\n this.hostEl = null;\n this.mount = null;\n this.rendered = false;\n }\n}\n","/**\n * Minimal client for the public embed surface of workers/api (the `/pub/*`\n * routes). Deliberately self-contained — it does NOT reuse src/lib/api.ts,\n * which bakes in a build-time API base and dashboard session credentials. The\n * SDK runs cross-origin on a third-party ticketing page, so:\n * - apiBase is per-instance (constructor option), not a build constant;\n * - credentials are omitted (no cookie to send, avoids CORS-credential setup);\n * - no custom headers on mutating calls (keeps the CORS preflight trivial).\n */\nimport type { ChartDoc, PickerSeat as SelectedSeat } from '@seatlayer/core';\n\nexport interface HoldConflict {\n label: string;\n status: string;\n}\n\nexport interface HoldLineItem {\n label: string; objectId: string; objectType: 'seat' | 'booth' | 'ga'; categoryKey: string;\n tierId: string | null;\n /** Price in major currency units (for example 45 means $45.00). */\n unitPrice: number;\n currency: string;\n quantity?: number;\n}\n\nexport class ApiError extends Error {\n status: number;\n code?: string;\n /** Present when a hold 409s because seats were just taken/held. */\n conflicts?: HoldConflict[];\n /** Present when best-available 409s ('not_enough_together' | 'sold_out'). */\n reason?: string;\n\n constructor(status: number, message: string, code?: string, conflicts?: HoldConflict[], reason?: string) {\n super(message);\n this.name = 'ApiError';\n this.status = status;\n this.code = code;\n this.conflicts = conflicts;\n this.reason = reason;\n }\n}\n\nexport interface PubChartResult {\n event: { key: string; name: string };\n doc: ChartDoc;\n}\n\nexport interface PubObjectsResult {\n /** Every non-free seat's status, keyed by seat label. */\n seats: Record<string, string>;\n updatedAt: number;\n}\n\nexport interface HoldResult {\n holdId: string;\n expiresAt: number;\n /** The held seats with the buyer's chosen ticket tier per seat (present on hold). */\n seats?: SelectedSeat[];\n items?: HoldLineItem[];\n}\n\n/** Best-available response — the server-picked seats plus the hold they landed in. */\nexport interface BestAvailableResult {\n holdId: string;\n expiresAt: number;\n labels: string[];\n seats?: SelectedSeat[];\n items?: HoldResult['items'];\n}\n\nasync function request<T>(\n base: string,\n path: string,\n init: { method?: 'GET' | 'POST'; body?: unknown } = {},\n): Promise<T> {\n const method = init.method ?? 'GET';\n const headers: Record<string, string> = {};\n let body: string | undefined;\n if (init.body !== undefined) {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(init.body);\n }\n\n const res = await fetch(`${base}${path}`, { method, headers, body, credentials: 'omit' });\n\n const isJson = (res.headers.get('content-type') ?? '').includes('application/json');\n const data = isJson ? await res.json().catch(() => null) : null;\n\n if (!res.ok) {\n const err = data as\n | { error?: string; code?: string; conflicts?: HoldConflict[]; reason?: string }\n | null;\n throw new ApiError(res.status, err?.error ?? `request_failed_${res.status}`, err?.code, err?.conflicts, err?.reason);\n }\n return data as T;\n}\n\n/** Public-surface client bound to one apiBase (e.g. https://api.seatlayer.io). */\nexport class PubApi {\n constructor(private readonly base: string) {}\n\n chart(key: string): Promise<PubChartResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/chart`);\n }\n\n objects(key: string): Promise<PubObjectsResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/objects`);\n }\n\n hold(key: string, selections: Array<{ label: string; tierId?: string | null }>, ttlMs?: number, replaceHoldId?: string): Promise<HoldResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold`, {\n method: 'POST',\n body: { selections, ...(ttlMs ? { ttlMs } : {}), ...(replaceHoldId ? { replaceHoldId } : {}) },\n });\n }\n\n bestAvailable(key: string, qty: number, categoryKey?: string): Promise<BestAvailableResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/best-available`, {\n method: 'POST',\n body: { qty, ...(categoryKey ? { categoryKey } : {}) },\n });\n }\n\n release(key: string, labels: string[], holdId: string): Promise<{ ok: true }> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {\n method: 'POST',\n body: { labels, holdId },\n });\n }\n\n socketUrl(key: string): string {\n const wsBase = this.base.replace(/^http/, 'ws');\n return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSA,kBAA4G;;;ACgBrG,IAAM,WAAN,cAAuB,MAAM;AAAA,EAQlC,YAAY,QAAgB,SAAiB,MAAe,WAA4B,QAAiB;AACvG,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AACZ,SAAK,YAAY;AACjB,SAAK,SAAS;AAAA,EAChB;AACF;AA8BA,eAAe,QACb,MACA,MACA,OAAoD,CAAC,GACzC;AACZ,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,UAAkC,CAAC;AACzC,MAAI;AACJ,MAAI,KAAK,SAAS,QAAW;AAC3B,YAAQ,cAAc,IAAI;AAC1B,WAAO,KAAK,UAAU,KAAK,IAAI;AAAA,EACjC;AAEA,QAAM,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,IAAI,EAAE,QAAQ,SAAS,MAAM,aAAa,OAAO,CAAC;AAExF,QAAM,UAAU,IAAI,QAAQ,IAAI,cAAc,KAAK,IAAI,SAAS,kBAAkB;AAClF,QAAM,OAAO,SAAS,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,IAAI,IAAI;AAE3D,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,MAAM;AAGZ,UAAM,IAAI,SAAS,IAAI,QAAQ,KAAK,SAAS,kBAAkB,IAAI,MAAM,IAAI,KAAK,MAAM,KAAK,WAAW,KAAK,MAAM;AAAA,EACrH;AACA,SAAO;AACT;AAGO,IAAM,SAAN,MAAa;AAAA,EAClB,YAA6B,MAAc;AAAd;AAAA,EAAe;AAAA,EAE5C,MAAM,KAAsC;AAC1C,WAAO,QAAQ,KAAK,MAAM,eAAe,mBAAmB,GAAG,CAAC,QAAQ;AAAA,EAC1E;AAAA,EAEA,QAAQ,KAAwC;AAC9C,WAAO,QAAQ,KAAK,MAAM,eAAe,mBAAmB,GAAG,CAAC,UAAU;AAAA,EAC5E;AAAA,EAEA,KAAK,KAAa,YAA8D,OAAgB,eAA6C;AAC3I,WAAO,QAAQ,KAAK,MAAM,eAAe,mBAAmB,GAAG,CAAC,SAAS;AAAA,MACvE,QAAQ;AAAA,MACR,MAAM,EAAE,YAAY,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC,GAAI,GAAI,gBAAgB,EAAE,cAAc,IAAI,CAAC,EAAG;AAAA,IAC/F,CAAC;AAAA,EACH;AAAA,EAEA,cAAc,KAAa,KAAa,aAAoD;AAC1F,WAAO,QAAQ,KAAK,MAAM,eAAe,mBAAmB,GAAG,CAAC,mBAAmB;AAAA,MACjF,QAAQ;AAAA,MACR,MAAM,EAAE,KAAK,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC,EAAG;AAAA,IACvD,CAAC;AAAA,EACH;AAAA,EAEA,QAAQ,KAAa,QAAkB,QAAuC;AAC5E,WAAO,QAAQ,KAAK,MAAM,eAAe,mBAAmB,GAAG,CAAC,YAAY;AAAA,MAC1E,QAAQ;AAAA,MACR,MAAM,EAAE,QAAQ,OAAO;AAAA,IACzB,CAAC;AAAA,EACH;AAAA,EAEA,UAAU,KAAqB;AAC7B,UAAM,SAAS,KAAK,KAAK,QAAQ,SAAS,IAAI;AAC9C,WAAO,GAAG,MAAM,eAAe,mBAAmB,GAAG,CAAC;AAAA,EACxD;AACF;;;AD3HA,IAAM,mBAAmB;AACzB,IAAM,wBAAwB;AAsE9B,SAAS,iBAAiB,WAA8C;AACtE,MAAI,OAAO,cAAc,UAAU;AACjC,UAAM,KAAK,SAAS,cAAc,SAAS;AAC3C,QAAI,CAAC,GAAI,OAAM,IAAI,MAAM,uBAAuB,SAAS,aAAa;AACtE,WAAO;AAAA,EACT;AACA,MAAI,EAAE,qBAAqB,cAAc;AACvC,UAAM,IAAI,MAAM,6DAA6D;AAAA,EAC/E;AACA,SAAO;AACT;AAEO,IAAM,eAAN,MAAmB;AAAA,EAaxB,YAAY,SAA8B;AAP1C,SAAQ,QAA4B;AACpC,SAAQ,SAAgC;AACxC,SAAQ,WAAW;AACnB,SAAQ,QAA+B;AACvC,SAAQ,SAAS,EAAE,GAAG,GAAG,GAAG,EAAE;AAC9B,SAAQ,YAA8C;AAGpD,QAAI,CAAC,WAAW,OAAO,YAAY,SAAU,OAAM,IAAI,MAAM,qCAAqC;AAClG,QAAI,CAAC,QAAQ,UAAW,OAAM,IAAI,MAAM,kCAAkC;AAC1E,QAAI,CAAC,QAAQ,SAAS,OAAO,QAAQ,UAAU,SAAU,OAAM,IAAI,MAAM,kCAAkC;AAE3G,SAAK,OAAO;AACZ,SAAK,YAAY,QAAQ;AACzB,UAAM,MAAM,IAAI,QAAQ,QAAQ,WAAW,kBAAkB,QAAQ,QAAQ,EAAE,CAAC;AAChF,SAAK,aAAa,IAAI,6BAAiB;AAAA,MACrC,WAAW;AAAA,MACX,UAAU,QAAQ;AAAA,MAClB,cAAc,QAAQ,gBAAgB;AAAA,MACtC,UAAU,QAAQ;AAAA,MAClB,mBAAmB,CAAC,UAAU,KAAK,KAAK,oBAAoB,KAAK;AAAA,MACjE,QAAQ,CAAC,MAAM,KAAK,KAAK,SAAS,EAAE,QAAQ,EAAE,QAAQ,WAAW,EAAE,WAAW,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,CAAC;AAAA,MAC9G,eAAe,MAAM,KAAK,KAAK,gBAAgB;AAAA,MAC/C,WAAW,CAAC,WAAW;AACrB,cAAM,OAAO,KAAK,WAAW,WAAW,EAAE,KAAK,CAAC,cAAc,UAAU,OAAO,MAAM;AACrF,YAAI,KAAM,MAAK,KAAK,YAAY,IAAI;AAAA,MACtC;AAAA,MACA,SAAS,CAAC,QAAQ,KAAK,KAAK,UAAU,GAAG;AAAA,MACzC,WAAW,CAAC,YAAY,KAAK,KAAK,YAAY,OAAO;AAAA,MACrD,QAAQ,CAAC,YAAY,KAAK,KAAK,SAAS,OAAO;AAAA;AAAA;AAAA,MAG/C,mBAAmB;AAAA,MACnB,aAAa,CAAC,YAAY;AACxB,aAAK,KAAK,cAAc,OAAO;AAC/B,YAAI,KAAK,KAAK,gBAAgB,MAAO,MAAK,cAAc,OAAO;AAAA,MACjE;AAAA,MACA,gBAAgB,QAAQ;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,SAAwB;AAC5B,QAAI,KAAK,SAAU,QAAO;AAC1B,SAAK,WAAW;AAKhB,cAAM,wBAAW,KAAK,KAAK,MAAM;AACjC,QAAI,KAAK,KAAK,SAAU,qCAAmB,KAAK,KAAK,QAAQ;AAI7D,SAAK,QAAQ,iBAAiB,KAAK,KAAK,SAAS;AACjD,UAAM,OAAO,SAAS,cAAc,KAAK;AACzC,SAAK,MAAM,QAAQ;AACnB,SAAK,MAAM,SAAS;AACpB,SAAK,MAAM,WAAW;AACtB,SAAK,MAAM,YAAY,IAAI;AAC3B,SAAK,SAAS;AAEd,UAAM,OAAO,MAAM,KAAK,WAAW,OAAO,IAAI;AAC9C,QAAI,CAAC,MAAM;AACT,WAAK,WAAW;AAChB,aAAO;AAAA,IACT;AAMA,QAAI,KAAK,KAAK,gBAAgB,OAAO;AACnC,YAAM,MAAM,SAAS,cAAc,KAAK;AACxC,UAAI,aAAa,QAAQ,SAAS;AAClC,UAAI,MAAM,UACR;AAIF,WAAK,YAAY,GAAG;AACpB,WAAK,QAAQ;AACb,WAAK,YAAY,CAAC,MAAkB;AAClC,cAAM,IAAI,KAAK,sBAAsB;AACrC,aAAK,SAAS,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,EAAE,UAAU,EAAE,IAAI;AAC5D,YAAI,KAAK,SAAS,KAAK,MAAM,MAAM,YAAY,OAAQ,MAAK,aAAa;AAAA,MAC3E;AACA,WAAK,iBAAiB,aAAa,KAAK,SAAS;AAAA,IACnD;AACA,QAAI,KAAK,SAAS,QAAQ;AACxB,WAAK,MAAM,WAAW;AACtB,YAAM,SAAS,SAAS,cAAc,KAAK;AAC3C,aAAO,kBAAc,eAAE,iBAAiB;AACxC,aAAO,aAAa,kBAAc,eAAE,iBAAiB,CAAC;AACtD,aAAO,MAAM,UACX;AAIF,WAAK,YAAY,MAAM;AAAA,IACzB;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,eAAqB;AAC3B,QAAI,CAAC,KAAK,SAAS,CAAC,KAAK,OAAQ;AACjC,UAAM,KAAK,KAAK,OAAO;AACvB,UAAM,KAAK,KAAK,MAAM;AACtB,UAAM,KAAK,KAAK,MAAM;AACtB,QAAI,IAAI,KAAK,OAAO,IAAI;AACxB,QAAI,IAAI,KAAK,OAAO,IAAI,KAAK;AAC7B,QAAI,IAAI,KAAK,KAAK,EAAG,KAAI,KAAK,OAAO,IAAI,KAAK;AAC9C,QAAI,IAAI,EAAG,KAAI,KAAK,OAAO,IAAI;AAC/B,SAAK,MAAM,MAAM,OAAO,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;AACzC,SAAK,MAAM,MAAM,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;AAAA,EAC1C;AAAA,EAEQ,cAAc,SAAwC;AAC5D,QAAI,CAAC,KAAK,MAAO;AACjB,QAAI,CAAC,SAAS;AACZ,WAAK,MAAM,MAAM,UAAU;AAC3B;AAAA,IACF;AACA,UAAM,SAAS,MAAM;AACnB,UAAI;AACF,eAAO,IAAI,KAAK,aAAa,QAAW,EAAE,OAAO,YAAY,UAAU,QAAQ,SAAS,CAAC,EAAE,OAAO,QAAQ,KAAK;AAAA,MACjH,QAAQ;AACN,eAAO,GAAG,QAAQ,KAAK,IAAI,QAAQ,QAAQ;AAAA,MAC7C;AAAA,IACF,GAAG;AACH,UAAM,aACJ,QAAQ,WAAW,SACf,KACA,4HACE,QAAQ,WAAW,aAAS,eAAE,gBAAgB,QAAI,eAAE,iBAAiB,CACvE;AACN,SAAK,MAAM,YACT,+CAA+C,QAAQ,KAAK,oKAEgB,QAAQ,aAAa,kBACxF,QAAQ,aAAa,oEAC+B,KAAK,kBAClE;AACF,SAAK,MAAM,MAAM,UAAU;AAC3B,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA,EAGA,eAA+B;AAC7B,WAAO,KAAK,WAAW,aAAa;AAAA,EACtC;AAAA;AAAA,EAGA,MAAM,KAAK,UAA8B,CAAC,GAA+B;AACvE,QAAI;AACF,YAAM,IAAI,MAAM,KAAK,WAAW,KAAK,QAAW,QAAQ,KAAK;AAC7D,aAAO,IAAI,EAAE,QAAQ,EAAE,QAAQ,WAAW,EAAE,WAAW,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,IAAI;AAAA,IAC5F,SAAS,KAAK;AACZ,WAAK,KAAK,UAAU,GAAG;AACvB,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,aAAmC;AACjC,WAAO,KAAK,WAAW,WAAW;AAAA,EACpC;AAAA,EAEA,MAAM,OACJ,QACA,KACA,UAAsD,CAAC,GAC3B;AAC5B,QAAI;AACF,YAAM,IAAI,MAAM,KAAK,WAAW,OAAO,QAAQ,KAAK,OAAO;AAC3D,aAAO,IAAI,EAAE,QAAQ,EAAE,QAAQ,WAAW,EAAE,WAAW,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,IAAI;AAAA,IAC5F,SAAS,KAAK;AACZ,WAAK,KAAK,UAAU,GAAG;AACvB,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,cAAc,KAAa,aAA2D;AAC1F,QAAI;AACF,YAAM,IAAI,MAAM,KAAK,WAAW,cAAc,KAAK,WAAW;AAC9D,aAAO,IAAI,EAAE,QAAQ,EAAE,QAAQ,WAAW,EAAE,WAAW,QAAQ,EAAE,QAAQ,OAAO,EAAE,OAAO,OAAO,EAAE,MAAM,IAAI;AAAA,IAC9G,SAAS,KAAK;AACZ,WAAK,KAAK,UAAU,GAAG;AACvB,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAY,QAAgB,QAA6B;AACvD,SAAK,WAAW,YAAY,QAAQ,MAAM;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAA4C;AAC1C,WAAO,KAAK,WAAW,UAAU;AAAA,EACnC;AAAA;AAAA,EAGA,SAAS,SAAuB;AAC9B,QAAI,KAAK,WAAW,UAAU,EAAE,UAAU,GAAG;AAC3C,cAAQ,KAAK,kEAA6D;AAC1E;AAAA,IACF;AACA,SAAK,WAAW,SAAS,OAAO;AAAA,EAClC;AAAA;AAAA,EAGA,kBAAkB,IAAmB;AACnC,SAAK,WAAW,kBAAkB,EAAE;AAAA,EACtC;AAAA;AAAA,EAGA,MAAM,UAAyB;AAC7B,UAAM,KAAK,WAAW,QAAQ;AAAA,EAChC;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,UAAU,KAAK,UAAW,MAAK,OAAO,oBAAoB,aAAa,KAAK,SAAS;AAC9F,SAAK,QAAQ;AACb,SAAK,YAAY;AACjB,SAAK,WAAW,QAAQ;AACxB,QAAI,KAAK,UAAU,KAAK,OAAO,WAAY,MAAK,OAAO,WAAW,YAAY,KAAK,MAAM;AACzF,SAAK,SAAS;AACd,SAAK,QAAQ;AACb,SAAK,WAAW;AAAA,EAClB;AACF;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { PickerSeat } from '@seatlayer/core';
|
|
1
|
+
import { PickerSeat, SeatHoverDetails } from '@seatlayer/core';
|
|
2
|
+
export { SeatHoverDetails } from '@seatlayer/core';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Minimal client for the public embed surface of workers/api (the `/pub/*`
|
|
@@ -106,6 +107,18 @@ interface SeatingChartOptions {
|
|
|
106
107
|
* Toggleable later with setColorblindSafe().
|
|
107
108
|
*/
|
|
108
109
|
colorblindSafe?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Built-in seat tooltip on mouse hover (seat · category · price · status).
|
|
112
|
+
* Rendered inside the widget so every host gets it; default true. Turn off
|
|
113
|
+
* to draw your own popover from onSeatHover.
|
|
114
|
+
*/
|
|
115
|
+
seatTooltip?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Seat hover with everything a popover needs (category label/color, resolved
|
|
118
|
+
* tier-aware price, live status, currency); null on hover-out. Fires whether
|
|
119
|
+
* or not the built-in tooltip is enabled.
|
|
120
|
+
*/
|
|
121
|
+
onSeatHover?: (details: SeatHoverDetails | null) => void;
|
|
109
122
|
onSelectionChange?: (seats: SelectedSeat[]) => void;
|
|
110
123
|
onHold?: (result: HoldResult) => void;
|
|
111
124
|
onHoldExpired?: () => void;
|
|
@@ -132,9 +145,14 @@ declare class SeatingChart {
|
|
|
132
145
|
private mount;
|
|
133
146
|
private hostEl;
|
|
134
147
|
private rendered;
|
|
148
|
+
private tipEl;
|
|
149
|
+
private tipPos;
|
|
150
|
+
private onTipMove;
|
|
135
151
|
constructor(options: SeatingChartOptions);
|
|
136
152
|
/** Fetch the chart, mount the renderer, seed statuses and go live. Idempotent. */
|
|
137
153
|
render(): Promise<this>;
|
|
154
|
+
private placeTooltip;
|
|
155
|
+
private updateTooltip;
|
|
138
156
|
/** Current selection with prices resolved from the chart categories. */
|
|
139
157
|
getSelection(): SelectedSeat[];
|
|
140
158
|
/** Hold the current selection. Resolves the hold, or null on a 409 conflict. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { PickerSeat } from '@seatlayer/core';
|
|
1
|
+
import { PickerSeat, SeatHoverDetails } from '@seatlayer/core';
|
|
2
|
+
export { SeatHoverDetails } from '@seatlayer/core';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Minimal client for the public embed surface of workers/api (the `/pub/*`
|
|
@@ -106,6 +107,18 @@ interface SeatingChartOptions {
|
|
|
106
107
|
* Toggleable later with setColorblindSafe().
|
|
107
108
|
*/
|
|
108
109
|
colorblindSafe?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Built-in seat tooltip on mouse hover (seat · category · price · status).
|
|
112
|
+
* Rendered inside the widget so every host gets it; default true. Turn off
|
|
113
|
+
* to draw your own popover from onSeatHover.
|
|
114
|
+
*/
|
|
115
|
+
seatTooltip?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Seat hover with everything a popover needs (category label/color, resolved
|
|
118
|
+
* tier-aware price, live status, currency); null on hover-out. Fires whether
|
|
119
|
+
* or not the built-in tooltip is enabled.
|
|
120
|
+
*/
|
|
121
|
+
onSeatHover?: (details: SeatHoverDetails | null) => void;
|
|
109
122
|
onSelectionChange?: (seats: SelectedSeat[]) => void;
|
|
110
123
|
onHold?: (result: HoldResult) => void;
|
|
111
124
|
onHoldExpired?: () => void;
|
|
@@ -132,9 +145,14 @@ declare class SeatingChart {
|
|
|
132
145
|
private mount;
|
|
133
146
|
private hostEl;
|
|
134
147
|
private rendered;
|
|
148
|
+
private tipEl;
|
|
149
|
+
private tipPos;
|
|
150
|
+
private onTipMove;
|
|
135
151
|
constructor(options: SeatingChartOptions);
|
|
136
152
|
/** Fetch the chart, mount the renderer, seed statuses and go live. Idempotent. */
|
|
137
153
|
render(): Promise<this>;
|
|
154
|
+
private placeTooltip;
|
|
155
|
+
private updateTooltip;
|
|
138
156
|
/** Current selection with prices resolved from the chart categories. */
|
|
139
157
|
getSelection(): SelectedSeat[];
|
|
140
158
|
/** Hold the current selection. Resolves the hold, or null on a 409 conflict. */
|
package/dist/index.js
CHANGED
|
@@ -82,6 +82,9 @@ var SeatingChart = class {
|
|
|
82
82
|
this.mount = null;
|
|
83
83
|
this.hostEl = null;
|
|
84
84
|
this.rendered = false;
|
|
85
|
+
this.tipEl = null;
|
|
86
|
+
this.tipPos = { x: 0, y: 0 };
|
|
87
|
+
this.onTipMove = null;
|
|
85
88
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
86
89
|
if (!options.container) throw new Error("seatmap: `container` is required");
|
|
87
90
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
@@ -103,6 +106,13 @@ var SeatingChart = class {
|
|
|
103
106
|
onError: (err) => this.opts.onError?.(err),
|
|
104
107
|
onDeckTap: (floorId) => this.opts.onDeckTap?.(floorId),
|
|
105
108
|
onHint: (message) => this.opts.onHint?.(message),
|
|
109
|
+
// Live-activity cue: pulse seats that other buyers take while the map is
|
|
110
|
+
// open — the WS feed already streams the status change, this makes it felt.
|
|
111
|
+
flashOnLiveChange: true,
|
|
112
|
+
onSeatHover: (details) => {
|
|
113
|
+
this.opts.onSeatHover?.(details);
|
|
114
|
+
if (this.opts.seatTooltip !== false) this.updateTooltip(details);
|
|
115
|
+
},
|
|
106
116
|
colorblindSafe: options.colorblindSafe
|
|
107
117
|
});
|
|
108
118
|
}
|
|
@@ -124,6 +134,19 @@ var SeatingChart = class {
|
|
|
124
134
|
this.rendered = false;
|
|
125
135
|
return this;
|
|
126
136
|
}
|
|
137
|
+
if (this.opts.seatTooltip !== false) {
|
|
138
|
+
const tip = document.createElement("div");
|
|
139
|
+
tip.setAttribute("role", "tooltip");
|
|
140
|
+
tip.style.cssText = 'position:absolute;z-index:7;pointer-events:none;display:none;max-width:240px;background:#10162a;color:#fff;border-radius:10px;padding:9px 12px;font:500 12px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;box-shadow:0 10px 30px -10px rgba(0,0,0,.5);';
|
|
141
|
+
host.appendChild(tip);
|
|
142
|
+
this.tipEl = tip;
|
|
143
|
+
this.onTipMove = (e) => {
|
|
144
|
+
const r = host.getBoundingClientRect();
|
|
145
|
+
this.tipPos = { x: e.clientX - r.left, y: e.clientY - r.top };
|
|
146
|
+
if (this.tipEl && this.tipEl.style.display !== "none") this.placeTooltip();
|
|
147
|
+
};
|
|
148
|
+
host.addEventListener("mousemove", this.onTipMove);
|
|
149
|
+
}
|
|
127
150
|
if (info.mode === "test") {
|
|
128
151
|
host.style.overflow = "hidden";
|
|
129
152
|
const ribbon = document.createElement("div");
|
|
@@ -134,6 +157,36 @@ var SeatingChart = class {
|
|
|
134
157
|
}
|
|
135
158
|
return this;
|
|
136
159
|
}
|
|
160
|
+
placeTooltip() {
|
|
161
|
+
if (!this.tipEl || !this.hostEl) return;
|
|
162
|
+
const hw = this.hostEl.clientWidth;
|
|
163
|
+
const tw = this.tipEl.offsetWidth;
|
|
164
|
+
const th = this.tipEl.offsetHeight;
|
|
165
|
+
let x = this.tipPos.x + 14;
|
|
166
|
+
let y = this.tipPos.y - th - 12;
|
|
167
|
+
if (x + tw > hw - 8) x = this.tipPos.x - tw - 14;
|
|
168
|
+
if (y < 8) y = this.tipPos.y + 18;
|
|
169
|
+
this.tipEl.style.left = `${Math.max(8, x)}px`;
|
|
170
|
+
this.tipEl.style.top = `${Math.max(8, y)}px`;
|
|
171
|
+
}
|
|
172
|
+
updateTooltip(details) {
|
|
173
|
+
if (!this.tipEl) return;
|
|
174
|
+
if (!details) {
|
|
175
|
+
this.tipEl.style.display = "none";
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const money = (() => {
|
|
179
|
+
try {
|
|
180
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency: details.currency }).format(details.price);
|
|
181
|
+
} catch {
|
|
182
|
+
return `${details.price} ${details.currency}`;
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
const statusLine = details.status === "free" ? "" : `<div style="margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:#fca5a5;font-weight:700">${details.status === "held" ? t("map.statusHeld") : t("map.statusTaken")}</div>`;
|
|
186
|
+
this.tipEl.innerHTML = `<div style="font-weight:700;font-size:13px">${details.label}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px;color:#c7cddc"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span>${details.categoryLabel}</span><span style="margin-left:auto;font-weight:700;color:#fff">${money}</span></div>` + statusLine;
|
|
187
|
+
this.tipEl.style.display = "block";
|
|
188
|
+
this.placeTooltip();
|
|
189
|
+
}
|
|
137
190
|
/** Current selection with prices resolved from the chart categories. */
|
|
138
191
|
getSelection() {
|
|
139
192
|
return this.controller.getSelection();
|
|
@@ -206,6 +259,9 @@ var SeatingChart = class {
|
|
|
206
259
|
}
|
|
207
260
|
/** Tear everything down: close the socket, stop timers, drop the canvas. */
|
|
208
261
|
destroy() {
|
|
262
|
+
if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
|
|
263
|
+
this.tipEl = null;
|
|
264
|
+
this.onTipMove = null;
|
|
209
265
|
this.controller.destroy();
|
|
210
266
|
if (this.hostEl && this.hostEl.parentNode) this.hostEl.parentNode.removeChild(this.hostEl);
|
|
211
267
|
this.hostEl = null;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/SeatingChart.ts","../src/api.ts"],"sourcesContent":["/**\n * SeatingChart — the embeddable buyer picker.\n *\n * A thin wrapper over the shared PickerController (src/picker/PickerController):\n * it owns the mount <div> + the public embed contract (hold-only — the SDK hands\n * the holdId to the host page for a server-side book) and delegates all transport\n * + booking to the controller, so the SDK inherits every fix made for the live\n * buyer page and the demo picker.\n */\nimport { PickerController, loadLocale, setStringOverrides, t, type PickerSeat } from '@seatlayer/core';\nimport { PubApi, type BestAvailableResult, type HoldResult } from './api';\n\nconst DEFAULT_API_BASE = 'https://api.seatlayer.io';\nconst DEFAULT_MAX_SELECTION = 10;\n\n/** A seat as surfaced to the host page (prices resolved from the chart's categories). */\nexport type SelectedSeat = PickerSeat;\nexport interface GAAreaAvailability {\n id: string; label: string; capacity: number; available: number; categoryKey: string; price: number; currency: string;\n tiers?: Array<{ id: string; name: string; price: number }>;\n}\n\nexport interface SeatingChartOptions {\n /** CSS selector or an HTMLElement to render into. */\n container: string | HTMLElement;\n /** Event key, e.g. `ev_xxx`. */\n event: string;\n /** API origin. Defaults to https://api.seatlayer.io. */\n apiBase?: string;\n /** Reserved for future authenticated rendering — accepted + stored, not yet sent. */\n publicKey?: string;\n /** Max seats selectable at once (default 10). */\n maxSelection?: number;\n /**\n * BCP 47 language for the widget UI — `'de'`, `'es-MX'`, etc. Falls back to\n * the browser language, then English. Built-in: en, es, de, fr. The German\n * bundle (etc.) is fetched on demand so unused languages cost nothing.\n */\n locale?: string;\n /**\n * Per-key string overrides layered over the active locale — white-label copy\n * without shipping a whole bundle, e.g. `{ 'map.fromPrice': 'ab {price}' }`.\n */\n messages?: Record<string, string>;\n /** ISO 4217 currency for on-map prices (default USD). */\n currency?: string;\n /**\n * Colorblind-safe rendering: category hues switch to an Okabe-Ito palette\n * and booked seats render hollow, so state never relies on hue alone.\n * Toggleable later with setColorblindSafe().\n */\n colorblindSafe?: boolean;\n onSelectionChange?: (seats: SelectedSeat[]) => void;\n onHold?: (result: HoldResult) => void;\n onHoldExpired?: () => void;\n onGAClick?: (area: GAAreaAvailability) => void;\n onError?: (err: unknown) => void;\n /**\n * Multi-floor charts only: fires when the buyer taps a deck in the stacked\n * 3D view, after the picker switches to that floor — lets the host page sync\n * its own floor UI (tabs, labels) with the map.\n */\n onDeckTap?: (floorId: string) => void;\n /**\n * Non-blocking, localized selection advice — currently the orphan-seat hint\n * (the selection would strand a single free seat between taken neighbors).\n * `null` clears it. Purely informational; nothing is ever prevented.\n */\n onHint?: (message: string | null) => void;\n}\n\nfunction resolveContainer(container: string | HTMLElement): HTMLElement {\n if (typeof container === 'string') {\n const el = document.querySelector(container);\n if (!el) throw new Error(`seatmap: container \"${container}\" not found`);\n return el as HTMLElement;\n }\n if (!(container instanceof HTMLElement)) {\n throw new Error('seatmap: container must be a CSS selector or an HTMLElement');\n }\n return container;\n}\n\nexport class SeatingChart {\n private readonly opts: SeatingChartOptions;\n private readonly controller: PickerController;\n /** Reserved for future authenticated rendering — stored, not yet sent on any request. */\n readonly publicKey?: string;\n\n private mount: HTMLElement | null = null;\n private hostEl: HTMLDivElement | null = null;\n private rendered = false;\n\n constructor(options: SeatingChartOptions) {\n if (!options || typeof options !== 'object') throw new Error('seatmap: options object is required');\n if (!options.container) throw new Error('seatmap: `container` is required');\n if (!options.event || typeof options.event !== 'string') throw new Error('seatmap: `event` key is required');\n\n this.opts = options;\n this.publicKey = options.publicKey;\n const api = new PubApi((options.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, ''));\n this.controller = new PickerController({\n transport: api,\n eventKey: options.event,\n maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION,\n currency: options.currency,\n onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),\n onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),\n onHoldExpired: () => this.opts.onHoldExpired?.(),\n onGAClick: (areaId) => {\n const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);\n if (area) this.opts.onGAClick?.(area);\n },\n onError: (err) => this.opts.onError?.(err),\n onDeckTap: (floorId) => this.opts.onDeckTap?.(floorId),\n onHint: (message) => this.opts.onHint?.(message),\n colorblindSafe: options.colorblindSafe,\n });\n }\n\n /** Fetch the chart, mount the renderer, seed statuses and go live. Idempotent. */\n async render(): Promise<this> {\n if (this.rendered) return this;\n this.rendered = true;\n\n // Resolve + load the UI language before the first paint so on-map labels\n // (\"N LEFT\", \"FROM …\", the map aria-label) render translated. English and\n // already-loaded locales resolve synchronously; others fetch one small chunk.\n await loadLocale(this.opts.locale);\n if (this.opts.messages) setStringOverrides(this.opts.messages);\n\n // Mount an owned <div> inside the caller's container so we never fight their\n // layout and can cleanly remove it on destroy().\n this.mount = resolveContainer(this.opts.container);\n const host = document.createElement('div');\n host.style.width = '100%';\n host.style.height = '100%';\n host.style.position = 'relative';\n this.mount.appendChild(host);\n this.hostEl = host;\n\n const info = await this.controller.render(host);\n if (!info) {\n this.rendered = false;\n return this;\n }\n if (info.mode === 'test') {\n host.style.overflow = 'hidden';\n const ribbon = document.createElement('div');\n ribbon.textContent = t('picker.testMode');\n ribbon.setAttribute('aria-label', t('picker.testMode'));\n ribbon.style.cssText =\n 'position:absolute;top:18px;right:-34px;z-index:6;transform:rotate(45deg);' +\n 'width:140px;text-align:center;padding:4px 0;background:#f4b740;color:#1a1200;' +\n 'font:800 10.5px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;letter-spacing:.12em;' +\n 'box-shadow:0 2px 8px rgba(0,0,0,.25);pointer-events:none;';\n host.appendChild(ribbon);\n }\n return this;\n }\n\n /** Current selection with prices resolved from the chart categories. */\n getSelection(): SelectedSeat[] {\n return this.controller.getSelection();\n }\n\n /** Hold the current selection. Resolves the hold, or null on a 409 conflict. */\n async hold(options: { ttlMs?: number } = {}): Promise<HoldResult | null> {\n try {\n const h = await this.controller.hold(undefined, options.ttlMs);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n getGAAreas(): GAAreaAvailability[] {\n return this.controller.getGAAreas();\n }\n\n async holdGA(\n areaId: string,\n qty: number,\n options: { tierId?: string | null; ttlMs?: number } = {},\n ): Promise<HoldResult | null> {\n try {\n const h = await this.controller.holdGA(areaId, qty, options);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n /** Ask the server for the `qty` best free seats and hold them atomically. */\n async bestAvailable(qty: number, categoryKey?: string): Promise<BestAvailableResult | null> {\n try {\n const h = await this.controller.bestAvailable(qty, categoryKey);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, labels: h.labels, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n /**\n * Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's\n * available `tiers` are on each `SelectedSeat` from `getSelection()` /\n * `onSelectionChange`. Re-emits the selection with the new tier + price, and\n * the tier rides along in the next `hold()` / `onHold` per seat. `tierId=null`\n * reverts to the default tier.\n */\n setSeatTier(seatId: string, tierId: string | null): void {\n this.controller.setSeatTier(seatId, tierId);\n }\n\n /**\n * Floors of a multi-floor chart — `[{ id, name }]` (single-floor charts\n * return one entry; empty before render()). Pair with setFloor() to build a\n * host-side floor switcher.\n */\n getFloors(): { id: string; name: string }[] {\n return this.controller.getFloors();\n }\n\n /** Switch the shown floor (2D). Warns + no-ops on single-floor charts. */\n setFloor(floorId: string): void {\n if (this.controller.getFloors().length <= 1) {\n console.warn('seatmap: setFloor() ignored — this chart has a single floor');\n return;\n }\n this.controller.setFloor(floorId);\n }\n\n /** Toggle colorblind-safe rendering at runtime (see options.colorblindSafe). */\n setColorblindSafe(on: boolean): void {\n this.controller.setColorblindSafe(on);\n }\n\n /** Release the current hold (if any). No-op when nothing is held. */\n async release(): Promise<void> {\n await this.controller.release();\n }\n\n /** Tear everything down: close the socket, stop timers, drop the canvas. */\n destroy(): void {\n this.controller.destroy();\n if (this.hostEl && this.hostEl.parentNode) this.hostEl.parentNode.removeChild(this.hostEl);\n this.hostEl = null;\n this.mount = null;\n this.rendered = false;\n }\n}\n","/**\n * Minimal client for the public embed surface of workers/api (the `/pub/*`\n * routes). Deliberately self-contained — it does NOT reuse src/lib/api.ts,\n * which bakes in a build-time API base and dashboard session credentials. The\n * SDK runs cross-origin on a third-party ticketing page, so:\n * - apiBase is per-instance (constructor option), not a build constant;\n * - credentials are omitted (no cookie to send, avoids CORS-credential setup);\n * - no custom headers on mutating calls (keeps the CORS preflight trivial).\n */\nimport type { ChartDoc, PickerSeat as SelectedSeat } from '@seatlayer/core';\n\nexport interface HoldConflict {\n label: string;\n status: string;\n}\n\nexport interface HoldLineItem {\n label: string; objectId: string; objectType: 'seat' | 'booth' | 'ga'; categoryKey: string;\n tierId: string | null;\n /** Price in major currency units (for example 45 means $45.00). */\n unitPrice: number;\n currency: string;\n quantity?: number;\n}\n\nexport class ApiError extends Error {\n status: number;\n code?: string;\n /** Present when a hold 409s because seats were just taken/held. */\n conflicts?: HoldConflict[];\n /** Present when best-available 409s ('not_enough_together' | 'sold_out'). */\n reason?: string;\n\n constructor(status: number, message: string, code?: string, conflicts?: HoldConflict[], reason?: string) {\n super(message);\n this.name = 'ApiError';\n this.status = status;\n this.code = code;\n this.conflicts = conflicts;\n this.reason = reason;\n }\n}\n\nexport interface PubChartResult {\n event: { key: string; name: string };\n doc: ChartDoc;\n}\n\nexport interface PubObjectsResult {\n /** Every non-free seat's status, keyed by seat label. */\n seats: Record<string, string>;\n updatedAt: number;\n}\n\nexport interface HoldResult {\n holdId: string;\n expiresAt: number;\n /** The held seats with the buyer's chosen ticket tier per seat (present on hold). */\n seats?: SelectedSeat[];\n items?: HoldLineItem[];\n}\n\n/** Best-available response — the server-picked seats plus the hold they landed in. */\nexport interface BestAvailableResult {\n holdId: string;\n expiresAt: number;\n labels: string[];\n seats?: SelectedSeat[];\n items?: HoldResult['items'];\n}\n\nasync function request<T>(\n base: string,\n path: string,\n init: { method?: 'GET' | 'POST'; body?: unknown } = {},\n): Promise<T> {\n const method = init.method ?? 'GET';\n const headers: Record<string, string> = {};\n let body: string | undefined;\n if (init.body !== undefined) {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(init.body);\n }\n\n const res = await fetch(`${base}${path}`, { method, headers, body, credentials: 'omit' });\n\n const isJson = (res.headers.get('content-type') ?? '').includes('application/json');\n const data = isJson ? await res.json().catch(() => null) : null;\n\n if (!res.ok) {\n const err = data as\n | { error?: string; code?: string; conflicts?: HoldConflict[]; reason?: string }\n | null;\n throw new ApiError(res.status, err?.error ?? `request_failed_${res.status}`, err?.code, err?.conflicts, err?.reason);\n }\n return data as T;\n}\n\n/** Public-surface client bound to one apiBase (e.g. https://api.seatlayer.io). */\nexport class PubApi {\n constructor(private readonly base: string) {}\n\n chart(key: string): Promise<PubChartResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/chart`);\n }\n\n objects(key: string): Promise<PubObjectsResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/objects`);\n }\n\n hold(key: string, selections: Array<{ label: string; tierId?: string | null }>, ttlMs?: number, replaceHoldId?: string): Promise<HoldResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold`, {\n method: 'POST',\n body: { selections, ...(ttlMs ? { ttlMs } : {}), ...(replaceHoldId ? { replaceHoldId } : {}) },\n });\n }\n\n bestAvailable(key: string, qty: number, categoryKey?: string): Promise<BestAvailableResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/best-available`, {\n method: 'POST',\n body: { qty, ...(categoryKey ? { categoryKey } : {}) },\n });\n }\n\n release(key: string, labels: string[], holdId: string): Promise<{ ok: true }> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {\n method: 'POST',\n body: { labels, holdId },\n });\n }\n\n socketUrl(key: string): string {\n const wsBase = this.base.replace(/^http/, 'ws');\n return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/SeatingChart.ts","../src/api.ts"],"sourcesContent":["/**\n * SeatingChart — the embeddable buyer picker.\n *\n * A thin wrapper over the shared PickerController (src/picker/PickerController):\n * it owns the mount <div> + the public embed contract (hold-only — the SDK hands\n * the holdId to the host page for a server-side book) and delegates all transport\n * + booking to the controller, so the SDK inherits every fix made for the live\n * buyer page and the demo picker.\n */\nimport { PickerController, loadLocale, setStringOverrides, t, type PickerSeat, type SeatHoverDetails } from '@seatlayer/core';\nimport { PubApi, type BestAvailableResult, type HoldResult } from './api';\n\nconst DEFAULT_API_BASE = 'https://api.seatlayer.io';\nconst DEFAULT_MAX_SELECTION = 10;\n\n/** A seat as surfaced to the host page (prices resolved from the chart's categories). */\nexport type SelectedSeat = PickerSeat;\nexport interface GAAreaAvailability {\n id: string; label: string; capacity: number; available: number; categoryKey: string; price: number; currency: string;\n tiers?: Array<{ id: string; name: string; price: number }>;\n}\n\nexport interface SeatingChartOptions {\n /** CSS selector or an HTMLElement to render into. */\n container: string | HTMLElement;\n /** Event key, e.g. `ev_xxx`. */\n event: string;\n /** API origin. Defaults to https://api.seatlayer.io. */\n apiBase?: string;\n /** Reserved for future authenticated rendering — accepted + stored, not yet sent. */\n publicKey?: string;\n /** Max seats selectable at once (default 10). */\n maxSelection?: number;\n /**\n * BCP 47 language for the widget UI — `'de'`, `'es-MX'`, etc. Falls back to\n * the browser language, then English. Built-in: en, es, de, fr. The German\n * bundle (etc.) is fetched on demand so unused languages cost nothing.\n */\n locale?: string;\n /**\n * Per-key string overrides layered over the active locale — white-label copy\n * without shipping a whole bundle, e.g. `{ 'map.fromPrice': 'ab {price}' }`.\n */\n messages?: Record<string, string>;\n /** ISO 4217 currency for on-map prices (default USD). */\n currency?: string;\n /**\n * Colorblind-safe rendering: category hues switch to an Okabe-Ito palette\n * and booked seats render hollow, so state never relies on hue alone.\n * Toggleable later with setColorblindSafe().\n */\n colorblindSafe?: boolean;\n /**\n * Built-in seat tooltip on mouse hover (seat · category · price · status).\n * Rendered inside the widget so every host gets it; default true. Turn off\n * to draw your own popover from onSeatHover.\n */\n seatTooltip?: boolean;\n /**\n * Seat hover with everything a popover needs (category label/color, resolved\n * tier-aware price, live status, currency); null on hover-out. Fires whether\n * or not the built-in tooltip is enabled.\n */\n onSeatHover?: (details: SeatHoverDetails | null) => void;\n onSelectionChange?: (seats: SelectedSeat[]) => void;\n onHold?: (result: HoldResult) => void;\n onHoldExpired?: () => void;\n onGAClick?: (area: GAAreaAvailability) => void;\n onError?: (err: unknown) => void;\n /**\n * Multi-floor charts only: fires when the buyer taps a deck in the stacked\n * 3D view, after the picker switches to that floor — lets the host page sync\n * its own floor UI (tabs, labels) with the map.\n */\n onDeckTap?: (floorId: string) => void;\n /**\n * Non-blocking, localized selection advice — currently the orphan-seat hint\n * (the selection would strand a single free seat between taken neighbors).\n * `null` clears it. Purely informational; nothing is ever prevented.\n */\n onHint?: (message: string | null) => void;\n}\n\nfunction resolveContainer(container: string | HTMLElement): HTMLElement {\n if (typeof container === 'string') {\n const el = document.querySelector(container);\n if (!el) throw new Error(`seatmap: container \"${container}\" not found`);\n return el as HTMLElement;\n }\n if (!(container instanceof HTMLElement)) {\n throw new Error('seatmap: container must be a CSS selector or an HTMLElement');\n }\n return container;\n}\n\nexport class SeatingChart {\n private readonly opts: SeatingChartOptions;\n private readonly controller: PickerController;\n /** Reserved for future authenticated rendering — stored, not yet sent on any request. */\n readonly publicKey?: string;\n\n private mount: HTMLElement | null = null;\n private hostEl: HTMLDivElement | null = null;\n private rendered = false;\n private tipEl: HTMLDivElement | null = null;\n private tipPos = { x: 0, y: 0 };\n private onTipMove: ((e: MouseEvent) => void) | null = null;\n\n constructor(options: SeatingChartOptions) {\n if (!options || typeof options !== 'object') throw new Error('seatmap: options object is required');\n if (!options.container) throw new Error('seatmap: `container` is required');\n if (!options.event || typeof options.event !== 'string') throw new Error('seatmap: `event` key is required');\n\n this.opts = options;\n this.publicKey = options.publicKey;\n const api = new PubApi((options.apiBase ?? DEFAULT_API_BASE).replace(/\\/+$/, ''));\n this.controller = new PickerController({\n transport: api,\n eventKey: options.event,\n maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION,\n currency: options.currency,\n onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),\n onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),\n onHoldExpired: () => this.opts.onHoldExpired?.(),\n onGAClick: (areaId) => {\n const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);\n if (area) this.opts.onGAClick?.(area);\n },\n onError: (err) => this.opts.onError?.(err),\n onDeckTap: (floorId) => this.opts.onDeckTap?.(floorId),\n onHint: (message) => this.opts.onHint?.(message),\n // Live-activity cue: pulse seats that other buyers take while the map is\n // open — the WS feed already streams the status change, this makes it felt.\n flashOnLiveChange: true,\n onSeatHover: (details) => {\n this.opts.onSeatHover?.(details);\n if (this.opts.seatTooltip !== false) this.updateTooltip(details);\n },\n colorblindSafe: options.colorblindSafe,\n });\n }\n\n /** Fetch the chart, mount the renderer, seed statuses and go live. Idempotent. */\n async render(): Promise<this> {\n if (this.rendered) return this;\n this.rendered = true;\n\n // Resolve + load the UI language before the first paint so on-map labels\n // (\"N LEFT\", \"FROM …\", the map aria-label) render translated. English and\n // already-loaded locales resolve synchronously; others fetch one small chunk.\n await loadLocale(this.opts.locale);\n if (this.opts.messages) setStringOverrides(this.opts.messages);\n\n // Mount an owned <div> inside the caller's container so we never fight their\n // layout and can cleanly remove it on destroy().\n this.mount = resolveContainer(this.opts.container);\n const host = document.createElement('div');\n host.style.width = '100%';\n host.style.height = '100%';\n host.style.position = 'relative';\n this.mount.appendChild(host);\n this.hostEl = host;\n\n const info = await this.controller.render(host);\n if (!info) {\n this.rendered = false;\n return this;\n }\n\n // Tooltip element + cursor tracking (mouse only — touch selects directly and\n // reviews seats in the host tray). Positioned at the cursor, flipped at edges.\n // Appended AFTER controller.render — mounting the canvas replaces the host's\n // prior children, so anything added earlier would be wiped.\n if (this.opts.seatTooltip !== false) {\n const tip = document.createElement('div');\n tip.setAttribute('role', 'tooltip');\n tip.style.cssText =\n 'position:absolute;z-index:7;pointer-events:none;display:none;max-width:240px;' +\n 'background:#10162a;color:#fff;border-radius:10px;padding:9px 12px;' +\n 'font:500 12px/1.45 -apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;' +\n 'box-shadow:0 10px 30px -10px rgba(0,0,0,.5);';\n host.appendChild(tip);\n this.tipEl = tip;\n this.onTipMove = (e: MouseEvent) => {\n const r = host.getBoundingClientRect();\n this.tipPos = { x: e.clientX - r.left, y: e.clientY - r.top };\n if (this.tipEl && this.tipEl.style.display !== 'none') this.placeTooltip();\n };\n host.addEventListener('mousemove', this.onTipMove);\n }\n if (info.mode === 'test') {\n host.style.overflow = 'hidden';\n const ribbon = document.createElement('div');\n ribbon.textContent = t('picker.testMode');\n ribbon.setAttribute('aria-label', t('picker.testMode'));\n ribbon.style.cssText =\n 'position:absolute;top:18px;right:-34px;z-index:6;transform:rotate(45deg);' +\n 'width:140px;text-align:center;padding:4px 0;background:#f4b740;color:#1a1200;' +\n 'font:800 10.5px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;letter-spacing:.12em;' +\n 'box-shadow:0 2px 8px rgba(0,0,0,.25);pointer-events:none;';\n host.appendChild(ribbon);\n }\n return this;\n }\n\n private placeTooltip(): void {\n if (!this.tipEl || !this.hostEl) return;\n const hw = this.hostEl.clientWidth;\n const tw = this.tipEl.offsetWidth;\n const th = this.tipEl.offsetHeight;\n let x = this.tipPos.x + 14;\n let y = this.tipPos.y - th - 12;\n if (x + tw > hw - 8) x = this.tipPos.x - tw - 14;\n if (y < 8) y = this.tipPos.y + 18;\n this.tipEl.style.left = `${Math.max(8, x)}px`;\n this.tipEl.style.top = `${Math.max(8, y)}px`;\n }\n\n private updateTooltip(details: SeatHoverDetails | null): void {\n if (!this.tipEl) return;\n if (!details) {\n this.tipEl.style.display = 'none';\n return;\n }\n const money = (() => {\n try {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency: details.currency }).format(details.price);\n } catch {\n return `${details.price} ${details.currency}`;\n }\n })();\n const statusLine =\n details.status === 'free'\n ? ''\n : `<div style=\"margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:#fca5a5;font-weight:700\">${\n details.status === 'held' ? t('map.statusHeld') : t('map.statusTaken')\n }</div>`;\n this.tipEl.innerHTML =\n `<div style=\"font-weight:700;font-size:13px\">${details.label}</div>` +\n `<div style=\"display:flex;align-items:center;gap:6px;margin-top:4px;color:#c7cddc\">` +\n `<span style=\"width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}\"></span>` +\n `<span>${details.categoryLabel}</span>` +\n `<span style=\"margin-left:auto;font-weight:700;color:#fff\">${money}</span></div>` +\n statusLine;\n this.tipEl.style.display = 'block';\n this.placeTooltip();\n }\n\n /** Current selection with prices resolved from the chart categories. */\n getSelection(): SelectedSeat[] {\n return this.controller.getSelection();\n }\n\n /** Hold the current selection. Resolves the hold, or null on a 409 conflict. */\n async hold(options: { ttlMs?: number } = {}): Promise<HoldResult | null> {\n try {\n const h = await this.controller.hold(undefined, options.ttlMs);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n getGAAreas(): GAAreaAvailability[] {\n return this.controller.getGAAreas();\n }\n\n async holdGA(\n areaId: string,\n qty: number,\n options: { tierId?: string | null; ttlMs?: number } = {},\n ): Promise<HoldResult | null> {\n try {\n const h = await this.controller.holdGA(areaId, qty, options);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n /** Ask the server for the `qty` best free seats and hold them atomically. */\n async bestAvailable(qty: number, categoryKey?: string): Promise<BestAvailableResult | null> {\n try {\n const h = await this.controller.bestAvailable(qty, categoryKey);\n return h ? { holdId: h.holdId, expiresAt: h.expiresAt, labels: h.labels, seats: h.seats, items: h.items } : null;\n } catch (err) {\n this.opts.onError?.(err);\n return null;\n }\n }\n\n /**\n * Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's\n * available `tiers` are on each `SelectedSeat` from `getSelection()` /\n * `onSelectionChange`. Re-emits the selection with the new tier + price, and\n * the tier rides along in the next `hold()` / `onHold` per seat. `tierId=null`\n * reverts to the default tier.\n */\n setSeatTier(seatId: string, tierId: string | null): void {\n this.controller.setSeatTier(seatId, tierId);\n }\n\n /**\n * Floors of a multi-floor chart — `[{ id, name }]` (single-floor charts\n * return one entry; empty before render()). Pair with setFloor() to build a\n * host-side floor switcher.\n */\n getFloors(): { id: string; name: string }[] {\n return this.controller.getFloors();\n }\n\n /** Switch the shown floor (2D). Warns + no-ops on single-floor charts. */\n setFloor(floorId: string): void {\n if (this.controller.getFloors().length <= 1) {\n console.warn('seatmap: setFloor() ignored — this chart has a single floor');\n return;\n }\n this.controller.setFloor(floorId);\n }\n\n /** Toggle colorblind-safe rendering at runtime (see options.colorblindSafe). */\n setColorblindSafe(on: boolean): void {\n this.controller.setColorblindSafe(on);\n }\n\n /** Release the current hold (if any). No-op when nothing is held. */\n async release(): Promise<void> {\n await this.controller.release();\n }\n\n /** Tear everything down: close the socket, stop timers, drop the canvas. */\n destroy(): void {\n if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener('mousemove', this.onTipMove);\n this.tipEl = null;\n this.onTipMove = null;\n this.controller.destroy();\n if (this.hostEl && this.hostEl.parentNode) this.hostEl.parentNode.removeChild(this.hostEl);\n this.hostEl = null;\n this.mount = null;\n this.rendered = false;\n }\n}\n","/**\n * Minimal client for the public embed surface of workers/api (the `/pub/*`\n * routes). Deliberately self-contained — it does NOT reuse src/lib/api.ts,\n * which bakes in a build-time API base and dashboard session credentials. The\n * SDK runs cross-origin on a third-party ticketing page, so:\n * - apiBase is per-instance (constructor option), not a build constant;\n * - credentials are omitted (no cookie to send, avoids CORS-credential setup);\n * - no custom headers on mutating calls (keeps the CORS preflight trivial).\n */\nimport type { ChartDoc, PickerSeat as SelectedSeat } from '@seatlayer/core';\n\nexport interface HoldConflict {\n label: string;\n status: string;\n}\n\nexport interface HoldLineItem {\n label: string; objectId: string; objectType: 'seat' | 'booth' | 'ga'; categoryKey: string;\n tierId: string | null;\n /** Price in major currency units (for example 45 means $45.00). */\n unitPrice: number;\n currency: string;\n quantity?: number;\n}\n\nexport class ApiError extends Error {\n status: number;\n code?: string;\n /** Present when a hold 409s because seats were just taken/held. */\n conflicts?: HoldConflict[];\n /** Present when best-available 409s ('not_enough_together' | 'sold_out'). */\n reason?: string;\n\n constructor(status: number, message: string, code?: string, conflicts?: HoldConflict[], reason?: string) {\n super(message);\n this.name = 'ApiError';\n this.status = status;\n this.code = code;\n this.conflicts = conflicts;\n this.reason = reason;\n }\n}\n\nexport interface PubChartResult {\n event: { key: string; name: string };\n doc: ChartDoc;\n}\n\nexport interface PubObjectsResult {\n /** Every non-free seat's status, keyed by seat label. */\n seats: Record<string, string>;\n updatedAt: number;\n}\n\nexport interface HoldResult {\n holdId: string;\n expiresAt: number;\n /** The held seats with the buyer's chosen ticket tier per seat (present on hold). */\n seats?: SelectedSeat[];\n items?: HoldLineItem[];\n}\n\n/** Best-available response — the server-picked seats plus the hold they landed in. */\nexport interface BestAvailableResult {\n holdId: string;\n expiresAt: number;\n labels: string[];\n seats?: SelectedSeat[];\n items?: HoldResult['items'];\n}\n\nasync function request<T>(\n base: string,\n path: string,\n init: { method?: 'GET' | 'POST'; body?: unknown } = {},\n): Promise<T> {\n const method = init.method ?? 'GET';\n const headers: Record<string, string> = {};\n let body: string | undefined;\n if (init.body !== undefined) {\n headers['Content-Type'] = 'application/json';\n body = JSON.stringify(init.body);\n }\n\n const res = await fetch(`${base}${path}`, { method, headers, body, credentials: 'omit' });\n\n const isJson = (res.headers.get('content-type') ?? '').includes('application/json');\n const data = isJson ? await res.json().catch(() => null) : null;\n\n if (!res.ok) {\n const err = data as\n | { error?: string; code?: string; conflicts?: HoldConflict[]; reason?: string }\n | null;\n throw new ApiError(res.status, err?.error ?? `request_failed_${res.status}`, err?.code, err?.conflicts, err?.reason);\n }\n return data as T;\n}\n\n/** Public-surface client bound to one apiBase (e.g. https://api.seatlayer.io). */\nexport class PubApi {\n constructor(private readonly base: string) {}\n\n chart(key: string): Promise<PubChartResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/chart`);\n }\n\n objects(key: string): Promise<PubObjectsResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/objects`);\n }\n\n hold(key: string, selections: Array<{ label: string; tierId?: string | null }>, ttlMs?: number, replaceHoldId?: string): Promise<HoldResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold`, {\n method: 'POST',\n body: { selections, ...(ttlMs ? { ttlMs } : {}), ...(replaceHoldId ? { replaceHoldId } : {}) },\n });\n }\n\n bestAvailable(key: string, qty: number, categoryKey?: string): Promise<BestAvailableResult> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/best-available`, {\n method: 'POST',\n body: { qty, ...(categoryKey ? { categoryKey } : {}) },\n });\n }\n\n release(key: string, labels: string[], holdId: string): Promise<{ ok: true }> {\n return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {\n method: 'POST',\n body: { labels, holdId },\n });\n }\n\n socketUrl(key: string): string {\n const wsBase = this.base.replace(/^http/, 'ws');\n return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seatlayer/js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The SeatLayer embed SDK — render an interactive seat picker and hold seats from the browser. Works in any JS framework.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://docs.seatlayer.io",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"reserved-seating"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@seatlayer/core": "^0.
|
|
38
|
+
"@seatlayer/core": "^0.4.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsup": "^8.5.1",
|