@mushi-mushi/web 1.7.8 → 1.9.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
@@ -263,6 +263,44 @@ on every hide path.
263
263
  > sit at a certain z-index. Use `hideOnSelector` or `Mushi.hide()` for the safest
264
264
  > experience in native shells.
265
265
 
266
+ ### Rich banner layout (1.8+)
267
+
268
+ With `trigger: 'banner'`, setting `bannerConfig.message` switches the strip from
269
+ button-only CTAs to the rich layout used by the Mushi admin console's beta
270
+ banner — a pill label, body copy, and flat text actions:
271
+
272
+ ```typescript
273
+ Mushi.init({
274
+ projectId: 'proj_xxx',
275
+ apiKey: 'mushi_xxx',
276
+ widget: {
277
+ trigger: 'banner',
278
+ bannerConfig: {
279
+ variant: 'brand', // 'neon' | 'brand' | 'subtle'
280
+ position: 'top', // 'top' | 'bottom'
281
+ message: 'Mushi is in beta — spotted something off? Tell us.',
282
+ label: 'Beta', // pill before the message; `false` hides it
283
+ bugCta: '🐛 Report a bug',
284
+ featureCta: true,
285
+ links: [
286
+ { label: 'My submissions', href: 'https://app.example.com/feedback' },
287
+ { label: 'Request a feature', featureRequest: true }, // opens widget in feature mode
288
+ ],
289
+ },
290
+ },
291
+ });
292
+ ```
293
+
294
+ - All banner text renders via `textContent` — never HTML — so server- or
295
+ CMS-sourced copy can't inject markup.
296
+ - `links[].href` opens in a new tab with `rel="noopener noreferrer"`; omit
297
+ `href` and set `featureRequest: true` to open the widget instead.
298
+ - `message` and `label` can also be driven remotely per-project from the
299
+ dashboard runtime config (`bannerMessage` / `bannerLabel`), so you can change
300
+ the copy without a deploy. `links` are local-config only.
301
+ - Without `message`, the banner keeps the original compact button-only layout —
302
+ existing integrations are unchanged.
303
+
266
304
  ### Presets and widget anchor
267
305
 
268
306
  ```typescript
@@ -462,4 +500,4 @@ MIT
462
500
  <!-- mushi-readme-stats-footer -->
463
501
  ---
464
502
 
465
- <sub>Monorepo scale (June 2026): 43 edge functions · 233 SQL migrations · 13 outbound plugins · 11 inbound adapters. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
503
+ <sub>Monorepo scale (June 2026): 43 edge functions · 234 SQL migrations · 13 outbound plugins · 11 inbound adapters. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
package/dist/index.cjs CHANGED
@@ -1512,6 +1512,106 @@ function getWidgetStyles(theme) {
1512
1512
  text-overflow: ellipsis;
1513
1513
  }
1514
1514
 
1515
+ /* Rich layout \u2014 pill + message + flat text actions (admin BetaBanner parity) */
1516
+ .mushi-banner--rich {
1517
+ justify-content: space-between;
1518
+ gap: 12px;
1519
+ min-height: 36px;
1520
+ height: auto;
1521
+ padding: 4px 12px 4px 16px;
1522
+ white-space: normal;
1523
+ }
1524
+ .mushi-banner-body {
1525
+ display: flex;
1526
+ align-items: center;
1527
+ gap: 8px;
1528
+ flex: 1;
1529
+ min-width: 0;
1530
+ overflow: hidden;
1531
+ }
1532
+ .mushi-banner-pill {
1533
+ display: inline-flex;
1534
+ flex-shrink: 0;
1535
+ align-items: center;
1536
+ padding: 1px 6px;
1537
+ border-radius: 3px;
1538
+ border: 1px solid currentColor;
1539
+ font-size: 10px;
1540
+ font-weight: 700;
1541
+ letter-spacing: 0.18em;
1542
+ text-transform: uppercase;
1543
+ opacity: 0.92;
1544
+ }
1545
+ .mushi-banner.neon .mushi-banner-pill {
1546
+ border-color: rgba(10,26,10,0.45);
1547
+ background: rgba(10,26,10,0.12);
1548
+ }
1549
+ .mushi-banner.brand .mushi-banner-pill {
1550
+ border-color: rgba(255,255,255,0.45);
1551
+ background: rgba(255,255,255,0.14);
1552
+ }
1553
+ .mushi-banner.subtle .mushi-banner-pill {
1554
+ border-color: ${ruleStrong};
1555
+ background: ${isDark ? "rgba(242,235,221,0.08)" : "rgba(14,13,11,0.06)"};
1556
+ }
1557
+ .mushi-banner-message {
1558
+ min-width: 0;
1559
+ overflow: hidden;
1560
+ text-overflow: ellipsis;
1561
+ white-space: nowrap;
1562
+ font-size: 11.5px;
1563
+ font-weight: 500;
1564
+ line-height: 1.3;
1565
+ opacity: 0.9;
1566
+ }
1567
+ .mushi-banner-actions {
1568
+ display: inline-flex;
1569
+ align-items: center;
1570
+ gap: 0;
1571
+ /* Shrinkable + swipe-scrollable so a long action row can never push
1572
+ past the viewport edge (dismiss sits outside this nav). */
1573
+ flex-shrink: 1;
1574
+ min-width: 0;
1575
+ overflow-x: auto;
1576
+ scrollbar-width: none;
1577
+ font-size: 11px;
1578
+ }
1579
+ .mushi-banner-actions::-webkit-scrollbar { display: none; }
1580
+ @media (max-width: 480px) {
1581
+ /* Phones: keep only the primary bug CTA (+ dismiss outside the nav). */
1582
+ .mushi-banner-actions .mushi-banner-extra { display: none; }
1583
+ }
1584
+ .mushi-banner-link {
1585
+ display: inline-flex;
1586
+ align-items: center;
1587
+ padding: 2px 8px;
1588
+ border: none;
1589
+ background: transparent;
1590
+ color: inherit;
1591
+ cursor: pointer;
1592
+ font: inherit;
1593
+ letter-spacing: inherit;
1594
+ text-decoration: none;
1595
+ opacity: 0.88;
1596
+ transition: opacity 0.15s ease;
1597
+ flex-shrink: 0;
1598
+ }
1599
+ .mushi-banner-link:hover { opacity: 1; }
1600
+ .mushi-banner-link:focus-visible {
1601
+ outline: 2px solid ${widgetAccent};
1602
+ outline-offset: 2px;
1603
+ border-radius: 2px;
1604
+ }
1605
+ .mushi-banner-divider {
1606
+ opacity: 0.28;
1607
+ padding: 0 1px;
1608
+ user-select: none;
1609
+ flex-shrink: 0;
1610
+ }
1611
+ .mushi-banner--rich .mushi-banner-dismiss {
1612
+ margin-left: 4px;
1613
+ }
1614
+
1515
1615
  .mushi-banner-btn {
1516
1616
  display: inline-flex;
1517
1617
  align-items: center;
@@ -1754,7 +1854,12 @@ var MushiWidget = class _MushiWidget {
1754
1854
  ...config.responseSlaLabel !== void 0 ? { responseSlaLabel: config.responseSlaLabel } : {},
1755
1855
  ...config.featureRequestCard !== void 0 ? { featureRequestCard: config.featureRequestCard } : {},
1756
1856
  ...config.featureRequestLabel !== void 0 ? { featureRequestLabel: config.featureRequestLabel } : {},
1757
- ...config.featureRequestDescription !== void 0 ? { featureRequestDescription: config.featureRequestDescription } : {}
1857
+ ...config.featureRequestDescription !== void 0 ? { featureRequestDescription: config.featureRequestDescription } : {},
1858
+ // Runtime/dashboard config delivers bannerMessage/bannerLabel via
1859
+ // mergeRuntimeConfig → bannerConfig. The widget is constructed before
1860
+ // that fetch resolves, so this pass-through is what makes server-driven
1861
+ // banner copy actually render.
1862
+ ...config.bannerConfig !== void 0 ? { bannerConfig: config.bannerConfig } : {}
1758
1863
  };
1759
1864
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
1760
1865
  if (this.host.isConnected) this.syncHostChromeState();
@@ -2139,18 +2244,16 @@ var MushiWidget = class _MushiWidget {
2139
2244
  const bc = this.config.bannerConfig ?? {};
2140
2245
  const variant = bc.variant ?? "brand";
2141
2246
  const position = bc.position ?? "top";
2247
+ const message = bc.message?.trim() ?? "";
2248
+ const richLayout = message.length > 0;
2142
2249
  const bugLabel = bc.bugCta ?? "\u{1F41B} Report a bug";
2143
2250
  const showFeat = bc.featureCta !== false;
2144
2251
  const featLabel = bc.featureCtaLabel ?? "\u2728 Request feature";
2145
2252
  const zIdx = bc.zIndex ?? (this.config.zIndex ?? 99999) - 1;
2146
2253
  const banner = document.createElement("div");
2147
- banner.className = `mushi-banner ${variant} ${position}`;
2254
+ banner.className = `mushi-banner ${variant} ${position}${richLayout ? " mushi-banner--rich" : ""}`;
2148
2255
  banner.style.setProperty("--mushi-banner-z", String(zIdx));
2149
2256
  banner.setAttribute("role", "banner");
2150
- const bugBtn = document.createElement("button");
2151
- bugBtn.className = "mushi-banner-btn";
2152
- bugBtn.textContent = bugLabel;
2153
- bugBtn.addEventListener("click", () => this.open());
2154
2257
  const dismissBtn = document.createElement("button");
2155
2258
  dismissBtn.className = "mushi-banner-dismiss";
2156
2259
  dismissBtn.textContent = "\u2715";
@@ -2160,15 +2263,81 @@ var MushiWidget = class _MushiWidget {
2160
2263
  this.removeBodyNudge();
2161
2264
  this.render();
2162
2265
  });
2163
- banner.appendChild(bugBtn);
2164
- if (showFeat) {
2165
- const featBtn = document.createElement("button");
2166
- featBtn.className = "mushi-banner-btn";
2167
- featBtn.textContent = featLabel;
2168
- featBtn.addEventListener("click", () => this.open({ featureRequest: true }));
2169
- banner.appendChild(featBtn);
2170
- }
2171
- banner.appendChild(dismissBtn);
2266
+ if (richLayout) {
2267
+ const body = document.createElement("div");
2268
+ body.className = "mushi-banner-body";
2269
+ const labelText = bc.label === false ? null : bc.label ?? "Beta";
2270
+ if (labelText) {
2271
+ const pill = document.createElement("span");
2272
+ pill.className = "mushi-banner-pill";
2273
+ pill.textContent = labelText;
2274
+ body.appendChild(pill);
2275
+ }
2276
+ const msg = document.createElement("span");
2277
+ msg.className = "mushi-banner-message";
2278
+ msg.textContent = message;
2279
+ body.appendChild(msg);
2280
+ banner.appendChild(body);
2281
+ const nav = document.createElement("nav");
2282
+ nav.className = "mushi-banner-actions";
2283
+ nav.setAttribute("aria-label", "Feedback banner actions");
2284
+ const appendDivider = (extra = false) => {
2285
+ const sep = document.createElement("span");
2286
+ sep.className = `mushi-banner-divider${extra ? " mushi-banner-extra" : ""}`;
2287
+ sep.setAttribute("aria-hidden", "true");
2288
+ sep.textContent = "|";
2289
+ nav.appendChild(sep);
2290
+ };
2291
+ const appendAction = (label, onClick, extra = false) => {
2292
+ const btn = document.createElement("button");
2293
+ btn.type = "button";
2294
+ btn.className = `mushi-banner-link${extra ? " mushi-banner-extra" : ""}`;
2295
+ btn.textContent = label;
2296
+ btn.addEventListener("click", onClick);
2297
+ nav.appendChild(btn);
2298
+ };
2299
+ appendAction(bugLabel, () => this.open());
2300
+ if (showFeat) {
2301
+ appendDivider(true);
2302
+ appendAction(featLabel, () => this.open({ featureRequest: true }), true);
2303
+ }
2304
+ for (const link of bc.links ?? []) {
2305
+ const linkLabel = link.label?.trim();
2306
+ if (!linkLabel) continue;
2307
+ const href = link.href && (/^https?:\/\//i.test(link.href) || link.href.startsWith("/")) ? link.href : void 0;
2308
+ appendDivider(true);
2309
+ if (href) {
2310
+ const anchor = document.createElement("a");
2311
+ anchor.className = "mushi-banner-link mushi-banner-extra";
2312
+ anchor.href = href;
2313
+ anchor.textContent = linkLabel;
2314
+ anchor.target = "_blank";
2315
+ anchor.rel = "noopener noreferrer";
2316
+ nav.appendChild(anchor);
2317
+ } else {
2318
+ appendAction(linkLabel, () => {
2319
+ if (link.featureRequest) this.open({ featureRequest: true });
2320
+ else this.open();
2321
+ }, true);
2322
+ }
2323
+ }
2324
+ banner.appendChild(nav);
2325
+ banner.appendChild(dismissBtn);
2326
+ } else {
2327
+ const bugBtn = document.createElement("button");
2328
+ bugBtn.className = "mushi-banner-btn";
2329
+ bugBtn.textContent = bugLabel;
2330
+ bugBtn.addEventListener("click", () => this.open());
2331
+ banner.appendChild(bugBtn);
2332
+ if (showFeat) {
2333
+ const featBtn = document.createElement("button");
2334
+ featBtn.className = "mushi-banner-btn";
2335
+ featBtn.textContent = featLabel;
2336
+ featBtn.addEventListener("click", () => this.open({ featureRequest: true }));
2337
+ banner.appendChild(featBtn);
2338
+ }
2339
+ banner.appendChild(dismissBtn);
2340
+ }
2172
2341
  this.shadow.appendChild(banner);
2173
2342
  this.applyBodyNudge(position);
2174
2343
  }
@@ -3560,6 +3729,29 @@ function readHeader(headers, name) {
3560
3729
 
3561
3730
  // src/capture/network.ts
3562
3731
  var MAX_ENTRIES2 = 30;
3732
+ var TRACEPARENT_VERSION = "00";
3733
+ function generateTraceparent() {
3734
+ const traceBytes = crypto.getRandomValues(new Uint8Array(16));
3735
+ const spanBytes = crypto.getRandomValues(new Uint8Array(8));
3736
+ const toHex = (bytes) => Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3737
+ const traceId = toHex(traceBytes);
3738
+ const spanId = toHex(spanBytes);
3739
+ return {
3740
+ traceparent: `${TRACEPARENT_VERSION}-${traceId}-${spanId}-01`,
3741
+ traceId,
3742
+ spanId
3743
+ };
3744
+ }
3745
+ function matchesCorsUrls(url, corsUrls) {
3746
+ for (const pattern of corsUrls) {
3747
+ if (typeof pattern === "string") {
3748
+ if (url.includes(pattern)) return true;
3749
+ } else {
3750
+ if (pattern.test(url)) return true;
3751
+ }
3752
+ }
3753
+ return false;
3754
+ }
3563
3755
  function createNetworkCapture(options = {}) {
3564
3756
  const entries = [];
3565
3757
  const originalFetch = globalThis.fetch;
@@ -3570,15 +3762,29 @@ function createNetworkCapture(options = {}) {
3570
3762
  const url = getRequestUrl(input);
3571
3763
  const internalKind = getInternalRequestKind(input, init);
3572
3764
  const shouldRecord = !internalKind && !shouldIgnoreMushiUrl(url, activeOptions);
3765
+ let traceId;
3766
+ let patchedInit = init;
3767
+ const tp = activeOptions.tracePropagation;
3768
+ if (shouldRecord && tp?.enabled && tp.corsUrls?.length && matchesCorsUrls(url, tp.corsUrls)) {
3769
+ const { traceparent, traceId: tid } = generateTraceparent();
3770
+ traceId = tid;
3771
+ const existingHeaders = init?.headers ? new Headers(init.headers) : new Headers();
3772
+ existingHeaders.set("traceparent", traceparent);
3773
+ if (activeOptions.sessionId) {
3774
+ existingHeaders.set("x-mushi-session", activeOptions.sessionId);
3775
+ }
3776
+ patchedInit = { ...init, headers: existingHeaders };
3777
+ }
3573
3778
  try {
3574
- const response = await originalFetch.call(globalThis, input, init);
3779
+ const response = await originalFetch.call(globalThis, input, patchedInit);
3575
3780
  if (shouldRecord) {
3576
3781
  addEntry({
3577
3782
  method,
3578
3783
  url: truncateUrl(url),
3579
3784
  status: response.status,
3580
3785
  duration: Date.now() - startTime,
3581
- timestamp: startTime
3786
+ timestamp: startTime,
3787
+ ...traceId ? { traceId } : {}
3582
3788
  });
3583
3789
  }
3584
3790
  return response;
@@ -3590,7 +3796,8 @@ function createNetworkCapture(options = {}) {
3590
3796
  status: 0,
3591
3797
  duration: Date.now() - startTime,
3592
3798
  timestamp: startTime,
3593
- error: error instanceof Error ? error.message : "Network error"
3799
+ error: error instanceof Error ? error.message : "Network error",
3800
+ ...traceId ? { traceId } : {}
3594
3801
  });
3595
3802
  }
3596
3803
  throw error;
@@ -4823,7 +5030,7 @@ function createProactiveManager(config = {}) {
4823
5030
 
4824
5031
  // src/version.ts
4825
5032
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
4826
- var MUSHI_SDK_VERSION = "1.7.7" ;
5033
+ var MUSHI_SDK_VERSION = "1.9.0" ;
4827
5034
 
4828
5035
  // src/mushi.ts
4829
5036
  var instance = null;
@@ -4913,7 +5120,9 @@ function createInstance(config) {
4913
5120
  if (activeConfig.capture?.network !== false) {
4914
5121
  const networkOptions = {
4915
5122
  apiEndpoint: resolveApiEndpoint(activeConfig),
4916
- ignoreUrls: activeConfig.capture?.ignoreUrls
5123
+ ignoreUrls: activeConfig.capture?.ignoreUrls,
5124
+ tracePropagation: activeConfig.capture?.tracePropagation,
5125
+ sessionId: core.getSessionId()
4917
5126
  };
4918
5127
  if (networkCap) {
4919
5128
  networkCap.updateOptions(networkOptions);
@@ -5004,8 +5213,10 @@ function createInstance(config) {
5004
5213
  });
5005
5214
  let detachAutoBreadcrumbs = null;
5006
5215
  detachAutoBreadcrumbs = installAutoBreadcrumbs(breadcrumbs);
5216
+ let screenshotCaptureInFlight = false;
5007
5217
  async function takeScreenshotWithoutChrome() {
5008
- if (!screenshotCap) return null;
5218
+ if (!screenshotCap || screenshotCaptureInFlight) return null;
5219
+ screenshotCaptureInFlight = true;
5009
5220
  const panelWasVisible = widget.getIsOpen();
5010
5221
  if (panelWasVisible) widget.hidePanel();
5011
5222
  const host = document.getElementById("mushi-mushi-widget");
@@ -5017,6 +5228,7 @@ function createInstance(config) {
5017
5228
  try {
5018
5229
  return await screenshotCap.take();
5019
5230
  } finally {
5231
+ screenshotCaptureInFlight = false;
5020
5232
  if (host) host.style.visibility = prevVisibility;
5021
5233
  if (panelWasVisible) widget.showPanel();
5022
5234
  }
@@ -5690,14 +5902,21 @@ function mergeRuntimeConfig(config, runtime) {
5690
5902
  const nativeTrigger = runtime.native?.triggerMode;
5691
5903
  const runtimeLauncher = runtime.widget?.launcher;
5692
5904
  const widgetTrigger = runtimeLauncher ?? runtime.widget?.trigger ?? (nativeTrigger === "none" || nativeTrigger === "shake" ? "manual" : void 0);
5693
- const runtimeBannerVariant = runtime.widget?.bannerVariant;
5694
- const runtimeBannerPosition = runtime.widget?.bannerPosition;
5695
- const runtimeBannerBugCta = runtime.widget?.bannerBugCta;
5696
- const runtimeBannerFeatureCta = runtime.widget?.bannerFeatureCta;
5697
- const derivedBannerConfig = runtimeBannerVariant || runtimeBannerPosition || runtimeBannerBugCta != null || runtimeBannerFeatureCta != null ? {
5905
+ const runtimeWidget = runtime.widget;
5906
+ const runtimeBannerVariant = runtimeWidget?.bannerVariant;
5907
+ const runtimeBannerPosition = runtimeWidget?.bannerPosition;
5908
+ const runtimeBannerMessage = runtimeWidget?.bannerMessage;
5909
+ const runtimeBannerLabel = runtimeWidget?.bannerLabel;
5910
+ const runtimeBannerBugCta = runtimeWidget?.bannerBugCta;
5911
+ const runtimeBannerFeatureCta = runtimeWidget?.bannerFeatureCta;
5912
+ const derivedBannerConfig = runtimeBannerVariant || runtimeBannerPosition || runtimeBannerMessage != null || runtimeBannerLabel != null || runtimeBannerBugCta != null || runtimeBannerFeatureCta != null ? {
5698
5913
  ...config.widget?.bannerConfig ?? {},
5699
5914
  ...runtimeBannerVariant ? { variant: runtimeBannerVariant } : {},
5700
5915
  ...runtimeBannerPosition ? { position: runtimeBannerPosition } : {},
5916
+ ...runtimeBannerMessage != null ? { message: runtimeBannerMessage } : {},
5917
+ // Dashboard sends an empty string to hide the pill (the runtime
5918
+ // payload has no way to express the local-config `label: false`).
5919
+ ...runtimeBannerLabel != null ? { label: runtimeBannerLabel === "" ? false : runtimeBannerLabel } : {},
5701
5920
  ...runtimeBannerBugCta != null ? { bugCta: runtimeBannerBugCta ?? void 0 } : {},
5702
5921
  ...runtimeBannerFeatureCta != null ? { featureCta: runtimeBannerFeatureCta } : {}
5703
5922
  } : void 0;