@seatlayer/js 0.17.0 → 0.18.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
@@ -87,6 +87,11 @@ const designer = new EmbeddedDesigner({
87
87
  onPublished: ({ chartId }) => refreshVenue(chartId),
88
88
  onClose: () => closeVenueEditor(),
89
89
  onError: ({ code, message }) => showError(code ?? message),
90
+ // Mint a fresh session when the user retries an expired/failed editor:
91
+ onRequestRelaunch: async () => {
92
+ const next = await mintDesignerSession(session.chartId);
93
+ designer.setDesignerUrl(next.designerUrl);
94
+ },
90
95
  });
91
96
  designer.mount();
92
97
 
@@ -99,6 +104,24 @@ designer.destroy();
99
104
  Give the container a height, for example `min-height: 760px`. Keep the default
100
105
  `referrerPolicy: 'origin'`; the Designer uses it to verify the parent origin.
101
106
 
107
+ ### Built-in loading, error, and expiry states
108
+
109
+ By default the host paints a lightweight, branded skeleton inside the container
110
+ while the Designer boots, then removes it the moment the iframe reports `ready`.
111
+ If the session expires, the identity check fails, the iframe reports an error, or
112
+ it never becomes ready, the host swaps in a dark **"Try again"** card with copy
113
+ matched to the cause. The skeleton respects `prefers-reduced-motion` and adds no
114
+ CSS files or external assets.
115
+
116
+ | Option | Type | Default | What it does |
117
+ | --- | --- | --- | --- |
118
+ | `showLoadingState` | `boolean` | `true` | Render the built-in skeleton and error card. Set `false` when you draw your own chrome. |
119
+ | `loadingTimeoutMs` | `number` | `20000` | If `ready` never arrives within this window, show the error card with a timeout message. |
120
+ | `onRequestRelaunch` | `() => void` | — | Called by **"Try again"**. Mint a fresh session and call `setDesignerUrl()`; the iframe recreates and returns to loading. When omitted, "Try again" reloads the current URL in place. |
121
+
122
+ `setDesignerUrl()` always returns the host to the loading state, so a relaunch
123
+ flow needs no extra bookkeeping.
124
+
102
125
  ## API
103
126
 
104
127
  `new SeatingChart(options)` — options: `container` (selector or element, required),
package/dist/index.cjs CHANGED
@@ -358,16 +358,46 @@ var TYPES = /* @__PURE__ */ new Set([
358
358
  "seatlayer.designer.close",
359
359
  "seatlayer.designer.error"
360
360
  ]);
361
+ var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
361
362
  function resolveContainer2(container) {
362
363
  if (typeof container !== "string") return container;
363
364
  const element = document.querySelector(container);
364
365
  if (!element) throw new Error(`EmbeddedDesigner container not found: ${container}`);
365
366
  return element;
366
367
  }
368
+ function causeFromCode(code) {
369
+ const value = (code ?? "").toLowerCase();
370
+ if (value.includes("expire") || value.includes("revoke") || value === "401") return "expired";
371
+ if (value.includes("mismatch")) return "mismatch";
372
+ if (value.includes("timeout")) return "timeout";
373
+ return "load";
374
+ }
375
+ var ERROR_COPY = {
376
+ expired: {
377
+ title: "This design session expired",
378
+ body: "For your security, editing sessions are short-lived. Start a fresh one to keep designing."
379
+ },
380
+ mismatch: {
381
+ title: "This editor doesn't match this chart",
382
+ body: "The session that loaded belongs to a different chart or workspace. Reopen the designer to continue."
383
+ },
384
+ timeout: {
385
+ title: "The designer is taking too long",
386
+ body: "It did not finish loading in time. This is usually a slow connection \u2014 try again."
387
+ },
388
+ load: {
389
+ title: "We couldn't load the designer",
390
+ body: "Something went wrong while opening the editor. Please try again."
391
+ }
392
+ };
367
393
  var EmbeddedDesigner = class {
368
394
  constructor(options) {
369
395
  this.frame = null;
370
396
  this.designerOrigin = "";
397
+ this.overlay = null;
398
+ this.timeoutTimer = null;
399
+ this.phase = "loading";
400
+ this.restoreContainerPosition = null;
371
401
  this.handleMessage = (event) => {
372
402
  if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
373
403
  if (!event.data || typeof event.data !== "object") return;
@@ -382,10 +412,15 @@ var EmbeddedDesigner = class {
382
412
  message: typeof data.message === "string" ? data.message : void 0,
383
413
  meta: data.meta
384
414
  };
385
- if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId) return;
386
- if (this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) return;
415
+ if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId || this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) {
416
+ this.showError("mismatch");
417
+ return;
418
+ }
387
419
  switch (message.type) {
388
420
  case "seatlayer.designer.ready":
421
+ this.phase = "ready";
422
+ this.clearTimeoutTimer();
423
+ this.removeOverlay();
389
424
  this.options.onReady?.(message);
390
425
  break;
391
426
  case "seatlayer.designer.saved":
@@ -398,6 +433,7 @@ var EmbeddedDesigner = class {
398
433
  this.options.onClose?.(message);
399
434
  break;
400
435
  case "seatlayer.designer.error":
436
+ this.showError(causeFromCode(message.code));
401
437
  this.options.onError?.(message);
402
438
  break;
403
439
  }
@@ -421,9 +457,21 @@ var EmbeddedDesigner = class {
421
457
  frame.style.border = "0";
422
458
  Object.assign(frame.style, this.options.style);
423
459
  if (this.options.className) frame.className = this.options.className;
460
+ const container = resolveContainer2(this.options.container);
424
461
  window.addEventListener("message", this.handleMessage);
425
- resolveContainer2(this.options.container).append(frame);
462
+ container.append(frame);
426
463
  this.frame = frame;
464
+ this.phase = "loading";
465
+ if (this.loadingStateEnabled()) {
466
+ this.ensureContainerPositioned(container);
467
+ this.renderOverlay(container, "loading");
468
+ const timeout = this.options.loadingTimeoutMs ?? DEFAULT_LOADING_TIMEOUT_MS;
469
+ if (timeout > 0 && Number.isFinite(timeout)) {
470
+ this.timeoutTimer = setTimeout(() => {
471
+ if (this.phase === "loading") this.showError("timeout");
472
+ }, timeout);
473
+ }
474
+ }
427
475
  return frame;
428
476
  }
429
477
  /** Replace the iframe instead of assigning a new fragment to an existing one. */
@@ -436,9 +484,208 @@ var EmbeddedDesigner = class {
436
484
  }
437
485
  destroy() {
438
486
  window.removeEventListener("message", this.handleMessage);
487
+ this.clearTimeoutTimer();
488
+ this.removeOverlay();
489
+ this.restoreContainerStyle();
439
490
  this.frame?.remove();
440
491
  this.frame = null;
441
492
  this.designerOrigin = "";
493
+ this.phase = "loading";
494
+ }
495
+ loadingStateEnabled() {
496
+ return this.options.showLoadingState !== false;
497
+ }
498
+ clearTimeoutTimer() {
499
+ if (this.timeoutTimer !== null) {
500
+ clearTimeout(this.timeoutTimer);
501
+ this.timeoutTimer = null;
502
+ }
503
+ }
504
+ ensureContainerPositioned(container) {
505
+ const position = getComputedStyle(container).position;
506
+ if (position === "static") {
507
+ this.restoreContainerPosition = container.style.position;
508
+ container.style.position = "relative";
509
+ }
510
+ }
511
+ restoreContainerStyle() {
512
+ if (this.restoreContainerPosition === null) return;
513
+ try {
514
+ resolveContainer2(this.options.container).style.position = this.restoreContainerPosition;
515
+ } catch {
516
+ }
517
+ this.restoreContainerPosition = null;
518
+ }
519
+ removeOverlay() {
520
+ this.overlay?.remove();
521
+ this.overlay = null;
522
+ }
523
+ showError(cause) {
524
+ this.phase = "error";
525
+ this.clearTimeoutTimer();
526
+ if (!this.loadingStateEnabled()) return;
527
+ let container;
528
+ try {
529
+ container = resolveContainer2(this.options.container);
530
+ } catch {
531
+ return;
532
+ }
533
+ this.renderOverlay(container, "error", cause);
534
+ }
535
+ handleTryAgain() {
536
+ if (this.options.onRequestRelaunch) {
537
+ this.options.onRequestRelaunch();
538
+ return;
539
+ }
540
+ this.mount();
541
+ }
542
+ /**
543
+ * Build (or rebuild) the overlay for the given phase. A single overlay element
544
+ * is reused so we never stack stale skeletons or cards.
545
+ */
546
+ renderOverlay(container, phase, cause) {
547
+ this.removeOverlay();
548
+ const overlay = document.createElement("div");
549
+ overlay.setAttribute("data-seatlayer-designer-overlay", phase);
550
+ overlay.setAttribute("role", phase === "error" ? "alert" : "status");
551
+ overlay.setAttribute("aria-live", "polite");
552
+ Object.assign(overlay.style, {
553
+ position: "absolute",
554
+ inset: "0",
555
+ display: "flex",
556
+ alignItems: "center",
557
+ justifyContent: "center",
558
+ background: "#101625",
559
+ color: "#e6ebf5",
560
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
561
+ zIndex: "2",
562
+ overflow: "hidden"
563
+ });
564
+ if (phase === "loading") this.buildSkeleton(overlay);
565
+ else this.buildErrorCard(overlay, cause ?? "load");
566
+ container.append(overlay);
567
+ this.overlay = overlay;
568
+ }
569
+ buildSkeleton(overlay) {
570
+ const style = document.createElement("style");
571
+ style.textContent = `
572
+ @media (prefers-reduced-motion: no-preference) {
573
+ @keyframes seatlayer-designer-shimmer {
574
+ 0% { background-position: -320px 0; }
575
+ 100% { background-position: 320px 0; }
576
+ }
577
+ [data-seatlayer-designer-overlay="loading"] .sl-shimmer {
578
+ animation: seatlayer-designer-shimmer 1.25s ease-in-out infinite;
579
+ background-size: 640px 100%;
580
+ }
581
+ }`;
582
+ overlay.append(style);
583
+ const shimmer = "linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.10) 37%, rgba(255,255,255,0.04) 63%)";
584
+ const scaffold = document.createElement("div");
585
+ Object.assign(scaffold.style, {
586
+ position: "absolute",
587
+ inset: "0",
588
+ display: "flex",
589
+ flexDirection: "column",
590
+ padding: "16px",
591
+ gap: "14px",
592
+ opacity: "0.9"
593
+ });
594
+ const bar = (styles) => {
595
+ const node = document.createElement("div");
596
+ node.className = "sl-shimmer";
597
+ Object.assign(node.style, {
598
+ background: shimmer,
599
+ borderRadius: "8px"
600
+ });
601
+ Object.assign(node.style, styles);
602
+ return node;
603
+ };
604
+ scaffold.append(bar({ height: "40px", width: "100%", flex: "0 0 auto" }));
605
+ const body = document.createElement("div");
606
+ Object.assign(body.style, {
607
+ display: "flex",
608
+ gap: "14px",
609
+ flex: "1 1 auto",
610
+ minHeight: "0"
611
+ });
612
+ body.append(bar({ width: "220px", height: "100%", flex: "0 0 auto" }));
613
+ body.append(bar({ flex: "1 1 auto", height: "100%" }));
614
+ scaffold.append(body);
615
+ overlay.append(scaffold);
616
+ const caption = document.createElement("div");
617
+ Object.assign(caption.style, {
618
+ position: "relative",
619
+ zIndex: "1",
620
+ display: "flex",
621
+ alignItems: "center",
622
+ gap: "10px",
623
+ padding: "10px 16px",
624
+ borderRadius: "999px",
625
+ background: "rgba(16, 22, 37, 0.72)",
626
+ fontSize: "13px",
627
+ fontWeight: "500",
628
+ letterSpacing: "0.01em"
629
+ });
630
+ const dot = document.createElement("span");
631
+ dot.className = "sl-shimmer";
632
+ Object.assign(dot.style, {
633
+ width: "9px",
634
+ height: "9px",
635
+ borderRadius: "50%",
636
+ background: shimmer,
637
+ flex: "0 0 auto"
638
+ });
639
+ caption.append(dot);
640
+ caption.append(document.createTextNode("Loading designer\u2026"));
641
+ overlay.append(caption);
642
+ }
643
+ buildErrorCard(overlay, cause) {
644
+ const copy = ERROR_COPY[cause];
645
+ const card = document.createElement("div");
646
+ Object.assign(card.style, {
647
+ maxWidth: "420px",
648
+ margin: "0 24px",
649
+ padding: "28px",
650
+ textAlign: "center",
651
+ background: "rgba(255, 255, 255, 0.03)",
652
+ border: "1px solid rgba(255, 255, 255, 0.08)",
653
+ borderRadius: "16px",
654
+ boxShadow: "0 12px 40px rgba(0, 0, 0, 0.35)"
655
+ });
656
+ const heading = document.createElement("h2");
657
+ heading.textContent = copy.title;
658
+ Object.assign(heading.style, {
659
+ margin: "0 0 8px",
660
+ fontSize: "17px",
661
+ fontWeight: "600",
662
+ color: "#f4f7ff"
663
+ });
664
+ const body = document.createElement("p");
665
+ body.textContent = copy.body;
666
+ Object.assign(body.style, {
667
+ margin: "0 0 20px",
668
+ fontSize: "13.5px",
669
+ lineHeight: "1.5",
670
+ color: "#aab4c8"
671
+ });
672
+ const button = document.createElement("button");
673
+ button.type = "button";
674
+ button.textContent = "Try again";
675
+ Object.assign(button.style, {
676
+ appearance: "none",
677
+ cursor: "pointer",
678
+ border: "0",
679
+ borderRadius: "10px",
680
+ padding: "10px 22px",
681
+ fontSize: "14px",
682
+ fontWeight: "600",
683
+ color: "#101625",
684
+ background: "#7aa2ff"
685
+ });
686
+ button.addEventListener("click", () => this.handleTryAgain());
687
+ card.append(heading, body, button);
688
+ overlay.append(card);
442
689
  }
443
690
  };
444
691