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