@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/dist/index.d.cts CHANGED
@@ -242,6 +242,25 @@ interface EmbeddedDesignerOptions {
242
242
  style?: Partial<CSSStyleDeclaration>;
243
243
  allow?: string;
244
244
  referrerPolicy?: ReferrerPolicy;
245
+ /**
246
+ * Show the built-in branded loading skeleton and error/expiry card inside the
247
+ * container while the Designer boots. Defaults to `true`. Set `false` when the
248
+ * host renders its own loading and error chrome.
249
+ */
250
+ showLoadingState?: boolean;
251
+ /**
252
+ * If the Designer never posts `ready` within this many milliseconds, the host
253
+ * transitions to the error card with a timeout message. Defaults to `20000`.
254
+ * Only used when `showLoadingState` is enabled.
255
+ */
256
+ loadingTimeoutMs?: number;
257
+ /**
258
+ * Called when the user presses "Try again" on the error card. Use it to mint a
259
+ * fresh Designer session and call `setDesignerUrl()` with the new URL, which
260
+ * recreates the iframe and returns to the loading state. When omitted, "Try
261
+ * again" reloads the current `designerUrl` in place.
262
+ */
263
+ onRequestRelaunch?: () => void;
245
264
  onReady?: (message: EmbeddedDesignerMessage) => void;
246
265
  onSaved?: (message: EmbeddedDesignerMessage) => void;
247
266
  onPublished?: (message: EmbeddedDesignerMessage) => void;
@@ -253,12 +272,30 @@ declare class EmbeddedDesigner {
253
272
  private options;
254
273
  private frame;
255
274
  private designerOrigin;
275
+ private overlay;
276
+ private timeoutTimer;
277
+ private phase;
278
+ private restoreContainerPosition;
256
279
  constructor(options: EmbeddedDesignerOptions);
257
280
  mount(): HTMLIFrameElement;
258
281
  /** Replace the iframe instead of assigning a new fragment to an existing one. */
259
282
  setDesignerUrl(designerUrl: string): HTMLIFrameElement;
260
283
  getIframe(): HTMLIFrameElement | null;
261
284
  destroy(): void;
285
+ private loadingStateEnabled;
286
+ private clearTimeoutTimer;
287
+ private ensureContainerPositioned;
288
+ private restoreContainerStyle;
289
+ private removeOverlay;
290
+ private showError;
291
+ private handleTryAgain;
292
+ /**
293
+ * Build (or rebuild) the overlay for the given phase. A single overlay element
294
+ * is reused so we never stack stale skeletons or cards.
295
+ */
296
+ private renderOverlay;
297
+ private buildSkeleton;
298
+ private buildErrorCard;
262
299
  private handleMessage;
263
300
  }
264
301
 
package/dist/index.d.ts CHANGED
@@ -242,6 +242,25 @@ interface EmbeddedDesignerOptions {
242
242
  style?: Partial<CSSStyleDeclaration>;
243
243
  allow?: string;
244
244
  referrerPolicy?: ReferrerPolicy;
245
+ /**
246
+ * Show the built-in branded loading skeleton and error/expiry card inside the
247
+ * container while the Designer boots. Defaults to `true`. Set `false` when the
248
+ * host renders its own loading and error chrome.
249
+ */
250
+ showLoadingState?: boolean;
251
+ /**
252
+ * If the Designer never posts `ready` within this many milliseconds, the host
253
+ * transitions to the error card with a timeout message. Defaults to `20000`.
254
+ * Only used when `showLoadingState` is enabled.
255
+ */
256
+ loadingTimeoutMs?: number;
257
+ /**
258
+ * Called when the user presses "Try again" on the error card. Use it to mint a
259
+ * fresh Designer session and call `setDesignerUrl()` with the new URL, which
260
+ * recreates the iframe and returns to the loading state. When omitted, "Try
261
+ * again" reloads the current `designerUrl` in place.
262
+ */
263
+ onRequestRelaunch?: () => void;
245
264
  onReady?: (message: EmbeddedDesignerMessage) => void;
246
265
  onSaved?: (message: EmbeddedDesignerMessage) => void;
247
266
  onPublished?: (message: EmbeddedDesignerMessage) => void;
@@ -253,12 +272,30 @@ declare class EmbeddedDesigner {
253
272
  private options;
254
273
  private frame;
255
274
  private designerOrigin;
275
+ private overlay;
276
+ private timeoutTimer;
277
+ private phase;
278
+ private restoreContainerPosition;
256
279
  constructor(options: EmbeddedDesignerOptions);
257
280
  mount(): HTMLIFrameElement;
258
281
  /** Replace the iframe instead of assigning a new fragment to an existing one. */
259
282
  setDesignerUrl(designerUrl: string): HTMLIFrameElement;
260
283
  getIframe(): HTMLIFrameElement | null;
261
284
  destroy(): void;
285
+ private loadingStateEnabled;
286
+ private clearTimeoutTimer;
287
+ private ensureContainerPositioned;
288
+ private restoreContainerStyle;
289
+ private removeOverlay;
290
+ private showError;
291
+ private handleTryAgain;
292
+ /**
293
+ * Build (or rebuild) the overlay for the given phase. A single overlay element
294
+ * is reused so we never stack stale skeletons or cards.
295
+ */
296
+ private renderOverlay;
297
+ private buildSkeleton;
298
+ private buildErrorCard;
262
299
  private handleMessage;
263
300
  }
264
301
 
package/dist/index.js CHANGED
@@ -326,16 +326,46 @@ var TYPES = /* @__PURE__ */ new Set([
326
326
  "seatlayer.designer.close",
327
327
  "seatlayer.designer.error"
328
328
  ]);
329
+ var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
329
330
  function resolveContainer2(container) {
330
331
  if (typeof container !== "string") return container;
331
332
  const element = document.querySelector(container);
332
333
  if (!element) throw new Error(`EmbeddedDesigner container not found: ${container}`);
333
334
  return element;
334
335
  }
336
+ function causeFromCode(code) {
337
+ const value = (code ?? "").toLowerCase();
338
+ if (value.includes("expire") || value.includes("revoke") || value === "401") return "expired";
339
+ if (value.includes("mismatch")) return "mismatch";
340
+ if (value.includes("timeout")) return "timeout";
341
+ return "load";
342
+ }
343
+ var ERROR_COPY = {
344
+ expired: {
345
+ title: "This design session expired",
346
+ body: "For your security, editing sessions are short-lived. Start a fresh one to keep designing."
347
+ },
348
+ mismatch: {
349
+ title: "This editor doesn't match this chart",
350
+ body: "The session that loaded belongs to a different chart or workspace. Reopen the designer to continue."
351
+ },
352
+ timeout: {
353
+ title: "The designer is taking too long",
354
+ body: "It did not finish loading in time. This is usually a slow connection \u2014 try again."
355
+ },
356
+ load: {
357
+ title: "We couldn't load the designer",
358
+ body: "Something went wrong while opening the editor. Please try again."
359
+ }
360
+ };
335
361
  var EmbeddedDesigner = class {
336
362
  constructor(options) {
337
363
  this.frame = null;
338
364
  this.designerOrigin = "";
365
+ this.overlay = null;
366
+ this.timeoutTimer = null;
367
+ this.phase = "loading";
368
+ this.restoreContainerPosition = null;
339
369
  this.handleMessage = (event) => {
340
370
  if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
341
371
  if (!event.data || typeof event.data !== "object") return;
@@ -350,10 +380,15 @@ var EmbeddedDesigner = class {
350
380
  message: typeof data.message === "string" ? data.message : void 0,
351
381
  meta: data.meta
352
382
  };
353
- if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId) return;
354
- if (this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) return;
383
+ if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId || this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) {
384
+ this.showError("mismatch");
385
+ return;
386
+ }
355
387
  switch (message.type) {
356
388
  case "seatlayer.designer.ready":
389
+ this.phase = "ready";
390
+ this.clearTimeoutTimer();
391
+ this.removeOverlay();
357
392
  this.options.onReady?.(message);
358
393
  break;
359
394
  case "seatlayer.designer.saved":
@@ -366,6 +401,7 @@ var EmbeddedDesigner = class {
366
401
  this.options.onClose?.(message);
367
402
  break;
368
403
  case "seatlayer.designer.error":
404
+ this.showError(causeFromCode(message.code));
369
405
  this.options.onError?.(message);
370
406
  break;
371
407
  }
@@ -389,9 +425,21 @@ var EmbeddedDesigner = class {
389
425
  frame.style.border = "0";
390
426
  Object.assign(frame.style, this.options.style);
391
427
  if (this.options.className) frame.className = this.options.className;
428
+ const container = resolveContainer2(this.options.container);
392
429
  window.addEventListener("message", this.handleMessage);
393
- resolveContainer2(this.options.container).append(frame);
430
+ container.append(frame);
394
431
  this.frame = frame;
432
+ this.phase = "loading";
433
+ if (this.loadingStateEnabled()) {
434
+ this.ensureContainerPositioned(container);
435
+ this.renderOverlay(container, "loading");
436
+ const timeout = this.options.loadingTimeoutMs ?? DEFAULT_LOADING_TIMEOUT_MS;
437
+ if (timeout > 0 && Number.isFinite(timeout)) {
438
+ this.timeoutTimer = setTimeout(() => {
439
+ if (this.phase === "loading") this.showError("timeout");
440
+ }, timeout);
441
+ }
442
+ }
395
443
  return frame;
396
444
  }
397
445
  /** Replace the iframe instead of assigning a new fragment to an existing one. */
@@ -404,9 +452,208 @@ var EmbeddedDesigner = class {
404
452
  }
405
453
  destroy() {
406
454
  window.removeEventListener("message", this.handleMessage);
455
+ this.clearTimeoutTimer();
456
+ this.removeOverlay();
457
+ this.restoreContainerStyle();
407
458
  this.frame?.remove();
408
459
  this.frame = null;
409
460
  this.designerOrigin = "";
461
+ this.phase = "loading";
462
+ }
463
+ loadingStateEnabled() {
464
+ return this.options.showLoadingState !== false;
465
+ }
466
+ clearTimeoutTimer() {
467
+ if (this.timeoutTimer !== null) {
468
+ clearTimeout(this.timeoutTimer);
469
+ this.timeoutTimer = null;
470
+ }
471
+ }
472
+ ensureContainerPositioned(container) {
473
+ const position = getComputedStyle(container).position;
474
+ if (position === "static") {
475
+ this.restoreContainerPosition = container.style.position;
476
+ container.style.position = "relative";
477
+ }
478
+ }
479
+ restoreContainerStyle() {
480
+ if (this.restoreContainerPosition === null) return;
481
+ try {
482
+ resolveContainer2(this.options.container).style.position = this.restoreContainerPosition;
483
+ } catch {
484
+ }
485
+ this.restoreContainerPosition = null;
486
+ }
487
+ removeOverlay() {
488
+ this.overlay?.remove();
489
+ this.overlay = null;
490
+ }
491
+ showError(cause) {
492
+ this.phase = "error";
493
+ this.clearTimeoutTimer();
494
+ if (!this.loadingStateEnabled()) return;
495
+ let container;
496
+ try {
497
+ container = resolveContainer2(this.options.container);
498
+ } catch {
499
+ return;
500
+ }
501
+ this.renderOverlay(container, "error", cause);
502
+ }
503
+ handleTryAgain() {
504
+ if (this.options.onRequestRelaunch) {
505
+ this.options.onRequestRelaunch();
506
+ return;
507
+ }
508
+ this.mount();
509
+ }
510
+ /**
511
+ * Build (or rebuild) the overlay for the given phase. A single overlay element
512
+ * is reused so we never stack stale skeletons or cards.
513
+ */
514
+ renderOverlay(container, phase, cause) {
515
+ this.removeOverlay();
516
+ const overlay = document.createElement("div");
517
+ overlay.setAttribute("data-seatlayer-designer-overlay", phase);
518
+ overlay.setAttribute("role", phase === "error" ? "alert" : "status");
519
+ overlay.setAttribute("aria-live", "polite");
520
+ Object.assign(overlay.style, {
521
+ position: "absolute",
522
+ inset: "0",
523
+ display: "flex",
524
+ alignItems: "center",
525
+ justifyContent: "center",
526
+ background: "#101625",
527
+ color: "#e6ebf5",
528
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
529
+ zIndex: "2",
530
+ overflow: "hidden"
531
+ });
532
+ if (phase === "loading") this.buildSkeleton(overlay);
533
+ else this.buildErrorCard(overlay, cause ?? "load");
534
+ container.append(overlay);
535
+ this.overlay = overlay;
536
+ }
537
+ buildSkeleton(overlay) {
538
+ const style = document.createElement("style");
539
+ style.textContent = `
540
+ @media (prefers-reduced-motion: no-preference) {
541
+ @keyframes seatlayer-designer-shimmer {
542
+ 0% { background-position: -320px 0; }
543
+ 100% { background-position: 320px 0; }
544
+ }
545
+ [data-seatlayer-designer-overlay="loading"] .sl-shimmer {
546
+ animation: seatlayer-designer-shimmer 1.25s ease-in-out infinite;
547
+ background-size: 640px 100%;
548
+ }
549
+ }`;
550
+ overlay.append(style);
551
+ 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%)";
552
+ const scaffold = document.createElement("div");
553
+ Object.assign(scaffold.style, {
554
+ position: "absolute",
555
+ inset: "0",
556
+ display: "flex",
557
+ flexDirection: "column",
558
+ padding: "16px",
559
+ gap: "14px",
560
+ opacity: "0.9"
561
+ });
562
+ const bar = (styles) => {
563
+ const node = document.createElement("div");
564
+ node.className = "sl-shimmer";
565
+ Object.assign(node.style, {
566
+ background: shimmer,
567
+ borderRadius: "8px"
568
+ });
569
+ Object.assign(node.style, styles);
570
+ return node;
571
+ };
572
+ scaffold.append(bar({ height: "40px", width: "100%", flex: "0 0 auto" }));
573
+ const body = document.createElement("div");
574
+ Object.assign(body.style, {
575
+ display: "flex",
576
+ gap: "14px",
577
+ flex: "1 1 auto",
578
+ minHeight: "0"
579
+ });
580
+ body.append(bar({ width: "220px", height: "100%", flex: "0 0 auto" }));
581
+ body.append(bar({ flex: "1 1 auto", height: "100%" }));
582
+ scaffold.append(body);
583
+ overlay.append(scaffold);
584
+ const caption = document.createElement("div");
585
+ Object.assign(caption.style, {
586
+ position: "relative",
587
+ zIndex: "1",
588
+ display: "flex",
589
+ alignItems: "center",
590
+ gap: "10px",
591
+ padding: "10px 16px",
592
+ borderRadius: "999px",
593
+ background: "rgba(16, 22, 37, 0.72)",
594
+ fontSize: "13px",
595
+ fontWeight: "500",
596
+ letterSpacing: "0.01em"
597
+ });
598
+ const dot = document.createElement("span");
599
+ dot.className = "sl-shimmer";
600
+ Object.assign(dot.style, {
601
+ width: "9px",
602
+ height: "9px",
603
+ borderRadius: "50%",
604
+ background: shimmer,
605
+ flex: "0 0 auto"
606
+ });
607
+ caption.append(dot);
608
+ caption.append(document.createTextNode("Loading designer\u2026"));
609
+ overlay.append(caption);
610
+ }
611
+ buildErrorCard(overlay, cause) {
612
+ const copy = ERROR_COPY[cause];
613
+ const card = document.createElement("div");
614
+ Object.assign(card.style, {
615
+ maxWidth: "420px",
616
+ margin: "0 24px",
617
+ padding: "28px",
618
+ textAlign: "center",
619
+ background: "rgba(255, 255, 255, 0.03)",
620
+ border: "1px solid rgba(255, 255, 255, 0.08)",
621
+ borderRadius: "16px",
622
+ boxShadow: "0 12px 40px rgba(0, 0, 0, 0.35)"
623
+ });
624
+ const heading = document.createElement("h2");
625
+ heading.textContent = copy.title;
626
+ Object.assign(heading.style, {
627
+ margin: "0 0 8px",
628
+ fontSize: "17px",
629
+ fontWeight: "600",
630
+ color: "#f4f7ff"
631
+ });
632
+ const body = document.createElement("p");
633
+ body.textContent = copy.body;
634
+ Object.assign(body.style, {
635
+ margin: "0 0 20px",
636
+ fontSize: "13.5px",
637
+ lineHeight: "1.5",
638
+ color: "#aab4c8"
639
+ });
640
+ const button = document.createElement("button");
641
+ button.type = "button";
642
+ button.textContent = "Try again";
643
+ Object.assign(button.style, {
644
+ appearance: "none",
645
+ cursor: "pointer",
646
+ border: "0",
647
+ borderRadius: "10px",
648
+ padding: "10px 22px",
649
+ fontSize: "14px",
650
+ fontWeight: "600",
651
+ color: "#101625",
652
+ background: "#7aa2ff"
653
+ });
654
+ button.addEventListener("click", () => this.handleTryAgain());
655
+ card.append(heading, body, button);
656
+ overlay.append(card);
410
657
  }
411
658
  };
412
659