@mushi-mushi/web 1.6.0 → 1.7.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
@@ -132,6 +132,8 @@ declare class MushiWidget {
132
132
  * Drives a different success copy so the user knows the report
133
133
  * hasn't actually reached the console yet. */
134
134
  private lastSubmitQueuedOffline;
135
+ /** Whether the user has clicked ✕ on the header banner this session. */
136
+ private bannerDismissed;
135
137
  constructor(config: MushiWidgetConfig | undefined, callbacks: WidgetCallbacks, sdkVersion?: string);
136
138
  mount(): void;
137
139
  getIsMounted(): boolean;
@@ -180,6 +182,13 @@ declare class MushiWidget {
180
182
  private syncAttachedLaunchers;
181
183
  private syncSmartHide;
182
184
  private shouldRenderTrigger;
185
+ /** Height of the banner in px — kept in sync with the CSS `.mushi-banner` height (36px). */
186
+ private static readonly BANNER_HEIGHT;
187
+ /** CSS property applied to document.body so host-app content doesn't slide under the banner. */
188
+ private static readonly BODY_NUDGE_PROP;
189
+ private applyBodyNudge;
190
+ private removeBodyNudge;
191
+ private renderBanner;
183
192
  private effectiveTrigger;
184
193
  private isMobileSmartHidden;
185
194
  private detectEnvironment;
package/dist/index.d.ts CHANGED
@@ -132,6 +132,8 @@ declare class MushiWidget {
132
132
  * Drives a different success copy so the user knows the report
133
133
  * hasn't actually reached the console yet. */
134
134
  private lastSubmitQueuedOffline;
135
+ /** Whether the user has clicked ✕ on the header banner this session. */
136
+ private bannerDismissed;
135
137
  constructor(config: MushiWidgetConfig | undefined, callbacks: WidgetCallbacks, sdkVersion?: string);
136
138
  mount(): void;
137
139
  getIsMounted(): boolean;
@@ -180,6 +182,13 @@ declare class MushiWidget {
180
182
  private syncAttachedLaunchers;
181
183
  private syncSmartHide;
182
184
  private shouldRenderTrigger;
185
+ /** Height of the banner in px — kept in sync with the CSS `.mushi-banner` height (36px). */
186
+ private static readonly BANNER_HEIGHT;
187
+ /** CSS property applied to document.body so host-app content doesn't slide under the banner. */
188
+ private static readonly BODY_NUDGE_PROP;
189
+ private applyBodyNudge;
190
+ private removeBodyNudge;
191
+ private renderBanner;
183
192
  private effectiveTrigger;
184
193
  private isMobileSmartHidden;
185
194
  private detectEnvironment;
package/dist/index.js CHANGED
@@ -1415,6 +1415,145 @@ function getWidgetStyles(theme) {
1415
1415
  font-size: 10.5px;
1416
1416
  }
1417
1417
 
1418
+ /* \u2500\u2500\u2500 Banner launcher (trigger: 'banner') \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1419
+
1420
+ .mushi-banner {
1421
+ position: fixed;
1422
+ left: 0;
1423
+ right: 0;
1424
+ height: 36px;
1425
+ display: flex;
1426
+ align-items: center;
1427
+ justify-content: center;
1428
+ gap: 10px;
1429
+ padding: 0 16px;
1430
+ font-family: ${fontMono};
1431
+ font-size: 11.5px;
1432
+ letter-spacing: 0.04em;
1433
+ white-space: nowrap;
1434
+ overflow: hidden;
1435
+ z-index: var(--mushi-banner-z, 99998);
1436
+ animation: mushi-banner-slide-in 0.3s ${easeStamp} both;
1437
+ }
1438
+
1439
+ .mushi-banner.top { top: 0; }
1440
+ .mushi-banner.bottom { bottom: 0; }
1441
+
1442
+ /* --- neon variant (electric lime \u2014 dev / beta tool aesthetic) --- */
1443
+ .mushi-banner.neon {
1444
+ background: #0FFF50;
1445
+ color: #0a1a0a;
1446
+ border-bottom: 1.5px solid #00C43A;
1447
+ }
1448
+ .mushi-banner.neon.bottom {
1449
+ border-top: 1.5px solid #00C43A;
1450
+ border-bottom: none;
1451
+ }
1452
+ .mushi-banner.neon .mushi-banner-btn {
1453
+ background: rgba(0,0,0,0.14);
1454
+ color: #0a1a0a;
1455
+ border: 1px solid rgba(0,0,0,0.22);
1456
+ }
1457
+ .mushi-banner.neon .mushi-banner-btn:hover {
1458
+ background: rgba(0,0,0,0.22);
1459
+ }
1460
+
1461
+ /* --- brand variant (vermillion \u2014 editorial, app-quality) --- */
1462
+ .mushi-banner.brand {
1463
+ background: ${vermillion};
1464
+ color: #fff;
1465
+ border-bottom: 1.5px solid ${isDark ? "#C4321E" : "#B52F1F"};
1466
+ }
1467
+ .mushi-banner.brand.bottom {
1468
+ border-top: 1.5px solid ${isDark ? "#C4321E" : "#B52F1F"};
1469
+ border-bottom: none;
1470
+ }
1471
+ .mushi-banner.brand .mushi-banner-btn {
1472
+ background: rgba(255,255,255,0.18);
1473
+ color: #fff;
1474
+ border: 1px solid rgba(255,255,255,0.32);
1475
+ }
1476
+ .mushi-banner.brand .mushi-banner-btn:hover {
1477
+ background: rgba(255,255,255,0.28);
1478
+ }
1479
+
1480
+ /* --- subtle variant (hairline, muted \u2014 least disruptive) --- */
1481
+ .mushi-banner.subtle {
1482
+ background: ${isDark ? "rgba(242,235,221,0.06)" : "rgba(14,13,11,0.04)"};
1483
+ color: ${inkMuted};
1484
+ border-bottom: 1px solid ${rule};
1485
+ }
1486
+ .mushi-banner.subtle.bottom {
1487
+ border-top: 1px solid ${rule};
1488
+ border-bottom: none;
1489
+ }
1490
+ .mushi-banner.subtle .mushi-banner-btn {
1491
+ background: ${isDark ? "rgba(242,235,221,0.10)" : "rgba(14,13,11,0.07)"};
1492
+ color: ${ink};
1493
+ border: 1px solid ${rule};
1494
+ }
1495
+ .mushi-banner.subtle .mushi-banner-btn:hover {
1496
+ background: ${isDark ? "rgba(242,235,221,0.16)" : "rgba(14,13,11,0.12)"};
1497
+ }
1498
+
1499
+ .mushi-banner-label {
1500
+ flex: 1;
1501
+ text-align: center;
1502
+ overflow: hidden;
1503
+ text-overflow: ellipsis;
1504
+ }
1505
+
1506
+ .mushi-banner-btn {
1507
+ display: inline-flex;
1508
+ align-items: center;
1509
+ gap: 4px;
1510
+ padding: 3px 10px;
1511
+ border-radius: 3px;
1512
+ cursor: pointer;
1513
+ font: inherit;
1514
+ letter-spacing: inherit;
1515
+ transition: background 0.15s ease, opacity 0.15s ease;
1516
+ flex-shrink: 0;
1517
+ height: 24px;
1518
+ line-height: 1;
1519
+ }
1520
+ .mushi-banner-btn:focus-visible {
1521
+ outline: 2px solid ${vermillion};
1522
+ outline-offset: 2px;
1523
+ }
1524
+
1525
+ .mushi-banner-dismiss {
1526
+ background: transparent !important;
1527
+ border: none !important;
1528
+ opacity: 0.65;
1529
+ cursor: pointer;
1530
+ font-size: 14px;
1531
+ line-height: 1;
1532
+ padding: 4px 8px;
1533
+ margin-left: auto;
1534
+ flex-shrink: 0;
1535
+ color: inherit;
1536
+ border-radius: 3px;
1537
+ transition: opacity 0.15s, background 0.15s;
1538
+ }
1539
+ .mushi-banner-dismiss:hover {
1540
+ opacity: 1;
1541
+ background: rgba(0,0,0,0.12) !important;
1542
+ }
1543
+ .mushi-banner.neon .mushi-banner-dismiss:hover { background: rgba(0,0,0,0.18) !important; }
1544
+
1545
+ @keyframes mushi-banner-slide-in {
1546
+ from { transform: translateY(calc(-1 * 100%)); opacity: 0.5; }
1547
+ to { transform: translateY(0); opacity: 1; }
1548
+ }
1549
+ .mushi-banner.bottom {
1550
+ animation-name: mushi-banner-slide-in-bottom;
1551
+ }
1552
+ @keyframes mushi-banner-slide-in-bottom {
1553
+ from { transform: translateY(100%); opacity: 0.5; }
1554
+ to { transform: translateY(0); opacity: 1; }
1555
+ }
1556
+
1418
1557
  @media (prefers-reduced-motion: reduce) {
1419
1558
  *,
1420
1559
  *::before,
@@ -1453,7 +1592,7 @@ function isSubmitShortcut(e) {
1453
1592
  function escapeHtml(value) {
1454
1593
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1455
1594
  }
1456
- var MushiWidget = class {
1595
+ var MushiWidget = class _MushiWidget {
1457
1596
  constructor(config = {}, callbacks, sdkVersion = "0.7.0") {
1458
1597
  this.sdkVersion = sdkVersion;
1459
1598
  this.config = {
@@ -1473,6 +1612,7 @@ var MushiWidget = class {
1473
1612
  locale: config.locale ?? "auto",
1474
1613
  zIndex: config.zIndex ?? 99999,
1475
1614
  trigger: config.trigger ?? "auto",
1615
+ bannerConfig: config.bannerConfig ?? {},
1476
1616
  attachToSelector: config.attachToSelector ?? "",
1477
1617
  inset: config.inset ?? {},
1478
1618
  respectSafeArea: config.respectSafeArea ?? true,
@@ -1562,6 +1702,8 @@ var MushiWidget = class {
1562
1702
  * Drives a different success copy so the user knows the report
1563
1703
  * hasn't actually reached the console yet. */
1564
1704
  lastSubmitQueuedOffline = false;
1705
+ /** Whether the user has clicked ✕ on the header banner this session. */
1706
+ bannerDismissed = false;
1565
1707
  mount() {
1566
1708
  if (this.host.isConnected) return;
1567
1709
  document.body.appendChild(this.host);
@@ -1870,7 +2012,7 @@ var MushiWidget = class {
1870
2012
  shouldRenderTrigger() {
1871
2013
  if (!this.triggerVisible) return false;
1872
2014
  if (this.triggerHiddenByScroll) return false;
1873
- if (this.config.trigger === "manual" || this.config.trigger === "hidden" || this.config.trigger === "attach") {
2015
+ if (this.config.trigger === "manual" || this.config.trigger === "hidden" || this.config.trigger === "attach" || this.config.trigger === "banner") {
1874
2016
  return false;
1875
2017
  }
1876
2018
  if (this.isMobileSmartHidden()) return false;
@@ -1879,6 +2021,81 @@ var MushiWidget = class {
1879
2021
  const action = this.config.environments[this.detectEnvironment()];
1880
2022
  return action !== "never" && action !== "manual";
1881
2023
  }
2024
+ /** Height of the banner in px — kept in sync with the CSS `.mushi-banner` height (36px). */
2025
+ static BANNER_HEIGHT = 36;
2026
+ /** CSS property applied to document.body so host-app content doesn't slide under the banner. */
2027
+ static BODY_NUDGE_PROP = "--mushi-banner-offset";
2028
+ applyBodyNudge(position) {
2029
+ const h = `${_MushiWidget.BANNER_HEIGHT}px`;
2030
+ if (position === "top") {
2031
+ document.documentElement.style.setProperty(_MushiWidget.BODY_NUDGE_PROP, h);
2032
+ if (!document.body.style.paddingTop) {
2033
+ document.body.style.paddingTop = h;
2034
+ document.body.dataset.mushiBannerNudged = "top";
2035
+ }
2036
+ } else {
2037
+ document.documentElement.style.setProperty(_MushiWidget.BODY_NUDGE_PROP, h);
2038
+ if (!document.body.style.paddingBottom) {
2039
+ document.body.style.paddingBottom = h;
2040
+ document.body.dataset.mushiBannerNudged = "bottom";
2041
+ }
2042
+ }
2043
+ }
2044
+ removeBodyNudge() {
2045
+ document.documentElement.style.removeProperty(_MushiWidget.BODY_NUDGE_PROP);
2046
+ const nudged = document.body.dataset.mushiBannerNudged;
2047
+ if (nudged === "top") {
2048
+ document.body.style.paddingTop = "";
2049
+ delete document.body.dataset.mushiBannerNudged;
2050
+ } else if (nudged === "bottom") {
2051
+ document.body.style.paddingBottom = "";
2052
+ delete document.body.dataset.mushiBannerNudged;
2053
+ }
2054
+ }
2055
+ renderBanner() {
2056
+ if (this.config.trigger !== "banner") return;
2057
+ if (this.bannerDismissed) {
2058
+ this.removeBodyNudge();
2059
+ return;
2060
+ }
2061
+ if (!this.triggerVisible) return;
2062
+ if (this.isRouteHidden()) return;
2063
+ const bc = this.config.bannerConfig ?? {};
2064
+ const variant = bc.variant ?? "brand";
2065
+ const position = bc.position ?? "top";
2066
+ const bugLabel = bc.bugCta ?? "\u{1F41B} Report a bug";
2067
+ const showFeat = bc.featureCta !== false;
2068
+ const featLabel = bc.featureCtaLabel ?? "\u2728 Request feature";
2069
+ const zIdx = bc.zIndex ?? (this.config.zIndex ?? 99999) - 1;
2070
+ const banner = document.createElement("div");
2071
+ banner.className = `mushi-banner ${variant} ${position}`;
2072
+ banner.style.setProperty("--mushi-banner-z", String(zIdx));
2073
+ banner.setAttribute("role", "banner");
2074
+ const bugBtn = document.createElement("button");
2075
+ bugBtn.className = "mushi-banner-btn";
2076
+ bugBtn.textContent = bugLabel;
2077
+ bugBtn.addEventListener("click", () => this.open());
2078
+ const dismissBtn = document.createElement("button");
2079
+ dismissBtn.className = "mushi-banner-dismiss";
2080
+ dismissBtn.textContent = "\u2715";
2081
+ dismissBtn.setAttribute("aria-label", "Dismiss feedback banner");
2082
+ dismissBtn.addEventListener("click", () => {
2083
+ this.bannerDismissed = true;
2084
+ this.removeBodyNudge();
2085
+ this.render();
2086
+ });
2087
+ banner.appendChild(bugBtn);
2088
+ if (showFeat) {
2089
+ const featBtn = document.createElement("button");
2090
+ featBtn.className = "mushi-banner-btn";
2091
+ featBtn.textContent = featLabel;
2092
+ featBtn.addEventListener("click", () => this.open({ featureRequest: true }));
2093
+ banner.appendChild(featBtn);
2094
+ }
2095
+ banner.appendChild(dismissBtn);
2096
+ this.shadow.appendChild(banner);
2097
+ this.applyBodyNudge(position);
2098
+ }
1882
2099
  effectiveTrigger() {
1883
2100
  if (!this.config.smartHide || typeof window === "undefined") return this.config.trigger;
1884
2101
  const smart = this.config.smartHide === true ? { onMobile: "edge-tab" } : this.config.smartHide;
@@ -1917,6 +2134,7 @@ var MushiWidget = class {
1917
2134
  const style = document.createElement("style");
1918
2135
  style.textContent = getWidgetStyles(theme);
1919
2136
  this.shadow.appendChild(style);
2137
+ this.renderBanner();
1920
2138
  if (this.shouldRenderTrigger()) {
1921
2139
  const effectiveTrigger = this.effectiveTrigger();
1922
2140
  const trigger = document.createElement("button");
@@ -4421,7 +4639,7 @@ function createProactiveManager(config = {}) {
4421
4639
 
4422
4640
  // src/version.ts
4423
4641
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
4424
- var MUSHI_SDK_VERSION = "1.6.0" ;
4642
+ var MUSHI_SDK_VERSION = "1.7.0" ;
4425
4643
 
4426
4644
  // src/mushi.ts
4427
4645
  var instance = null;
@@ -5257,13 +5475,26 @@ function createInstance(config) {
5257
5475
  }
5258
5476
  function mergeRuntimeConfig(config, runtime) {
5259
5477
  const nativeTrigger = runtime.native?.triggerMode;
5260
- const widgetTrigger = runtime.widget?.trigger ?? (nativeTrigger === "none" || nativeTrigger === "shake" ? "manual" : void 0);
5478
+ const runtimeLauncher = runtime.widget?.launcher;
5479
+ const widgetTrigger = runtimeLauncher ?? runtime.widget?.trigger ?? (nativeTrigger === "none" || nativeTrigger === "shake" ? "manual" : void 0);
5480
+ const runtimeBannerVariant = runtime.widget?.bannerVariant;
5481
+ const runtimeBannerPosition = runtime.widget?.bannerPosition;
5482
+ const runtimeBannerBugCta = runtime.widget?.bannerBugCta;
5483
+ const runtimeBannerFeatureCta = runtime.widget?.bannerFeatureCta;
5484
+ const derivedBannerConfig = runtimeBannerVariant || runtimeBannerPosition || runtimeBannerBugCta != null || runtimeBannerFeatureCta != null ? {
5485
+ ...config.widget?.bannerConfig ?? {},
5486
+ ...runtimeBannerVariant ? { variant: runtimeBannerVariant } : {},
5487
+ ...runtimeBannerPosition ? { position: runtimeBannerPosition } : {},
5488
+ ...runtimeBannerBugCta != null ? { bugCta: runtimeBannerBugCta ?? void 0 } : {},
5489
+ ...runtimeBannerFeatureCta != null ? { featureCta: runtimeBannerFeatureCta } : {}
5490
+ } : void 0;
5261
5491
  return {
5262
5492
  ...config,
5263
5493
  widget: {
5264
5494
  ...config.widget,
5265
5495
  ...runtime.widget,
5266
5496
  ...widgetTrigger ? { trigger: widgetTrigger } : {},
5497
+ ...derivedBannerConfig ? { bannerConfig: derivedBannerConfig } : {},
5267
5498
  // betaMode is local-only: set by the host app, not the dashboard.
5268
5499
  // Restore it after the runtime spread so it is never silently cleared.
5269
5500
  ...config.widget?.betaMode ? { betaMode: config.widget.betaMode } : {}