@mushi-mushi/web 1.6.0 → 1.7.1

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