@syntrologie/adapt-chatbot 2.39.0 → 2.41.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/runtime.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  getElementInstanceStore,
18
18
  setElementInstanceStore
19
19
  } from "./chunk-EWPPVPJ4.js";
20
- import "./chunk-P74YY3MD.js";
20
+ import "./chunk-3Z52KSYZ.js";
21
21
  import "./chunk-VZIUXXAM.js";
22
22
  import "./chunk-OIGVZGSX.js";
23
23
  import {
@@ -2297,70 +2297,177 @@ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2297
2297
  var box = {
2298
2298
  display: "flex",
2299
2299
  flexDirection: "column",
2300
- gap: "var(--sc-content-item-gap, 10px)",
2300
+ gap: "10px",
2301
2301
  fontFamily: "var(--sc-font-family, system-ui)",
2302
- fontSize: "var(--sc-content-body-font-size, 13px)"
2302
+ fontSize: "13px",
2303
+ // Defect: card content sat flush against its container's edges (no
2304
+ // padding anywhere). Fixed, compact — matches the rest of this file's
2305
+ // "form scale" sizing, not the content section's chat-bubble padding.
2306
+ padding: "16px",
2307
+ boxSizing: "border-box"
2303
2308
  };
2304
- var fieldStyles = {
2305
- padding: "var(--sc-content-item-padding, 10px 12px)",
2309
+ var fieldWrapStyles = {
2310
+ position: "relative",
2306
2311
  borderRadius: "var(--sc-content-border-radius, 8px)",
2307
2312
  border: "var(--sc-content-bubble-border, 1px solid currentColor)",
2308
2313
  background: "var(--sc-content-bubble-background, transparent)",
2314
+ boxSizing: "border-box",
2315
+ // Clips the (square-cornered, 100%-filled) inner field to this shell's
2316
+ // rounded corners — without it, flat textarea/input corners can poke
2317
+ // past the shell's curve at the very edges.
2318
+ overflow: "hidden"
2319
+ };
2320
+ var messageWrapStyles = {
2321
+ ...fieldWrapStyles,
2322
+ height: "84px"
2323
+ };
2324
+ var singleLineWrapStyles = {
2325
+ ...fieldWrapStyles,
2326
+ height: "44px"
2327
+ };
2328
+ var fieldInnerBase = {
2329
+ border: "none",
2330
+ outline: "none",
2331
+ background: "transparent",
2309
2332
  color: "inherit",
2310
2333
  font: "inherit",
2311
- // The browser's default focus ring is a loud blue on a black-and-white
2312
- // brand — the one saturated pixel on the card. Suppress it and paint our
2313
- // own from the theme in the focus/blur handlers below.
2314
- outline: "none",
2315
2334
  width: "100%",
2335
+ height: "100%",
2316
2336
  boxSizing: "border-box",
2317
- // The drag grabber pokes out of the card's bottom-right corner and lets
2318
- // the visitor break the tile frame — the card owns its own geometry.
2319
2337
  resize: "none"
2320
2338
  };
2339
+ var messageInnerStyles = {
2340
+ ...fieldInnerBase,
2341
+ padding: "8px 42px 8px 10px",
2342
+ overflowY: "auto"
2343
+ };
2344
+ var singleLineInnerStyles = {
2345
+ ...fieldInnerBase,
2346
+ padding: "0 42px 0 10px"
2347
+ };
2348
+ var srOnlyStyles = {
2349
+ position: "absolute",
2350
+ width: "1px",
2351
+ height: "1px",
2352
+ padding: "0",
2353
+ margin: "-1px",
2354
+ overflow: "hidden",
2355
+ whiteSpace: "nowrap",
2356
+ border: "0"
2357
+ };
2358
+ var sendIcon = html`<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M8 14V2M8 2L3 7M8 2L13 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
2359
+ var embeddedIconButtonStyles = {
2360
+ position: "absolute",
2361
+ right: "5px",
2362
+ bottom: "5px",
2363
+ display: "flex",
2364
+ alignItems: "center",
2365
+ justifyContent: "center",
2366
+ width: "30px",
2367
+ height: "30px",
2368
+ padding: "0",
2369
+ borderRadius: "50%",
2370
+ border: "none",
2371
+ background: "var(--sc-color-primary, #000000)",
2372
+ color: "var(--sc-launcher-color, #ffffff)",
2373
+ cursor: "pointer"
2374
+ };
2375
+ var embeddedIconButtonCenteredStyles = {
2376
+ ...embeddedIconButtonStyles,
2377
+ bottom: "auto",
2378
+ top: "50%",
2379
+ transform: "translateY(-50%)"
2380
+ };
2381
+ var embeddedIconButtonDisabledStyles = {
2382
+ ...embeddedIconButtonStyles,
2383
+ background: "rgba(120, 120, 120, 0.25)",
2384
+ color: "rgba(90, 90, 90, 0.9)",
2385
+ cursor: "default"
2386
+ };
2387
+ var embeddedIconButtonCenteredDisabledStyles = {
2388
+ ...embeddedIconButtonCenteredStyles,
2389
+ background: "rgba(120, 120, 120, 0.25)",
2390
+ color: "rgba(90, 90, 90, 0.9)",
2391
+ cursor: "default"
2392
+ };
2321
2393
  var buttonStyles = {
2322
2394
  padding: "11px 14px",
2323
2395
  borderRadius: "var(--sc-border-radius, 8px)",
2324
2396
  border: "none",
2325
2397
  background: "var(--sc-color-primary, #000000)",
2326
- color: "var(--sc-accent-foreground, #ffffff)",
2398
+ color: "var(--sc-launcher-color, #ffffff)",
2327
2399
  fontWeight: "600",
2328
2400
  cursor: "pointer",
2329
2401
  font: "inherit",
2330
2402
  width: "100%",
2331
2403
  boxSizing: "border-box"
2332
2404
  };
2333
- var textStyles = {
2334
- margin: "0"
2405
+ var fieldRowStyles = {
2406
+ display: "flex",
2407
+ alignItems: "center",
2408
+ gap: "8px"
2409
+ };
2410
+ var skipButtonStyles = {
2411
+ flexShrink: "0",
2412
+ padding: "0",
2413
+ border: "none",
2414
+ background: "transparent",
2415
+ color: "inherit",
2416
+ textDecoration: "underline",
2417
+ cursor: "pointer",
2418
+ font: "inherit",
2419
+ fontSize: "12px",
2420
+ opacity: "0.8",
2421
+ whiteSpace: "nowrap"
2335
2422
  };
2336
2423
  var mutedTextStyles = {
2337
2424
  margin: "0",
2338
2425
  // No opacity token exists in any publishable section — plain constant.
2339
2426
  opacity: "0.8"
2340
2427
  };
2428
+ var trailHostStyles = {
2429
+ display: "flex",
2430
+ flexDirection: "column",
2431
+ height: "160px",
2432
+ overflow: "hidden",
2433
+ width: "100%",
2434
+ boxSizing: "border-box"
2435
+ };
2341
2436
  var PERSIST_NAMESPACE = "supportchat";
2342
2437
  var PERSIST_KEY = "leave-a-message";
2438
+ var lamUidCounter = 0;
2343
2439
  var LeaveAMessageLit = class extends LitElement {
2344
2440
  constructor() {
2345
2441
  super(...arguments);
2346
2442
  this.message = "";
2347
2443
  this.contextSummary = "";
2348
2444
  this.state = "form";
2445
+ /** Unique per-instance id prefix so `<label for>` never collides across
2446
+ * more than one card mounted on the same page. */
2447
+ this._uid = `lam-${++lamUidCounter}`;
2349
2448
  this._email = "";
2350
2449
  this._reply = "";
2351
- this._operatorMessages = [];
2352
- /** Paint our own focus ring from the theme see fieldStyles.outline. */
2450
+ /** Visitor↔operator conversation, in order — rendered as a chat
2451
+ * transcript once the visitor taps into the mail state (see render()'s
2452
+ * 'flipped' branch). Seeded with the visitor's own compose-step message
2453
+ * in `_submitMessage`. */
2454
+ this._transcript = [];
2455
+ /** Paint our own focus ring from the theme. The border now lives on the
2456
+ * field SHELL (`data-lam-field-wrap`), not the borderless inner
2457
+ * textarea/input, so the ring targets that ancestor via `closest()`. */
2353
2458
  this._focusOn = (e) => {
2354
- const el = e.target;
2355
- el.style.outline = "2px solid var(--sc-color-primary, currentColor)";
2356
- el.style.outlineOffset = "1px";
2459
+ const field = e.target;
2460
+ const target = field.closest("[data-lam-field-wrap]") ?? field;
2461
+ target.style.outline = "2px solid var(--sc-color-primary, currentColor)";
2462
+ target.style.outlineOffset = "1px";
2357
2463
  };
2358
2464
  this._focusOff = (e) => {
2359
- const el = e.target;
2360
- el.style.outline = "none";
2465
+ const field = e.target;
2466
+ const target = field.closest("[data-lam-field-wrap]") ?? field;
2467
+ target.style.outline = "none";
2361
2468
  };
2362
2469
  this._onReply = (text) => {
2363
- this._operatorMessages = [...this._operatorMessages, text];
2470
+ this._transcript = [...this._transcript, { role: "operator", text }];
2364
2471
  if (this.state !== "flipped") {
2365
2472
  this.state = "mail";
2366
2473
  this.runtime?.events.publish("supportchat:reply_received", { text });
@@ -2376,12 +2483,37 @@ var LeaveAMessageLit = class extends LitElement {
2376
2483
  this._persist();
2377
2484
  this.requestUpdate();
2378
2485
  };
2379
- this._submit = () => {
2380
- if (!this.bridge || !EMAIL_RE.test(this._email)) return;
2381
- this.bridge.setVisitorEmail(this._email);
2486
+ /** Compose step → email step. Delivers the message immediately; email
2487
+ * capture is a SEPARATE, later, optional step (owner redesign). Seeds
2488
+ * the chat transcript with the visitor's OWN plain message text (not
2489
+ * the context-prefixed string sent to the bridge — that prefix is
2490
+ * internal plumbing for the human agent, not something the visitor
2491
+ * should see echoed back at themselves). */
2492
+ this._submitMessage = () => {
2493
+ if (!this.bridge || !this.message.trim()) return;
2382
2494
  const context = this.contextSummary ? `[Site assistant context: ${this.contextSummary}]
2383
2495
  ` : "";
2384
2496
  this.bridge.sendMessage(`${context}${this.message}`);
2497
+ this._transcript = [...this._transcript, { role: "visitor", text: this.message.trim() }];
2498
+ this.state = "email";
2499
+ this._persist();
2500
+ };
2501
+ /** Email step → sent. Blank email is a valid submission (the field is
2502
+ * optional) and simply skips `setVisitorEmail`; an invalid non-blank
2503
+ * email blocks advancing so a mistyped address doesn't silently vanish. */
2504
+ this._submitEmail = () => {
2505
+ if (!this.bridge) return;
2506
+ const trimmed = this._email.trim();
2507
+ if (trimmed && !EMAIL_RE.test(trimmed)) return;
2508
+ if (trimmed) this.bridge.setVisitorEmail(trimmed);
2509
+ this.state = "sent";
2510
+ this._persist();
2511
+ };
2512
+ /** Explicit decline — advances regardless of whatever is typed, without
2513
+ * ever calling `setVisitorEmail`. Distinct from submitting a blank
2514
+ * field only in that it discards partial input the visitor may not
2515
+ * have meant to send. */
2516
+ this._skipEmail = () => {
2385
2517
  this.state = "sent";
2386
2518
  this._persist();
2387
2519
  };
@@ -2391,8 +2523,11 @@ var LeaveAMessageLit = class extends LitElement {
2391
2523
  };
2392
2524
  this._sendReply = () => {
2393
2525
  if (!this.bridge || !this._reply.trim()) return;
2394
- this.bridge.sendMessage(this._reply.trim());
2526
+ const text = this._reply.trim();
2527
+ this.bridge.sendMessage(text);
2528
+ this._transcript = [...this._transcript, { role: "visitor", text }];
2395
2529
  this._reply = "";
2530
+ this._persist();
2396
2531
  this.requestUpdate();
2397
2532
  };
2398
2533
  }
@@ -2417,25 +2552,25 @@ var LeaveAMessageLit = class extends LitElement {
2417
2552
  _persistStore() {
2418
2553
  return this.runtime?.state?.user?.ns?.(PERSIST_NAMESPACE);
2419
2554
  }
2420
- /** Hydrate {state, operatorMessages, email} from a prior page's mount. */
2555
+ /** Hydrate {state, transcript, email} from a prior page's mount. */
2421
2556
  _loadPersisted() {
2422
2557
  const store = this._persistStore();
2423
2558
  if (!store) return;
2424
2559
  const saved = store.get(PERSIST_KEY);
2425
2560
  if (!saved) return;
2426
2561
  if (saved.state) this.state = saved.state;
2427
- if (Array.isArray(saved.operatorMessages)) this._operatorMessages = saved.operatorMessages;
2562
+ if (Array.isArray(saved.transcript)) this._transcript = saved.transcript;
2428
2563
  if (typeof saved.email === "string") this._email = saved.email;
2429
2564
  }
2430
- /** Snapshot {state, operatorMessages, email} so the NEXT mount (a fresh
2431
- * page in the same session) can pick the thread back up. */
2565
+ /** Snapshot {state, transcript, email} so the NEXT mount (a fresh page
2566
+ * in the same session) can pick the thread back up. */
2432
2567
  _persist() {
2433
2568
  const store = this._persistStore();
2434
2569
  if (!store) return;
2435
2570
  const state = this.state;
2436
- const operatorMessages = this._operatorMessages;
2571
+ const transcript = this._transcript;
2437
2572
  const email = this._email;
2438
- const snapshot = { state, operatorMessages, email };
2573
+ const snapshot = { state, transcript, email };
2439
2574
  store.set(PERSIST_KEY, snapshot);
2440
2575
  }
2441
2576
  /**
@@ -2467,71 +2602,164 @@ var LeaveAMessageLit = class extends LitElement {
2467
2602
  console.warn("supportChat.widget: reply toast failed", err);
2468
2603
  }
2469
2604
  }
2605
+ /** Maps this card's OWN `_transcript` (never the concierge's) to the
2606
+ * shape `<adaptive-chat-trail>` renders. `visitor` → `'user'` (right-
2607
+ * aligned, branded) and `operator` → `'assistant'` (left-aligned,
2608
+ * neutral) purely for the trail's existing visual treatment — no
2609
+ * claim that a human Crisp operator IS the AI assistant. Freshly
2610
+ * computed on every render from local state; nothing here reads from
2611
+ * or writes to `chatSession`. */
2612
+ _trailMessages() {
2613
+ return this._transcript.map((entry, i) => ({
2614
+ id: `lam-${this._uid}-${i}`,
2615
+ role: entry.role === "visitor" ? "user" : "assistant",
2616
+ text: entry.text,
2617
+ status: "complete"
2618
+ }));
2619
+ }
2470
2620
  render() {
2471
2621
  if (!this.bridge || !this.bridge.detect()) return nothing;
2622
+ const copy = this.copy;
2472
2623
  if (this.state === "form") {
2624
+ const messageId = `${this._uid}-message`;
2625
+ const canSend = this.message.trim().length > 0;
2473
2626
  return html`<div data-lam style=${styleMap(box)}>
2474
- <input
2475
- data-lam-email
2476
- type="email"
2477
- placeholder="Your email — so the team can reach you"
2478
- style=${styleMap(fieldStyles)}
2479
- @focus=${this._focusOn}
2480
- @blur=${this._focusOff}
2481
- @input=${(e) => {
2627
+ ${copy?.framing ? html`<p data-lam-framing style=${styleMap(mutedTextStyles)}>${copy.framing}</p>` : nothing}
2628
+ ${copy?.messageLabel ? html`<label data-lam-message-label for=${messageId} style=${styleMap(srOnlyStyles)}
2629
+ >${copy.messageLabel}</label
2630
+ >` : nothing}
2631
+ <div data-lam-message-wrap data-lam-field-wrap style=${styleMap(messageWrapStyles)}>
2632
+ <textarea
2633
+ data-lam-message
2634
+ id=${messageId}
2635
+ style=${styleMap(messageInnerStyles)}
2636
+ @focus=${this._focusOn}
2637
+ @blur=${this._focusOff}
2638
+ .value=${this.message}
2639
+ @input=${(e) => {
2640
+ this.message = e.target.value;
2641
+ }}
2642
+ ></textarea>
2643
+ ${copy?.sendLabel ? html`<button
2644
+ type="button"
2645
+ data-lam-send
2646
+ aria-label=${copy.sendLabel}
2647
+ ?disabled=${!canSend}
2648
+ style=${styleMap(
2649
+ canSend ? embeddedIconButtonStyles : embeddedIconButtonDisabledStyles
2650
+ )}
2651
+ @click=${this._submitMessage}
2652
+ >
2653
+ ${sendIcon}
2654
+ </button>` : nothing}
2655
+ </div>
2656
+ </div>`;
2657
+ }
2658
+ if (this.state === "email") {
2659
+ const emailId = `${this._uid}-email`;
2660
+ return html`<div data-lam style=${styleMap(box)}>
2661
+ ${copy?.emailPrompt ? html`<p data-lam-email-prompt style=${styleMap(mutedTextStyles)}>${copy.emailPrompt}</p>` : nothing}
2662
+ ${copy?.emailLabel ? html`<label data-lam-email-label for=${emailId} style=${styleMap(srOnlyStyles)}
2663
+ >${copy.emailLabel}</label
2664
+ >` : nothing}
2665
+ <div data-lam-email-row style=${styleMap(fieldRowStyles)}>
2666
+ <div
2667
+ data-lam-email-wrap
2668
+ data-lam-field-wrap
2669
+ style=${styleMap({ ...singleLineWrapStyles, flex: "1" })}
2670
+ >
2671
+ <input
2672
+ data-lam-email
2673
+ id=${emailId}
2674
+ type="email"
2675
+ placeholder=${copy?.emailPlaceholder ?? nothing}
2676
+ style=${styleMap(singleLineInnerStyles)}
2677
+ @focus=${this._focusOn}
2678
+ @blur=${this._focusOff}
2679
+ @input=${(e) => {
2482
2680
  this._email = e.target.value;
2483
2681
  this.requestUpdate();
2484
2682
  }}
2485
- />
2486
- <textarea
2487
- data-lam-message
2488
- rows="4"
2489
- style=${styleMap(fieldStyles)}
2490
- @focus=${this._focusOn}
2491
- @blur=${this._focusOff}
2492
- .value=${this.message}
2493
- @input=${(e) => {
2494
- this.message = e.target.value;
2495
- }}
2496
- ></textarea>
2497
- <button type="button" data-lam-send style=${styleMap(buttonStyles)} @click=${this._submit}>
2498
- Send to the team
2499
- </button>
2683
+ />
2684
+ ${copy?.emailSubmitLabel ? html`<button
2685
+ type="button"
2686
+ data-lam-email-submit
2687
+ aria-label=${copy.emailSubmitLabel}
2688
+ style=${styleMap(embeddedIconButtonCenteredStyles)}
2689
+ @click=${this._submitEmail}
2690
+ >
2691
+ ${sendIcon}
2692
+ </button>` : nothing}
2693
+ </div>
2694
+ ${copy?.emailSkipLabel ? html`<button
2695
+ type="button"
2696
+ data-lam-email-skip
2697
+ style=${styleMap(skipButtonStyles)}
2698
+ @click=${this._skipEmail}
2699
+ >
2700
+ ${copy.emailSkipLabel}
2701
+ </button>` : nothing}
2702
+ </div>
2500
2703
  </div>`;
2501
2704
  }
2502
2705
  if (this.state === "sent") {
2503
2706
  return html`<div data-lam style=${styleMap(box)}>
2504
- <p style=${styleMap(mutedTextStyles)}>
2505
- Sent. The team will reply here — or by email if you step away.
2506
- </p>
2707
+ ${copy?.sentBody ? html`<p style=${styleMap(mutedTextStyles)}>${copy.sentBody}</p>` : nothing}
2507
2708
  </div>`;
2508
2709
  }
2509
2710
  if (this.state === "mail") {
2510
2711
  return html`<div data-lam style=${styleMap(box)}>
2511
- <button type="button" data-lam-mail style=${styleMap(buttonStyles)} @click=${this._flip}>
2512
- 📬 You got mail! — tap to read
2513
- </button>
2712
+ ${copy?.mailLabel ? html`<button
2713
+ type="button"
2714
+ data-lam-mail
2715
+ style=${styleMap(buttonStyles)}
2716
+ @click=${this._flip}
2717
+ >
2718
+ ${copy.mailLabel}
2719
+ </button>` : nothing}
2514
2720
  </div>`;
2515
2721
  }
2722
+ const replyId = `${this._uid}-reply`;
2723
+ const canReply = this._reply.trim().length > 0;
2516
2724
  return html`<div data-lam style=${styleMap(box)}>
2517
- ${this._operatorMessages.map(
2518
- (m) => html`<p data-lam-operator style=${styleMap(textStyles)}>${m}</p>`
2519
- )}
2520
- <input
2521
- data-lam-reply
2522
- type="text"
2523
- placeholder="Reply to the team…"
2524
- style=${styleMap(fieldStyles)}
2525
- @focus=${this._focusOn}
2526
- @blur=${this._focusOff}
2527
- .value=${this._reply}
2528
- @input=${(e) => {
2725
+ <adaptive-chat-trail
2726
+ data-lam-trail
2727
+ .messages=${this._trailMessages()}
2728
+ .forceExpanded=${true}
2729
+ .hideTopBorder=${true}
2730
+ style=${styleMap(trailHostStyles)}
2731
+ ></adaptive-chat-trail>
2732
+ ${copy?.replyLabel ? html`<label data-lam-reply-label for=${replyId} style=${styleMap(srOnlyStyles)}
2733
+ >${copy.replyLabel}</label
2734
+ >` : nothing}
2735
+ <div data-lam-reply-wrap data-lam-field-wrap style=${styleMap(singleLineWrapStyles)}>
2736
+ <input
2737
+ data-lam-reply
2738
+ id=${replyId}
2739
+ type="text"
2740
+ placeholder=${copy?.replyPlaceholder ?? nothing}
2741
+ style=${styleMap(singleLineInnerStyles)}
2742
+ @focus=${this._focusOn}
2743
+ @blur=${this._focusOff}
2744
+ .value=${this._reply}
2745
+ @input=${(e) => {
2529
2746
  this._reply = e.target.value;
2747
+ this.requestUpdate();
2530
2748
  }}
2531
- />
2532
- <button type="button" data-lam-reply-send style=${styleMap(buttonStyles)} @click=${this._sendReply}>
2533
- Send
2534
- </button>
2749
+ />
2750
+ ${copy?.replySendLabel ? html`<button
2751
+ type="button"
2752
+ data-lam-reply-send
2753
+ aria-label=${copy.replySendLabel}
2754
+ ?disabled=${!canReply}
2755
+ style=${styleMap(
2756
+ canReply ? embeddedIconButtonCenteredStyles : embeddedIconButtonCenteredDisabledStyles
2757
+ )}
2758
+ @click=${this._sendReply}
2759
+ >
2760
+ ${sendIcon}
2761
+ </button>` : nothing}
2762
+ </div>
2535
2763
  </div>`;
2536
2764
  }
2537
2765
  };
@@ -2541,7 +2769,8 @@ LeaveAMessageLit.properties = {
2541
2769
  contextSummary: { attribute: false },
2542
2770
  state: { type: String },
2543
2771
  runtime: { attribute: false },
2544
- replyToast: { attribute: false }
2772
+ replyToast: { attribute: false },
2773
+ copy: { attribute: false }
2545
2774
  };
2546
2775
  function registerLeaveAMessageLit() {
2547
2776
  if (typeof customElements !== "undefined" && !customElements.get("syntro-leave-a-message")) {
@@ -2693,6 +2922,11 @@ var LeaveAMessageMountable = {
2693
2922
  // Visitor-facing toast copy from the workspace config — see
2694
2923
  // LeaveAMessageLit#replyToast (presentation is configuration).
2695
2924
  replyToast: stripped.replyToast,
2925
+ // Every OTHER visitor-facing string on the card — see
2926
+ // LeaveAMessageLit#copy / LeaveAMessageCopy. Same "config carries the
2927
+ // words" pattern as replyToast, just covering the whole card instead
2928
+ // of one notification.
2929
+ copy: stripped.copy,
2696
2930
  runtime: runtime2
2697
2931
  });
2698
2932
  container.appendChild(el);