@mushi-mushi/web 1.2.1 → 1.3.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.cjs CHANGED
@@ -47,10 +47,20 @@ var en = {
47
47
  heading: "Tell us more",
48
48
  descriptionPlaceholder: "Describe what happened\u2026",
49
49
  screenshotButton: "Attach Screenshot",
50
- screenshotAttached: "Screenshot attached",
50
+ screenshotAttached: "Screenshot attached \u2713",
51
+ screenshotCapturing: "Taking screenshot\u2026",
52
+ screenshotFailed: "Couldn't capture \u2014 describe it instead",
51
53
  elementButton: "Select Element",
52
- elementSelected: "Element selected",
53
- optional: "(optional)"
54
+ elementSelected: "Element selected \u2713",
55
+ elementCapturing: "Click anything on the page\u2026",
56
+ elementSelectorHint: "Click any element \xB7 Esc to cancel",
57
+ optional: "(optional)",
58
+ tooShort: "A bit more detail helps us fix it faster",
59
+ examplePrompts: [
60
+ "The save button does nothing",
61
+ "Page froze for 10 seconds",
62
+ "Layout looks broken here"
63
+ ]
54
64
  }
55
65
  };
56
66
 
@@ -97,10 +107,20 @@ var ja = {
97
107
  heading: "\u8A73\u7D30\u3092\u6559\u3048\u3066\u304F\u3060\u3055\u3044",
98
108
  descriptionPlaceholder: "\u4F55\u304C\u8D77\u304D\u305F\u304B\u8AAC\u660E\u3057\u3066\u304F\u3060\u3055\u3044\u2026",
99
109
  screenshotButton: "\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u6DFB\u4ED8",
100
- screenshotAttached: "\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u6DFB\u4ED8\u6E08\u307F",
110
+ screenshotAttached: "\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u6DFB\u4ED8\u6E08\u307F \u2713",
111
+ screenshotCapturing: "\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u64AE\u5F71\u4E2D\u2026",
112
+ screenshotFailed: "\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F \u2014 \u6587\u5B57\u3067\u6559\u3048\u3066\u304F\u3060\u3055\u3044",
101
113
  elementButton: "\u8981\u7D20\u3092\u9078\u629E",
102
- elementSelected: "\u8981\u7D20\u9078\u629E\u6E08\u307F",
103
- optional: "\uFF08\u4EFB\u610F\uFF09"
114
+ elementSelected: "\u8981\u7D20\u9078\u629E\u6E08\u307F \u2713",
115
+ elementCapturing: "\u30DA\u30FC\u30B8\u4E0A\u306E\u8981\u7D20\u3092\u30AF\u30EA\u30C3\u30AF\u2026",
116
+ elementSelectorHint: "\u8981\u7D20\u3092\u30AF\u30EA\u30C3\u30AF \xB7 Esc \u3067\u30AD\u30E3\u30F3\u30BB\u30EB",
117
+ optional: "\uFF08\u4EFB\u610F\uFF09",
118
+ tooShort: "\u3082\u3046\u5C11\u3057\u8A73\u3057\u304F\u6559\u3048\u3066\u304F\u3060\u3055\u3044",
119
+ examplePrompts: [
120
+ "\u4FDD\u5B58\u30DC\u30BF\u30F3\u304C\u53CD\u5FDC\u3057\u306A\u3044",
121
+ "\u30DA\u30FC\u30B8\u304C10\u79D2\u56FA\u307E\u3063\u305F",
122
+ "\u30EC\u30A4\u30A2\u30A6\u30C8\u304C\u5D29\u308C\u3066\u3044\u308B"
123
+ ]
104
124
  }
105
125
  };
106
126
 
@@ -147,10 +167,20 @@ var th = {
147
167
  heading: "\u0E1A\u0E2D\u0E01\u0E40\u0E23\u0E32\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E40\u0E15\u0E34\u0E21",
148
168
  descriptionPlaceholder: "\u0E2D\u0E18\u0E34\u0E1A\u0E32\u0E22\u0E2A\u0E34\u0E48\u0E07\u0E17\u0E35\u0E48\u0E40\u0E01\u0E34\u0E14\u0E02\u0E36\u0E49\u0E19\u2026",
149
169
  screenshotButton: "\u0E41\u0E19\u0E1A\u0E2A\u0E01\u0E23\u0E35\u0E19\u0E0A\u0E47\u0E2D\u0E15",
150
- screenshotAttached: "\u0E41\u0E19\u0E1A\u0E2A\u0E01\u0E23\u0E35\u0E19\u0E0A\u0E47\u0E2D\u0E15\u0E41\u0E25\u0E49\u0E27",
170
+ screenshotAttached: "\u0E41\u0E19\u0E1A\u0E2A\u0E01\u0E23\u0E35\u0E19\u0E0A\u0E47\u0E2D\u0E15\u0E41\u0E25\u0E49\u0E27 \u2713",
171
+ screenshotCapturing: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E16\u0E48\u0E32\u0E22\u0E2A\u0E01\u0E23\u0E35\u0E19\u0E0A\u0E47\u0E2D\u0E15\u2026",
172
+ screenshotFailed: "\u0E44\u0E21\u0E48\u0E2A\u0E32\u0E21\u0E32\u0E23\u0E16\u0E16\u0E48\u0E32\u0E22\u0E20\u0E32\u0E1E\u0E44\u0E14\u0E49 \u2014 \u0E42\u0E1B\u0E23\u0E14\u0E2D\u0E18\u0E34\u0E1A\u0E32\u0E22\u0E41\u0E17\u0E19",
151
173
  elementButton: "\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A",
152
- elementSelected: "\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A\u0E41\u0E25\u0E49\u0E27",
153
- optional: "(\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E07\u0E04\u0E31\u0E1A)"
174
+ elementSelected: "\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A\u0E41\u0E25\u0E49\u0E27 \u2713",
175
+ elementCapturing: "\u0E04\u0E25\u0E34\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A\u0E1A\u0E19\u0E2B\u0E19\u0E49\u0E32\u2026",
176
+ elementSelectorHint: "\u0E04\u0E25\u0E34\u0E01\u0E2D\u0E07\u0E04\u0E4C\u0E1B\u0E23\u0E30\u0E01\u0E2D\u0E1A\u0E43\u0E14\u0E01\u0E47\u0E44\u0E14\u0E49 \xB7 Esc \u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E22\u0E01\u0E40\u0E25\u0E34\u0E01",
177
+ optional: "(\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E07\u0E04\u0E31\u0E1A)",
178
+ tooShort: "\u0E01\u0E23\u0E38\u0E13\u0E32\u0E43\u0E2B\u0E49\u0E23\u0E32\u0E22\u0E25\u0E30\u0E40\u0E2D\u0E35\u0E22\u0E14\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E40\u0E15\u0E34\u0E21",
179
+ examplePrompts: [
180
+ "\u0E1B\u0E38\u0E48\u0E21\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E44\u0E21\u0E48\u0E17\u0E33\u0E07\u0E32\u0E19",
181
+ "\u0E2B\u0E19\u0E49\u0E32\u0E04\u0E49\u0E32\u0E07\u0E19\u0E32\u0E19 10 \u0E27\u0E34\u0E19\u0E32\u0E17\u0E35",
182
+ "\u0E40\u0E25\u0E22\u0E4C\u0E40\u0E2D\u0E32\u0E15\u0E4C\u0E1E\u0E31\u0E07"
183
+ ]
154
184
  }
155
185
  };
156
186
 
@@ -197,18 +227,29 @@ var es = {
197
227
  heading: "Cu\xE9ntanos m\xE1s",
198
228
  descriptionPlaceholder: "Describe lo que pas\xF3\u2026",
199
229
  screenshotButton: "Adjuntar captura",
200
- screenshotAttached: "Captura adjunta",
230
+ screenshotAttached: "Captura adjunta \u2713",
231
+ screenshotCapturing: "Tomando captura\u2026",
232
+ screenshotFailed: "No se pudo capturar \u2014 descr\xEDbelo en su lugar",
201
233
  elementButton: "Seleccionar elemento",
202
- elementSelected: "Elemento seleccionado",
203
- optional: "(opcional)"
234
+ elementSelected: "Elemento seleccionado \u2713",
235
+ elementCapturing: "Haz clic en cualquier elemento\u2026",
236
+ elementSelectorHint: "Clic en cualquier elemento \xB7 Esc para cancelar",
237
+ optional: "(opcional)",
238
+ tooShort: "Un poco m\xE1s de detalle nos ayuda a resolverlo",
239
+ examplePrompts: [
240
+ "El bot\xF3n guardar no responde",
241
+ "La p\xE1gina se congel\xF3 10 segundos",
242
+ "El dise\xF1o se ve roto aqu\xED"
243
+ ]
204
244
  }
205
245
  };
206
246
 
207
247
  // src/i18n/index.ts
208
248
  var locales = { en, ja, th, es };
209
249
  function getLocale(code) {
210
- if (!code) return en;
211
- const base = code.split("-")[0].toLowerCase();
250
+ const resolved = code && code !== "auto" ? code : typeof navigator !== "undefined" ? navigator.language ?? navigator.languages?.[0] : void 0;
251
+ if (!resolved) return en;
252
+ const base = resolved.split("-")[0].toLowerCase();
212
253
  return locales[base] ?? en;
213
254
  }
214
255
  function getAvailableLocales() {
@@ -241,6 +282,7 @@ function getWidgetStyles(theme) {
241
282
  -webkit-font-smoothing: antialiased;
242
283
  -moz-osx-font-smoothing: grayscale;
243
284
  font-feature-settings: 'ss01', 'cv11'; /* nicer system-ui glyphs where supported */
285
+ --mushi-ok: ${isDark ? "#4ade80" : "#16a34a"};
244
286
  }
245
287
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
246
288
  button { font-family: inherit; }
@@ -693,6 +735,51 @@ function getWidgetStyles(theme) {
693
735
  box-shadow: inset 2px 0 0 ${vermillion};
694
736
  }
695
737
 
738
+ /* Example starter chips \u2014 reduce first-report activation energy */
739
+ .mushi-example-chips {
740
+ display: flex;
741
+ flex-wrap: wrap;
742
+ gap: 6px;
743
+ margin-bottom: 10px;
744
+ }
745
+ .mushi-example-chip {
746
+ padding: 4px 10px;
747
+ border: 1px solid ${rule};
748
+ border-radius: 12px;
749
+ background: transparent;
750
+ color: ${inkMuted};
751
+ font-family: ${fontBody};
752
+ font-size: 11px;
753
+ cursor: pointer;
754
+ transition: color 150ms ${easeStamp}, border-color 150ms ${easeStamp}, background 150ms ${easeStamp};
755
+ white-space: nowrap;
756
+ }
757
+ .mushi-example-chip:hover {
758
+ color: ${ink};
759
+ border-color: ${inkMuted};
760
+ background: ${isDark ? "rgba(242,235,221,0.06)" : "rgba(14,13,11,0.04)"};
761
+ }
762
+ .mushi-example-chip:focus-visible {
763
+ outline: 2px solid ${vermillion};
764
+ outline-offset: 2px;
765
+ }
766
+
767
+ /* Textarea wrapper to position char counter */
768
+ .mushi-textarea-wrap {
769
+ position: relative;
770
+ }
771
+ .mushi-char-counter {
772
+ position: absolute;
773
+ bottom: 4px;
774
+ right: 0;
775
+ font-family: ${fontMono};
776
+ font-size: 10px;
777
+ letter-spacing: 0.04em;
778
+ color: ${inkFaint};
779
+ pointer-events: none;
780
+ transition: color 200ms ${easeStamp};
781
+ }
782
+
696
783
  .mushi-textarea {
697
784
  width: 100%;
698
785
  min-height: 96px;
@@ -755,10 +842,34 @@ function getWidgetStyles(theme) {
755
842
  border-color: ${vermillion};
756
843
  background: ${vermillionWash};
757
844
  }
845
+ .mushi-attach-btn.loading {
846
+ opacity: 0.7;
847
+ cursor: wait;
848
+ }
849
+ .mushi-attach-btn.error {
850
+ color: ${vermillion};
851
+ border-color: ${vermillionWash};
852
+ }
758
853
  .mushi-attach-btn:focus-visible {
759
854
  outline: 2px solid ${vermillion};
760
855
  outline-offset: 2px;
761
856
  }
857
+ @keyframes mushi-spin {
858
+ to { transform: rotate(360deg); }
859
+ }
860
+ @keyframes mushi-fade-in {
861
+ from { opacity: 0; transform: translateY(4px); }
862
+ to { opacity: 1; transform: translateY(0); }
863
+ }
864
+ .mushi-spinner {
865
+ display: inline-block;
866
+ width: 10px;
867
+ height: 10px;
868
+ border: 1.5px solid currentColor;
869
+ border-top-color: transparent;
870
+ border-radius: 50%;
871
+ animation: mushi-spin 0.7s linear infinite;
872
+ }
762
873
 
763
874
  .mushi-footer {
764
875
  padding: 14px 22px 16px;
@@ -1232,7 +1343,8 @@ var MushiWidget = class {
1232
1343
  draggable: config.draggable ?? false,
1233
1344
  brandFooter: config.brandFooter ?? true,
1234
1345
  outdatedBanner: config.outdatedBanner ?? "auto",
1235
- betaMode: config.betaMode ?? {}
1346
+ betaMode: config.betaMode ?? {},
1347
+ minDescriptionLength: config.minDescriptionLength ?? 20
1236
1348
  };
1237
1349
  this.callbacks = callbacks;
1238
1350
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
@@ -1251,12 +1363,22 @@ var MushiWidget = class {
1251
1363
  selectedCategory = null;
1252
1364
  selectedIntent = null;
1253
1365
  screenshotAttached = false;
1366
+ screenshotCapturing = false;
1367
+ screenshotError = false;
1254
1368
  allowScreenshotRemove = true;
1255
1369
  elementSelected = false;
1370
+ elementCapturing = false;
1256
1371
  submitting = false;
1372
+ /** Hint element injected outside the shadow DOM during element selection. */
1373
+ selectorHint = null;
1257
1374
  triggerVisible = true;
1258
1375
  triggerShrunk = false;
1259
1376
  triggerHiddenByScroll = false;
1377
+ /** Milliseconds since mount — used for the 30s first-time nudge gate. */
1378
+ mountedAt = null;
1379
+ nudgeShown = false;
1380
+ nudgeEl = null;
1381
+ nudgeTimer = null;
1260
1382
  sdkFreshness = null;
1261
1383
  reporterReports = [];
1262
1384
  reporterComments = [];
@@ -1283,6 +1405,7 @@ var MushiWidget = class {
1283
1405
  this.syncAttachedLaunchers();
1284
1406
  this.syncSmartHide();
1285
1407
  this.render();
1408
+ this.mountedAt = Date.now();
1286
1409
  }
1287
1410
  getIsMounted() {
1288
1411
  return this.host.isConnected;
@@ -1309,7 +1432,8 @@ var MushiWidget = class {
1309
1432
  ...config.draggable !== void 0 ? { draggable: config.draggable } : {},
1310
1433
  ...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
1311
1434
  ...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {},
1312
- ...config.betaMode !== void 0 ? { betaMode: config.betaMode } : {}
1435
+ ...config.betaMode !== void 0 ? { betaMode: config.betaMode } : {},
1436
+ ...config.minDescriptionLength !== void 0 ? { minDescriptionLength: config.minDescriptionLength } : {}
1313
1437
  };
1314
1438
  this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
1315
1439
  this.syncAttachedLaunchers();
@@ -1320,9 +1444,13 @@ var MushiWidget = class {
1320
1444
  if (this.isOpen) return;
1321
1445
  this.isOpen = true;
1322
1446
  this.screenshotAttached = false;
1447
+ this.screenshotCapturing = false;
1448
+ this.screenshotError = false;
1323
1449
  this.elementSelected = false;
1450
+ this.elementCapturing = false;
1324
1451
  this.submitting = false;
1325
1452
  this.submittedAt = null;
1453
+ this.removeSelectorHint();
1326
1454
  if (options?.category) {
1327
1455
  this.selectedCategory = options.category;
1328
1456
  this.selectedIntent = null;
@@ -1378,8 +1506,114 @@ var MushiWidget = class {
1378
1506
  }
1379
1507
  setElementSelected(selected) {
1380
1508
  this.elementSelected = selected;
1509
+ this.elementCapturing = false;
1510
+ this.removeSelectorHint();
1511
+ if (this.isOpen) this.render();
1512
+ }
1513
+ setScreenshotCapturing(capturing) {
1514
+ this.screenshotCapturing = capturing;
1515
+ this.screenshotError = false;
1516
+ if (this.isOpen) this.render();
1517
+ }
1518
+ setScreenshotError(failed) {
1519
+ this.screenshotError = failed;
1520
+ this.screenshotCapturing = false;
1381
1521
  if (this.isOpen) this.render();
1382
1522
  }
1523
+ setElementCapturing(capturing) {
1524
+ this.elementCapturing = capturing;
1525
+ if (capturing) {
1526
+ this.showSelectorHint();
1527
+ } else {
1528
+ this.removeSelectorHint();
1529
+ }
1530
+ if (this.isOpen) this.render();
1531
+ }
1532
+ /** Hide the widget panel (but keep the host element) during element selection
1533
+ * so the user can click any element on the page without the panel
1534
+ * intercepting the event. */
1535
+ hidePanel() {
1536
+ const panel = this.shadow.querySelector(".mushi-panel");
1537
+ if (panel) panel.style.display = "none";
1538
+ }
1539
+ showPanel() {
1540
+ const panel = this.shadow.querySelector(".mushi-panel");
1541
+ if (panel) panel.style.display = "";
1542
+ }
1543
+ showSelectorHint() {
1544
+ this.removeSelectorHint();
1545
+ const hint = document.createElement("div");
1546
+ hint.id = "mushi-selector-hint";
1547
+ hint.setAttribute("role", "status");
1548
+ hint.setAttribute("aria-live", "polite");
1549
+ hint.style.cssText = `
1550
+ position: fixed;
1551
+ bottom: 24px;
1552
+ left: 50%;
1553
+ transform: translateX(-50%);
1554
+ z-index: 2147483646;
1555
+ background: rgba(17,17,17,0.92);
1556
+ color: #fff;
1557
+ font-family: ui-monospace, SFMono-Regular, monospace;
1558
+ font-size: 12px;
1559
+ letter-spacing: 0.04em;
1560
+ padding: 8px 16px;
1561
+ border-radius: 20px;
1562
+ pointer-events: none;
1563
+ white-space: nowrap;
1564
+ backdrop-filter: blur(4px);
1565
+ box-shadow: 0 2px 12px rgba(0,0,0,0.35);
1566
+ `;
1567
+ hint.textContent = this.locale.step3.elementSelectorHint;
1568
+ document.body.appendChild(hint);
1569
+ this.selectorHint = hint;
1570
+ }
1571
+ removeSelectorHint() {
1572
+ this.selectorHint?.remove();
1573
+ this.selectorHint = null;
1574
+ document.getElementById("mushi-selector-hint")?.remove();
1575
+ }
1576
+ showNudge() {
1577
+ if (this.nudgeShown || this.nudgeEl) return;
1578
+ this.nudgeShown = true;
1579
+ const trigger = this.shadow.querySelector(".mushi-trigger");
1580
+ const rect = trigger?.getBoundingClientRect();
1581
+ const nudge = document.createElement("div");
1582
+ nudge.id = "mushi-nudge-bubble";
1583
+ nudge.setAttribute("role", "tooltip");
1584
+ const isRight = this.config.position.includes("right");
1585
+ nudge.style.cssText = `
1586
+ position: fixed;
1587
+ z-index: 2147483645;
1588
+ ${rect ? `bottom: ${window.innerHeight - rect.top + 8}px; ${isRight ? `right: ${window.innerWidth - rect.right}px;` : `left: ${rect.left}px;`}` : "bottom: 80px; right: 24px;"}
1589
+ background: rgba(17,17,17,0.92);
1590
+ color: #fff;
1591
+ font-family: ui-sans-serif, system-ui, sans-serif;
1592
+ font-size: 12px;
1593
+ line-height: 1.4;
1594
+ padding: 8px 12px;
1595
+ border-radius: 8px;
1596
+ max-width: 200px;
1597
+ pointer-events: none;
1598
+ backdrop-filter: blur(4px);
1599
+ box-shadow: 0 2px 12px rgba(0,0,0,0.35);
1600
+ animation: mushi-fade-in 0.15s ease forwards;
1601
+ `;
1602
+ nudge.textContent = this.locale.step3.tooShort.startsWith("A bit") ? "Found a bug? One sentence is enough \u{1F41B}" : "\u30D0\u30B0\u3092\u898B\u3064\u3051\u305F\uFF1F\u4E00\u884C\u3067\u5927\u4E08\u592B\u3067\u3059 \u{1F41B}";
1603
+ document.body.appendChild(nudge);
1604
+ this.nudgeEl = nudge;
1605
+ if (this.nudgeTimer !== null) clearTimeout(this.nudgeTimer);
1606
+ this.nudgeTimer = setTimeout(() => this.removeNudge(), 5e3);
1607
+ }
1608
+ removeNudge() {
1609
+ if (this.nudgeTimer !== null) {
1610
+ clearTimeout(this.nudgeTimer);
1611
+ this.nudgeTimer = null;
1612
+ }
1613
+ this.nudgeEl?.remove();
1614
+ this.nudgeEl = null;
1615
+ document.getElementById("mushi-nudge-bubble")?.remove();
1616
+ }
1383
1617
  setSdkFreshness(info) {
1384
1618
  this.sdkFreshness = info;
1385
1619
  if (this.isOpen) this.render();
@@ -1405,6 +1639,8 @@ var MushiWidget = class {
1405
1639
  this.smartHideCleanup = null;
1406
1640
  this.attachedLaunchers.forEach((cleanup) => cleanup());
1407
1641
  this.attachedLaunchers = [];
1642
+ this.removeSelectorHint();
1643
+ this.removeNudge();
1408
1644
  this.host.remove();
1409
1645
  }
1410
1646
  syncAttachedLaunchers() {
@@ -1500,9 +1736,22 @@ var MushiWidget = class {
1500
1736
  trigger.style.zIndex = String(this.config.zIndex);
1501
1737
  this.applyInsetVars(trigger);
1502
1738
  trigger.addEventListener("click", () => {
1739
+ this.removeNudge();
1503
1740
  if (this.isOpen) this.close();
1504
1741
  else this.open();
1505
1742
  });
1743
+ trigger.addEventListener("mouseenter", () => {
1744
+ const onPageMs = this.mountedAt ? Date.now() - this.mountedAt : 0;
1745
+ if (!this.nudgeShown && !this.isOpen && onPageMs >= 3e4) {
1746
+ this.showNudge();
1747
+ }
1748
+ });
1749
+ trigger.addEventListener("mouseleave", () => {
1750
+ if (this.nudgeEl) {
1751
+ if (this.nudgeTimer !== null) clearTimeout(this.nudgeTimer);
1752
+ this.nudgeTimer = setTimeout(() => this.removeNudge(), 2e3);
1753
+ }
1754
+ });
1506
1755
  this.shadow.appendChild(trigger);
1507
1756
  }
1508
1757
  const panel = document.createElement("div");
@@ -1754,25 +2003,62 @@ var MushiWidget = class {
1754
2003
  ${this.renderStepIndicator(STEP_NUMBER.intent)}
1755
2004
  `;
1756
2005
  }
2006
+ effectiveMinLength() {
2007
+ const base = this.config.minDescriptionLength ?? 20;
2008
+ const lang = this.config.locale === "auto" ? typeof navigator !== "undefined" ? navigator.language ?? "" : "" : this.config.locale ?? "";
2009
+ const isCjk = /^(ja|zh|ko)/i.test(lang);
2010
+ return isCjk ? Math.max(4, Math.floor(base / 2)) : base;
2011
+ }
1757
2012
  renderDetailsStep() {
1758
2013
  const t = this.locale;
2014
+ const minLen = this.effectiveMinLength();
2015
+ const screenshotLabel = this.screenshotCapturing ? t.step3.screenshotCapturing : this.screenshotError ? t.step3.screenshotFailed : this.screenshotAttached ? t.step3.screenshotAttached : t.step3.screenshotButton;
2016
+ const screenshotClass = [
2017
+ "mushi-attach-btn",
2018
+ this.screenshotAttached ? "active" : "",
2019
+ this.screenshotError ? "error" : "",
2020
+ this.screenshotCapturing ? "loading" : ""
2021
+ ].filter(Boolean).join(" ");
2022
+ const elementLabel = this.elementCapturing ? t.step3.elementCapturing : this.elementSelected ? t.step3.elementSelected : t.step3.elementButton;
2023
+ const elementClass = [
2024
+ "mushi-attach-btn",
2025
+ this.elementSelected ? "active" : "",
2026
+ this.elementCapturing ? "loading" : ""
2027
+ ].filter(Boolean).join(" ");
2028
+ const exampleChips = t.step3.examplePrompts.map((p) => `<button type="button" class="mushi-example-chip" data-example="${escapeHtml(p)}">${escapeHtml(p)}</button>`).join("");
1759
2029
  return `
1760
2030
  ${this.renderHeader({ title: t.step3.heading, showBack: true, step: STEP_NUMBER.details })}
1761
2031
  <div class="mushi-body">
1762
- <textarea
1763
- class="mushi-textarea"
1764
- placeholder="${t.step3.descriptionPlaceholder}"
1765
- rows="4"
1766
- aria-label="${t.step3.heading}"
1767
- autofocus
1768
- ></textarea>
2032
+ <div class="mushi-example-chips" aria-label="Example prompts">${exampleChips}</div>
2033
+ <div class="mushi-textarea-wrap">
2034
+ <textarea
2035
+ class="mushi-textarea"
2036
+ placeholder="${t.step3.descriptionPlaceholder}"
2037
+ rows="4"
2038
+ aria-label="${t.step3.heading}"
2039
+ autofocus
2040
+ ></textarea>
2041
+ <div class="mushi-char-counter" data-role="char-counter" aria-hidden="true">
2042
+ <span data-role="char-current">0</span>/<span data-role="char-min">${minLen}</span>
2043
+ </div>
2044
+ </div>
1769
2045
  <div class="mushi-attachments">
1770
- <button type="button" class="mushi-attach-btn${this.screenshotAttached ? " active" : ""}" data-action="screenshot">
1771
- \u{1F4F8} ${this.screenshotAttached ? t.step3.screenshotAttached : t.step3.screenshotButton}
2046
+ <button type="button" class="${screenshotClass}"
2047
+ data-action="screenshot"
2048
+ ${this.screenshotCapturing ? "disabled" : ""}
2049
+ aria-label="${escapeHtml(screenshotLabel)}"
2050
+ >
2051
+ ${this.screenshotCapturing ? '<span class="mushi-spinner" aria-hidden="true"></span>' : "\u{1F4F8}"}
2052
+ ${escapeHtml(screenshotLabel)}
1772
2053
  </button>
1773
- ${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot">\u2715 Remove screenshot</button>' : ""}
1774
- <button type="button" class="mushi-attach-btn${this.elementSelected ? " active" : ""}" data-action="element">
1775
- \u{1F3AF} ${this.elementSelected ? t.step3.elementSelected : t.step3.elementButton}
2054
+ ${this.screenshotAttached && this.allowScreenshotRemove ? '<button type="button" class="mushi-attach-btn danger" data-action="remove-screenshot" aria-label="Remove screenshot">\u2715 Remove</button>' : ""}
2055
+ <button type="button" class="${elementClass}"
2056
+ data-action="element"
2057
+ ${this.elementCapturing ? "disabled" : ""}
2058
+ aria-label="${escapeHtml(elementLabel)}"
2059
+ >
2060
+ ${this.elementCapturing ? '<span class="mushi-spinner" aria-hidden="true"></span>' : "\u{1F3AF}"}
2061
+ ${escapeHtml(elementLabel)}
1776
2062
  </button>
1777
2063
  </div>
1778
2064
  <div class="mushi-error" style="display:none" role="alert"></div>
@@ -1942,6 +2228,30 @@ var MushiWidget = class {
1942
2228
  this.render();
1943
2229
  });
1944
2230
  });
2231
+ const textarea = panel.querySelector(".mushi-textarea");
2232
+ const charCurrentEl = panel.querySelector('[data-role="char-current"]');
2233
+ if (textarea && charCurrentEl) {
2234
+ const minLen = this.effectiveMinLength();
2235
+ const updateCounter = () => {
2236
+ const len = textarea.value.trim().length;
2237
+ charCurrentEl.textContent = String(len);
2238
+ const counterEl = panel.querySelector('[data-role="char-counter"]');
2239
+ if (counterEl) {
2240
+ counterEl.style.color = len >= minLen ? "var(--mushi-ok, #22c55e)" : "";
2241
+ }
2242
+ };
2243
+ textarea.addEventListener("input", updateCounter);
2244
+ }
2245
+ panel.querySelectorAll("[data-example]").forEach((chip) => {
2246
+ chip.addEventListener("click", () => {
2247
+ const example = chip.dataset.example ?? "";
2248
+ if (textarea) {
2249
+ textarea.value = example;
2250
+ textarea.focus();
2251
+ textarea.dispatchEvent(new Event("input"));
2252
+ }
2253
+ });
2254
+ });
1945
2255
  panel.querySelector('[data-action="screenshot"]')?.addEventListener("click", () => {
1946
2256
  this.callbacks.onScreenshotRequest();
1947
2257
  });
@@ -1952,14 +2262,16 @@ var MushiWidget = class {
1952
2262
  this.callbacks.onElementSelectorRequest?.();
1953
2263
  });
1954
2264
  const submitReport = () => {
1955
- const textarea = panel.querySelector(".mushi-textarea");
1956
- const description = textarea?.value?.trim() ?? "";
2265
+ const textarea2 = panel.querySelector(".mushi-textarea");
2266
+ const description = textarea2?.value?.trim() ?? "";
1957
2267
  const errorEl = panel.querySelector(".mushi-error");
1958
- const MIN_DESCRIPTION_LENGTH = 20;
1959
- if (description.length < MIN_DESCRIPTION_LENGTH) {
2268
+ const minLen = this.effectiveMinLength();
2269
+ if (description.length < minLen) {
1960
2270
  if (errorEl) {
1961
- errorEl.textContent = `${t.widget.error} (${description.length}/${MIN_DESCRIPTION_LENGTH})`;
2271
+ const msg = `${t.step3.tooShort} (${description.length}/${minLen})`;
2272
+ errorEl.textContent = msg;
1962
2273
  errorEl.style.display = "block";
2274
+ textarea2?.focus();
1963
2275
  }
1964
2276
  return;
1965
2277
  }
@@ -2622,50 +2934,16 @@ function truncateUrl(url) {
2622
2934
  function createScreenshotCapture(options = {}) {
2623
2935
  let activeOptions = options;
2624
2936
  async function take() {
2625
- try {
2626
- if (typeof document === "undefined") return null;
2627
- const canvas = document.createElement("canvas");
2628
- const ctx = canvas.getContext("2d");
2629
- if (!ctx) return null;
2630
- const width = window.innerWidth;
2631
- const height = window.innerHeight;
2632
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
2633
- canvas.width = width * dpr;
2634
- canvas.height = height * dpr;
2635
- ctx.scale(dpr, dpr);
2636
- const safeDocument = buildPrivacySafeDocument(activeOptions.privacy);
2637
- const svgData = `
2638
- <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
2639
- <foreignObject width="100%" height="100%">
2640
- <div xmlns="http://www.w3.org/1999/xhtml">
2641
- ${new XMLSerializer().serializeToString(safeDocument)}
2642
- </div>
2643
- </foreignObject>
2644
- </svg>
2645
- `;
2646
- const img = new Image();
2647
- const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
2648
- const url = URL.createObjectURL(blob);
2649
- return new Promise((resolve) => {
2650
- img.onload = () => {
2651
- ctx.drawImage(img, 0, 0, width, height);
2652
- URL.revokeObjectURL(url);
2653
- try {
2654
- const dataUrl = canvas.toDataURL("image/jpeg", 0.7);
2655
- resolve(dataUrl);
2656
- } catch {
2657
- resolve(null);
2658
- }
2659
- };
2660
- img.onerror = () => {
2661
- URL.revokeObjectURL(url);
2662
- resolve(null);
2663
- };
2664
- img.src = url;
2665
- });
2666
- } catch {
2667
- return null;
2937
+ if (typeof document === "undefined") {
2938
+ return { ok: false, reason: "unsupported", message: "Not in a browser context" };
2668
2939
  }
2940
+ const svgResult = await trySvgCapture(activeOptions.privacy);
2941
+ if (svgResult.ok) return svgResult;
2942
+ if (svgResult.reason !== "unsupported") {
2943
+ const mediaResult = await tryDisplayMediaCapture();
2944
+ if (mediaResult.ok) return mediaResult;
2945
+ }
2946
+ return svgResult;
2669
2947
  }
2670
2948
  return {
2671
2949
  take,
@@ -2674,8 +2952,110 @@ function createScreenshotCapture(options = {}) {
2674
2952
  }
2675
2953
  };
2676
2954
  }
2955
+ async function trySvgCapture(privacy) {
2956
+ try {
2957
+ const canvas = document.createElement("canvas");
2958
+ const ctx = canvas.getContext("2d");
2959
+ if (!ctx) return { ok: false, reason: "unsupported", message: "Canvas 2d context unavailable" };
2960
+ const width = window.innerWidth;
2961
+ const height = window.innerHeight;
2962
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
2963
+ canvas.width = width * dpr;
2964
+ canvas.height = height * dpr;
2965
+ ctx.scale(dpr, dpr);
2966
+ const safeDocument = buildPrivacySafeDocument(privacy);
2967
+ const svgData = `
2968
+ <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
2969
+ <foreignObject width="100%" height="100%">
2970
+ <div xmlns="http://www.w3.org/1999/xhtml">
2971
+ ${new XMLSerializer().serializeToString(safeDocument)}
2972
+ </div>
2973
+ </foreignObject>
2974
+ </svg>
2975
+ `;
2976
+ const img = new Image();
2977
+ const blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
2978
+ const url = URL.createObjectURL(blob);
2979
+ const loadResult = await new Promise((resolve) => {
2980
+ img.onload = () => resolve("loaded");
2981
+ img.onerror = () => resolve("error");
2982
+ const timeout = setTimeout(() => resolve("error"), 5e3);
2983
+ img.onload = () => {
2984
+ clearTimeout(timeout);
2985
+ resolve("loaded");
2986
+ };
2987
+ });
2988
+ URL.revokeObjectURL(url);
2989
+ if (loadResult === "error") {
2990
+ return { ok: false, reason: "load-error", message: "SVG image load failed" };
2991
+ }
2992
+ ctx.drawImage(img, 0, 0, width, height);
2993
+ try {
2994
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.75);
2995
+ return { ok: true, dataUrl };
2996
+ } catch (err) {
2997
+ const message = err instanceof Error ? err.message : String(err);
2998
+ return { ok: false, reason: "tainted", message };
2999
+ }
3000
+ } catch (err) {
3001
+ return { ok: false, reason: "error", message: err instanceof Error ? err.message : String(err) };
3002
+ }
3003
+ }
3004
+ async function tryDisplayMediaCapture() {
3005
+ if (typeof navigator === "undefined" || !("mediaDevices" in navigator)) {
3006
+ return { ok: false, reason: "unsupported", message: "mediaDevices not available" };
3007
+ }
3008
+ const mediaDevices = navigator.mediaDevices;
3009
+ if (typeof mediaDevices.getDisplayMedia !== "function") {
3010
+ return { ok: false, reason: "unsupported", message: "getDisplayMedia not available" };
3011
+ }
3012
+ let stream = null;
3013
+ try {
3014
+ stream = await mediaDevices.getDisplayMedia({
3015
+ video: { displaySurface: "browser" },
3016
+ audio: false
3017
+ });
3018
+ const track = stream.getVideoTracks()[0];
3019
+ if (!track) return { ok: false, reason: "error", message: "No video track" };
3020
+ const imageCapture = new window.ImageCapture(track);
3021
+ const bitmap = await imageCapture.grabFrame();
3022
+ const canvas = document.createElement("canvas");
3023
+ canvas.width = bitmap.width;
3024
+ canvas.height = bitmap.height;
3025
+ const ctx = canvas.getContext("2d");
3026
+ ctx.drawImage(bitmap, 0, 0);
3027
+ bitmap.close();
3028
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
3029
+ return { ok: true, dataUrl };
3030
+ } catch (err) {
3031
+ const message = err instanceof Error ? err.message : String(err);
3032
+ if (err instanceof Error && err.name === "NotAllowedError") {
3033
+ return { ok: false, reason: "cancelled", message };
3034
+ }
3035
+ return { ok: false, reason: "error", message };
3036
+ } finally {
3037
+ stream?.getTracks().forEach((t) => t.stop());
3038
+ }
3039
+ }
2677
3040
  function buildPrivacySafeDocument(privacy) {
2678
3041
  const clone = document.documentElement.cloneNode(true);
3042
+ for (const img of Array.from(clone.querySelectorAll("img[src]"))) {
3043
+ const src = img.getAttribute("src") ?? "";
3044
+ try {
3045
+ const url = new URL(src, window.location.href);
3046
+ if (url.origin !== window.location.origin) {
3047
+ img.removeAttribute("src");
3048
+ img.removeAttribute("srcset");
3049
+ }
3050
+ } catch {
3051
+ }
3052
+ }
3053
+ for (const el of Array.from(clone.querySelectorAll("[style]"))) {
3054
+ const style = el.getAttribute("style") ?? "";
3055
+ if (/url\(["']?https?:\/\/(?!localhost)/.test(style)) {
3056
+ el.setAttribute("style", style.replace(/url\([^)]*\)/g, "none"));
3057
+ }
3058
+ }
2679
3059
  for (const selector of privacy?.blockSelectors ?? []) {
2680
3060
  for (const el of safeQueryAll(clone, selector)) {
2681
3061
  el.remove();
@@ -2872,6 +3252,11 @@ function createElementSelector() {
2872
3252
  e.stopPropagation();
2873
3253
  const target = e.target;
2874
3254
  if (target === overlay) return;
3255
+ const path = e.composedPath ? e.composedPath() : [];
3256
+ const hitsMushiHost = path.some(
3257
+ (node) => node instanceof Element && node.id === "mushi-mushi-widget"
3258
+ );
3259
+ if (hitsMushiHost) return;
2875
3260
  const captured = captureElement(target);
2876
3261
  finish(captured);
2877
3262
  }
@@ -3586,7 +3971,7 @@ function createProactiveManager(config = {}) {
3586
3971
 
3587
3972
  // src/version.ts
3588
3973
  var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
3589
- var MUSHI_SDK_VERSION = "1.2.1" ;
3974
+ var MUSHI_SDK_VERSION = "1.3.0" ;
3590
3975
 
3591
3976
  // src/mushi.ts
3592
3977
  var instance = null;
@@ -3791,8 +4176,21 @@ function createInstance(config) {
3791
4176
  onScreenshotRequest: async () => {
3792
4177
  if (!screenshotCap || activeConfig.capture?.screenshot === "off") return;
3793
4178
  log.debug("Taking screenshot");
3794
- pendingScreenshot = await screenshotCap.take();
3795
- widget.setScreenshotAttached(pendingScreenshot !== null);
4179
+ widget.setScreenshotCapturing(true);
4180
+ const result = await screenshotCap.take();
4181
+ if (result.ok) {
4182
+ pendingScreenshot = result.dataUrl;
4183
+ widget.setScreenshotAttached(true);
4184
+ log.debug("Screenshot captured");
4185
+ } else {
4186
+ pendingScreenshot = null;
4187
+ if (result.reason !== "cancelled") {
4188
+ widget.setScreenshotError(true);
4189
+ log.debug("Screenshot failed", { reason: result.reason, message: result.message });
4190
+ } else {
4191
+ widget.setScreenshotCapturing(false);
4192
+ }
4193
+ }
3796
4194
  },
3797
4195
  onScreenshotRemove: () => {
3798
4196
  log.debug("Screenshot attachment removed");
@@ -3802,11 +4200,19 @@ function createInstance(config) {
3802
4200
  onElementSelectorRequest: async () => {
3803
4201
  if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
3804
4202
  log.debug("Element selector activated");
3805
- const el = await elementSelector.activate();
3806
- if (el) {
3807
- pendingElement = el;
3808
- widget.setElementSelected(true);
3809
- log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
4203
+ widget.setElementCapturing(true);
4204
+ widget.hidePanel();
4205
+ try {
4206
+ const el = await elementSelector.activate();
4207
+ if (el) {
4208
+ pendingElement = el;
4209
+ widget.setElementSelected(true);
4210
+ log.debug("Element selected", { tagName: el.tagName, xpath: el.xpath });
4211
+ } else {
4212
+ widget.setElementCapturing(false);
4213
+ }
4214
+ } finally {
4215
+ widget.showPanel();
3810
4216
  }
3811
4217
  },
3812
4218
  async onReporterReportsRequest() {