@mushi-mushi/web 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +4 -0
- package/README.md +41 -0
- package/SECURITY.md +74 -0
- package/dist/index.cjs +389 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +389 -79
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -243,11 +243,6 @@ function getWidgetStyles(theme) {
|
|
|
243
243
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
244
244
|
button { font-family: inherit; }
|
|
245
245
|
|
|
246
|
-
/* \u2500\u2500 Trigger \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
247
|
-
A small "stamp card" \u2014 soft rounded square (4px radius), paper
|
|
248
|
-
background, vermillion bottom edge that reads as the inked face
|
|
249
|
-
of a real \u5370\u9451. A pulsing dot in the top-right hints there's a
|
|
250
|
-
channel here without needing a notification badge. */
|
|
251
246
|
.mushi-trigger {
|
|
252
247
|
position: fixed;
|
|
253
248
|
width: 52px;
|
|
@@ -301,10 +296,53 @@ function getWidgetStyles(theme) {
|
|
|
301
296
|
outline: 2px solid ${vermillion};
|
|
302
297
|
outline-offset: 3px;
|
|
303
298
|
}
|
|
304
|
-
.mushi-trigger.bottom-right {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
299
|
+
.mushi-trigger.bottom-right {
|
|
300
|
+
bottom: var(--mushi-bottom, calc(24px + env(safe-area-inset-bottom, 0px)));
|
|
301
|
+
right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
|
|
302
|
+
}
|
|
303
|
+
.mushi-trigger.bottom-left {
|
|
304
|
+
bottom: var(--mushi-bottom, calc(24px + env(safe-area-inset-bottom, 0px)));
|
|
305
|
+
left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
|
|
306
|
+
}
|
|
307
|
+
.mushi-trigger.top-right {
|
|
308
|
+
top: var(--mushi-top, calc(24px + env(safe-area-inset-top, 0px)));
|
|
309
|
+
right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
|
|
310
|
+
}
|
|
311
|
+
.mushi-trigger.top-left {
|
|
312
|
+
top: var(--mushi-top, calc(24px + env(safe-area-inset-top, 0px)));
|
|
313
|
+
left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
|
|
314
|
+
}
|
|
315
|
+
.mushi-trigger.edge-tab {
|
|
316
|
+
width: 32px;
|
|
317
|
+
height: 88px;
|
|
318
|
+
border-radius: 4px 0 0 4px;
|
|
319
|
+
writing-mode: vertical-rl;
|
|
320
|
+
text-orientation: upright;
|
|
321
|
+
font-size: 16px;
|
|
322
|
+
box-shadow:
|
|
323
|
+
0 1px 0 ${rule},
|
|
324
|
+
0 10px 24px -14px rgba(14,13,11,0.45),
|
|
325
|
+
inset -3px 0 0 ${vermillion};
|
|
326
|
+
}
|
|
327
|
+
.mushi-trigger.edge-tab.bottom-right,
|
|
328
|
+
.mushi-trigger.edge-tab.top-right {
|
|
329
|
+
right: var(--mushi-right, 0);
|
|
330
|
+
}
|
|
331
|
+
.mushi-trigger.edge-tab.bottom-left,
|
|
332
|
+
.mushi-trigger.edge-tab.top-left {
|
|
333
|
+
left: var(--mushi-left, 0);
|
|
334
|
+
border-radius: 0 4px 4px 0;
|
|
335
|
+
box-shadow:
|
|
336
|
+
0 1px 0 ${rule},
|
|
337
|
+
0 10px 24px -14px rgba(14,13,11,0.45),
|
|
338
|
+
inset 3px 0 0 ${vermillion};
|
|
339
|
+
}
|
|
340
|
+
.mushi-trigger.shrunk {
|
|
341
|
+
width: 36px;
|
|
342
|
+
height: 36px;
|
|
343
|
+
opacity: 0.82;
|
|
344
|
+
transform: scale(0.92);
|
|
345
|
+
}
|
|
308
346
|
|
|
309
347
|
@keyframes mushi-pulse {
|
|
310
348
|
0% { box-shadow: 0 0 0 0 ${vermillion}; opacity: 1; }
|
|
@@ -312,12 +350,6 @@ function getWidgetStyles(theme) {
|
|
|
312
350
|
100% { box-shadow: 0 0 0 0 rgba(224,60,44,0); opacity: 1; }
|
|
313
351
|
}
|
|
314
352
|
|
|
315
|
-
/* \u2500\u2500 Panel \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
316
|
-
Paper-card. Sharper corners (6px) than typical SaaS modals
|
|
317
|
-
(which default to 12-16px and read as plastic). Two-layer shadow:
|
|
318
|
-
one hairline that sells the paper edge, one diffuse that lifts
|
|
319
|
-
the panel off the underlying app. No backdrop-filter \u2014 we want
|
|
320
|
-
the widget to feel like it sits ON the page, not blur INTO it. */
|
|
321
353
|
.mushi-panel {
|
|
322
354
|
position: fixed;
|
|
323
355
|
width: 384px;
|
|
@@ -337,10 +369,26 @@ function getWidgetStyles(theme) {
|
|
|
337
369
|
}
|
|
338
370
|
.mushi-panel.open { animation: mushi-stamp-in 320ms ${easeStamp} both; }
|
|
339
371
|
.mushi-panel.closed { display: none; }
|
|
340
|
-
.mushi-panel.bottom-right {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
372
|
+
.mushi-panel.bottom-right {
|
|
373
|
+
bottom: var(--mushi-panel-bottom, calc(var(--mushi-bottom, 24px) + 64px));
|
|
374
|
+
right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
|
|
375
|
+
--mushi-origin: bottom right;
|
|
376
|
+
}
|
|
377
|
+
.mushi-panel.bottom-left {
|
|
378
|
+
bottom: var(--mushi-panel-bottom, calc(var(--mushi-bottom, 24px) + 64px));
|
|
379
|
+
left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
|
|
380
|
+
--mushi-origin: bottom left;
|
|
381
|
+
}
|
|
382
|
+
.mushi-panel.top-right {
|
|
383
|
+
top: var(--mushi-panel-top, calc(var(--mushi-top, 24px) + 64px));
|
|
384
|
+
right: var(--mushi-right, calc(24px + env(safe-area-inset-right, 0px)));
|
|
385
|
+
--mushi-origin: top right;
|
|
386
|
+
}
|
|
387
|
+
.mushi-panel.top-left {
|
|
388
|
+
top: var(--mushi-panel-top, calc(var(--mushi-top, 24px) + 64px));
|
|
389
|
+
left: var(--mushi-left, calc(24px + env(safe-area-inset-left, 0px)));
|
|
390
|
+
--mushi-origin: top left;
|
|
391
|
+
}
|
|
344
392
|
|
|
345
393
|
@keyframes mushi-stamp-in {
|
|
346
394
|
0% { opacity: 0; transform: scale(0.94) translateY(6px); }
|
|
@@ -348,10 +396,6 @@ function getWidgetStyles(theme) {
|
|
|
348
396
|
100% { opacity: 1; transform: scale(1) translateY(0); }
|
|
349
397
|
}
|
|
350
398
|
|
|
351
|
-
/* \u2500\u2500 Header \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
352
|
-
Editorial masthead: small mono eyebrow ("MUSHI / REPORT") on top,
|
|
353
|
-
serif display headline below, mono step counter on the far right.
|
|
354
|
-
A single hairline separates header from body \u2014 no card stacking. */
|
|
355
399
|
.mushi-header {
|
|
356
400
|
padding: 18px 20px 14px;
|
|
357
401
|
border-bottom: 1px solid ${rule};
|
|
@@ -448,10 +492,6 @@ function getWidgetStyles(theme) {
|
|
|
448
492
|
.mushi-body::-webkit-scrollbar { width: 6px; }
|
|
449
493
|
.mushi-body::-webkit-scrollbar-thumb { background: ${inkFaint}; border-radius: 3px; }
|
|
450
494
|
|
|
451
|
-
/* \u2500\u2500 Step 1: Categories as a contents-page list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
452
|
-
No boxes. Hairline rules between rows. Hovering a row pulls a
|
|
453
|
-
vermillion arrow in from the right and tints the row label \u2014
|
|
454
|
-
reads like flipping through an index card. */
|
|
455
495
|
.mushi-option-btn {
|
|
456
496
|
display: grid;
|
|
457
497
|
grid-template-columns: auto 1fr auto;
|
|
@@ -504,11 +544,6 @@ function getWidgetStyles(theme) {
|
|
|
504
544
|
transition: opacity 220ms ${easeStamp}, transform 220ms ${easeStamp}, color 220ms ${easeStamp};
|
|
505
545
|
}
|
|
506
546
|
|
|
507
|
-
/* \u2500\u2500 Step 2: Selected-category breadcrumb + intent text-buttons \u2500
|
|
508
|
-
Breadcrumb is a thin chip with the kanji-stamp aesthetic carried
|
|
509
|
-
over (vermillion left rule). Intents are inline TEXT buttons
|
|
510
|
-
with vermillion underlines on hover \u2014 not pill-shaped chips,
|
|
511
|
-
which is the SaaS default and not what we are. */
|
|
512
547
|
.mushi-selected-category {
|
|
513
548
|
display: inline-flex;
|
|
514
549
|
align-items: center;
|
|
@@ -564,10 +599,6 @@ function getWidgetStyles(theme) {
|
|
|
564
599
|
box-shadow: inset 2px 0 0 ${vermillion};
|
|
565
600
|
}
|
|
566
601
|
|
|
567
|
-
/* \u2500\u2500 Step 3: Borderless textarea + minimal attach pills \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
568
|
-
The textarea has no box around it \u2014 just a hairline underline
|
|
569
|
-
that turns vermillion on focus. Encourages writing rather than
|
|
570
|
-
form-filling. */
|
|
571
602
|
.mushi-textarea {
|
|
572
603
|
width: 100%;
|
|
573
604
|
min-height: 96px;
|
|
@@ -625,10 +656,6 @@ function getWidgetStyles(theme) {
|
|
|
625
656
|
outline-offset: 2px;
|
|
626
657
|
}
|
|
627
658
|
|
|
628
|
-
/* \u2500\u2500 Footer + submit (vermillion stamp) \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
|
|
629
|
-
Submit button is the heaviest visual moment in the widget \u2014
|
|
630
|
-
vermillion fill, mono-caps label, send arrow. Holds an ink-
|
|
631
|
-
bloom pseudo-element that animates outward when pressed. */
|
|
632
659
|
.mushi-footer {
|
|
633
660
|
padding: 14px 22px 16px;
|
|
634
661
|
border-top: 1px solid ${rule};
|
|
@@ -693,10 +720,6 @@ function getWidgetStyles(theme) {
|
|
|
693
720
|
}
|
|
694
721
|
.mushi-submit:hover .mushi-submit-arrow { transform: translateX(3px); }
|
|
695
722
|
|
|
696
|
-
/* \u2500\u2500 Step indicator (numeral ledger) \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
|
|
697
|
-
Replaces the generic three-dots with a typographic series:
|
|
698
|
-
"01 \u2014 02 \u2014 03". The active step uses serif numerals, the
|
|
699
|
-
others use mono so the active one literally reads heavier. */
|
|
700
723
|
.mushi-step-indicator {
|
|
701
724
|
display: flex;
|
|
702
725
|
align-items: center;
|
|
@@ -724,10 +747,6 @@ function getWidgetStyles(theme) {
|
|
|
724
747
|
}
|
|
725
748
|
.mushi-step-sep { width: 14px; height: 1px; background: ${rule}; }
|
|
726
749
|
|
|
727
|
-
/* \u2500\u2500 Success: \u6731\u5370 stamp animation \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
|
|
728
|
-
The success state is the signature moment. A vermillion ring
|
|
729
|
-
scribes itself, then a "RECEIVED" mono-caps label fades in at
|
|
730
|
-
the centre, evoking a hanko being pressed onto the form. */
|
|
731
750
|
.mushi-success {
|
|
732
751
|
text-align: center;
|
|
733
752
|
padding: 28px 16px 20px;
|
|
@@ -791,9 +810,6 @@ function getWidgetStyles(theme) {
|
|
|
791
810
|
100% { opacity: 1; transform: rotate(-6deg) scale(1); }
|
|
792
811
|
}
|
|
793
812
|
|
|
794
|
-
/* \u2500\u2500 Error \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
795
|
-
Inline editorial note rather than a red box. Vermillion left
|
|
796
|
-
rule keeps the same accent language. */
|
|
797
813
|
.mushi-error {
|
|
798
814
|
margin-top: 10px;
|
|
799
815
|
padding: 8px 0 8px 10px;
|
|
@@ -804,9 +820,6 @@ function getWidgetStyles(theme) {
|
|
|
804
820
|
letter-spacing: 0.02em;
|
|
805
821
|
}
|
|
806
822
|
|
|
807
|
-
/* \u2500\u2500 Reduced motion \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
808
|
-
Honour the OS preference: kill every transition + animation
|
|
809
|
-
except the focus underline (which is critical feedback). */
|
|
810
823
|
@media (prefers-reduced-motion: reduce) {
|
|
811
824
|
*,
|
|
812
825
|
*::before,
|
|
@@ -854,6 +867,12 @@ var MushiWidget = class {
|
|
|
854
867
|
screenshotAttached = false;
|
|
855
868
|
elementSelected = false;
|
|
856
869
|
submitting = false;
|
|
870
|
+
triggerVisible = true;
|
|
871
|
+
triggerShrunk = false;
|
|
872
|
+
triggerHiddenByScroll = false;
|
|
873
|
+
attachedLaunchers = [];
|
|
874
|
+
smartHideCleanup = null;
|
|
875
|
+
smartHideTimer = null;
|
|
857
876
|
/** Captured at the moment of submit so the success ledger metadata
|
|
858
877
|
* ("REPORT · 14:23:07 JST") doesn't drift while the success step
|
|
859
878
|
* is on screen. */
|
|
@@ -879,7 +898,16 @@ var MushiWidget = class {
|
|
|
879
898
|
expandedTitle: config.expandedTitle ?? "",
|
|
880
899
|
mode: config.mode ?? "conversational",
|
|
881
900
|
locale: config.locale ?? "auto",
|
|
882
|
-
zIndex: config.zIndex ?? 99999
|
|
901
|
+
zIndex: config.zIndex ?? 99999,
|
|
902
|
+
trigger: config.trigger ?? "auto",
|
|
903
|
+
attachToSelector: config.attachToSelector ?? "",
|
|
904
|
+
inset: config.inset ?? {},
|
|
905
|
+
respectSafeArea: config.respectSafeArea ?? true,
|
|
906
|
+
hideOnSelector: config.hideOnSelector ?? "",
|
|
907
|
+
hideOnRoutes: config.hideOnRoutes ?? [],
|
|
908
|
+
environments: config.environments ?? {},
|
|
909
|
+
smartHide: config.smartHide ?? false,
|
|
910
|
+
draggable: config.draggable ?? false
|
|
883
911
|
};
|
|
884
912
|
this.callbacks = callbacks;
|
|
885
913
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
@@ -889,6 +917,33 @@ var MushiWidget = class {
|
|
|
889
917
|
}
|
|
890
918
|
mount() {
|
|
891
919
|
document.body.appendChild(this.host);
|
|
920
|
+
this.syncAttachedLaunchers();
|
|
921
|
+
this.syncSmartHide();
|
|
922
|
+
this.render();
|
|
923
|
+
}
|
|
924
|
+
updateConfig(config = {}) {
|
|
925
|
+
this.config = {
|
|
926
|
+
...this.config,
|
|
927
|
+
...config.position ? { position: config.position } : {},
|
|
928
|
+
...config.theme ? { theme: config.theme } : {},
|
|
929
|
+
...config.triggerText !== void 0 ? { triggerText: config.triggerText || "\u{1F41B}" } : {},
|
|
930
|
+
...config.expandedTitle !== void 0 ? { expandedTitle: config.expandedTitle } : {},
|
|
931
|
+
...config.mode ? { mode: config.mode } : {},
|
|
932
|
+
...config.locale ? { locale: config.locale } : {},
|
|
933
|
+
...config.zIndex !== void 0 ? { zIndex: config.zIndex } : {},
|
|
934
|
+
...config.trigger ? { trigger: config.trigger } : {},
|
|
935
|
+
...config.attachToSelector !== void 0 ? { attachToSelector: config.attachToSelector } : {},
|
|
936
|
+
...config.inset !== void 0 ? { inset: config.inset } : {},
|
|
937
|
+
...config.respectSafeArea !== void 0 ? { respectSafeArea: config.respectSafeArea } : {},
|
|
938
|
+
...config.hideOnSelector !== void 0 ? { hideOnSelector: config.hideOnSelector } : {},
|
|
939
|
+
...config.hideOnRoutes !== void 0 ? { hideOnRoutes: config.hideOnRoutes } : {},
|
|
940
|
+
...config.environments !== void 0 ? { environments: config.environments } : {},
|
|
941
|
+
...config.smartHide !== void 0 ? { smartHide: config.smartHide } : {},
|
|
942
|
+
...config.draggable !== void 0 ? { draggable: config.draggable } : {}
|
|
943
|
+
};
|
|
944
|
+
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
945
|
+
this.syncAttachedLaunchers();
|
|
946
|
+
this.syncSmartHide();
|
|
892
947
|
this.render();
|
|
893
948
|
}
|
|
894
949
|
open(options) {
|
|
@@ -919,6 +974,30 @@ var MushiWidget = class {
|
|
|
919
974
|
getIsOpen() {
|
|
920
975
|
return this.isOpen;
|
|
921
976
|
}
|
|
977
|
+
showTrigger() {
|
|
978
|
+
this.triggerVisible = true;
|
|
979
|
+
this.render();
|
|
980
|
+
}
|
|
981
|
+
hideTrigger() {
|
|
982
|
+
this.triggerVisible = false;
|
|
983
|
+
this.render();
|
|
984
|
+
}
|
|
985
|
+
setTrigger(trigger) {
|
|
986
|
+
this.updateConfig({ trigger });
|
|
987
|
+
}
|
|
988
|
+
attachTo(selectorOrElement, options = {}) {
|
|
989
|
+
const elements = typeof selectorOrElement === "string" ? Array.from(document.querySelectorAll(selectorOrElement)) : [selectorOrElement];
|
|
990
|
+
const cleanups = elements.map((el) => {
|
|
991
|
+
const onClick = (event) => {
|
|
992
|
+
event.preventDefault();
|
|
993
|
+
this.updateConfig(options);
|
|
994
|
+
this.open();
|
|
995
|
+
};
|
|
996
|
+
el.addEventListener("click", onClick);
|
|
997
|
+
return () => el.removeEventListener("click", onClick);
|
|
998
|
+
});
|
|
999
|
+
return () => cleanups.forEach((cleanup) => cleanup());
|
|
1000
|
+
}
|
|
922
1001
|
setScreenshotAttached(attached) {
|
|
923
1002
|
this.screenshotAttached = attached;
|
|
924
1003
|
if (this.isOpen) this.render();
|
|
@@ -936,8 +1015,83 @@ var MushiWidget = class {
|
|
|
936
1015
|
clearTimeout(this.autoCloseTimer);
|
|
937
1016
|
this.autoCloseTimer = null;
|
|
938
1017
|
}
|
|
1018
|
+
if (this.smartHideTimer !== null) {
|
|
1019
|
+
clearTimeout(this.smartHideTimer);
|
|
1020
|
+
this.smartHideTimer = null;
|
|
1021
|
+
}
|
|
1022
|
+
this.smartHideCleanup?.();
|
|
1023
|
+
this.smartHideCleanup = null;
|
|
1024
|
+
this.attachedLaunchers.forEach((cleanup) => cleanup());
|
|
1025
|
+
this.attachedLaunchers = [];
|
|
939
1026
|
this.host.remove();
|
|
940
1027
|
}
|
|
1028
|
+
syncAttachedLaunchers() {
|
|
1029
|
+
this.attachedLaunchers.forEach((cleanup) => cleanup());
|
|
1030
|
+
this.attachedLaunchers = [];
|
|
1031
|
+
if (this.config.trigger !== "attach" || !this.config.attachToSelector) return;
|
|
1032
|
+
if (typeof document === "undefined") return;
|
|
1033
|
+
this.attachedLaunchers.push(this.attachTo(this.config.attachToSelector));
|
|
1034
|
+
}
|
|
1035
|
+
syncSmartHide() {
|
|
1036
|
+
this.smartHideCleanup?.();
|
|
1037
|
+
this.smartHideCleanup = null;
|
|
1038
|
+
this.triggerShrunk = false;
|
|
1039
|
+
this.triggerHiddenByScroll = false;
|
|
1040
|
+
if (!this.config.smartHide || typeof window === "undefined") return;
|
|
1041
|
+
const smart = this.config.smartHide === true ? { onScroll: "shrink", onIdleMs: 900 } : this.config.smartHide;
|
|
1042
|
+
if (!smart.onScroll) return;
|
|
1043
|
+
const onScroll = () => {
|
|
1044
|
+
if (smart.onScroll === "hide") {
|
|
1045
|
+
this.triggerHiddenByScroll = true;
|
|
1046
|
+
} else {
|
|
1047
|
+
this.triggerShrunk = true;
|
|
1048
|
+
}
|
|
1049
|
+
this.render();
|
|
1050
|
+
if (this.smartHideTimer !== null) clearTimeout(this.smartHideTimer);
|
|
1051
|
+
this.smartHideTimer = setTimeout(() => {
|
|
1052
|
+
this.triggerHiddenByScroll = false;
|
|
1053
|
+
this.triggerShrunk = false;
|
|
1054
|
+
this.render();
|
|
1055
|
+
}, smart.onIdleMs ?? 900);
|
|
1056
|
+
};
|
|
1057
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
1058
|
+
this.smartHideCleanup = () => window.removeEventListener("scroll", onScroll);
|
|
1059
|
+
}
|
|
1060
|
+
shouldRenderTrigger() {
|
|
1061
|
+
if (!this.triggerVisible) return false;
|
|
1062
|
+
if (this.triggerHiddenByScroll) return false;
|
|
1063
|
+
if (this.config.trigger === "manual" || this.config.trigger === "hidden" || this.config.trigger === "attach") {
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
if (this.isMobileSmartHidden()) return false;
|
|
1067
|
+
if (this.isRouteHidden()) return false;
|
|
1068
|
+
if (this.config.hideOnSelector && document.querySelector(this.config.hideOnSelector)) return false;
|
|
1069
|
+
const action = this.config.environments[this.detectEnvironment()];
|
|
1070
|
+
return action !== "never" && action !== "manual";
|
|
1071
|
+
}
|
|
1072
|
+
effectiveTrigger() {
|
|
1073
|
+
if (!this.config.smartHide || typeof window === "undefined") return this.config.trigger;
|
|
1074
|
+
const smart = this.config.smartHide === true ? { onMobile: "edge-tab" } : this.config.smartHide;
|
|
1075
|
+
if (window.matchMedia("(max-width: 768px)").matches && smart.onMobile === "edge-tab") {
|
|
1076
|
+
return "edge-tab";
|
|
1077
|
+
}
|
|
1078
|
+
return this.config.trigger;
|
|
1079
|
+
}
|
|
1080
|
+
isMobileSmartHidden() {
|
|
1081
|
+
if (!this.config.smartHide || typeof window === "undefined") return false;
|
|
1082
|
+
const smart = this.config.smartHide === true ? { onMobile: "edge-tab" } : this.config.smartHide;
|
|
1083
|
+
return window.matchMedia("(max-width: 768px)").matches && smart.onMobile === "hide";
|
|
1084
|
+
}
|
|
1085
|
+
detectEnvironment() {
|
|
1086
|
+
const host = typeof location !== "undefined" ? location.hostname : "";
|
|
1087
|
+
if (host === "localhost" || host === "127.0.0.1" || host.endsWith(".local")) return "development";
|
|
1088
|
+
if (/\b(staging|stage|preview|dev)\b/i.test(host)) return "staging";
|
|
1089
|
+
return "production";
|
|
1090
|
+
}
|
|
1091
|
+
isRouteHidden() {
|
|
1092
|
+
if (!this.config.hideOnRoutes.length || typeof location === "undefined") return false;
|
|
1093
|
+
return this.config.hideOnRoutes.some((route) => location.pathname.includes(route));
|
|
1094
|
+
}
|
|
941
1095
|
getTheme() {
|
|
942
1096
|
if (this.config.theme !== "auto") return this.config.theme;
|
|
943
1097
|
if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
@@ -953,24 +1107,29 @@ var MushiWidget = class {
|
|
|
953
1107
|
const style = document.createElement("style");
|
|
954
1108
|
style.textContent = getWidgetStyles(theme);
|
|
955
1109
|
this.shadow.appendChild(style);
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1110
|
+
if (this.shouldRenderTrigger()) {
|
|
1111
|
+
const effectiveTrigger = this.effectiveTrigger();
|
|
1112
|
+
const trigger = document.createElement("button");
|
|
1113
|
+
trigger.className = `mushi-trigger ${pos}${effectiveTrigger === "edge-tab" ? " edge-tab" : ""}${this.triggerShrunk ? " shrunk" : ""}`;
|
|
1114
|
+
trigger.textContent = this.config.triggerText;
|
|
1115
|
+
trigger.setAttribute("aria-label", t.widget.trigger);
|
|
1116
|
+
trigger.setAttribute("aria-haspopup", "dialog");
|
|
1117
|
+
trigger.setAttribute("aria-expanded", String(this.isOpen));
|
|
1118
|
+
trigger.style.zIndex = String(this.config.zIndex);
|
|
1119
|
+
this.applyInsetVars(trigger);
|
|
1120
|
+
trigger.addEventListener("click", () => {
|
|
1121
|
+
if (this.isOpen) this.close();
|
|
1122
|
+
else this.open();
|
|
1123
|
+
});
|
|
1124
|
+
this.shadow.appendChild(trigger);
|
|
1125
|
+
}
|
|
968
1126
|
const panel = document.createElement("div");
|
|
969
1127
|
panel.className = `mushi-panel ${pos}${this.isOpen ? " open" : " closed"}`;
|
|
970
1128
|
panel.setAttribute("role", "dialog");
|
|
971
1129
|
panel.setAttribute("aria-modal", "true");
|
|
972
1130
|
panel.setAttribute("aria-label", t.widget.title);
|
|
973
1131
|
panel.style.zIndex = String(this.config.zIndex + 1);
|
|
1132
|
+
this.applyInsetVars(panel);
|
|
974
1133
|
if (this.isOpen) {
|
|
975
1134
|
panel.innerHTML = this.renderStep();
|
|
976
1135
|
this.shadow.appendChild(panel);
|
|
@@ -978,6 +1137,20 @@ var MushiWidget = class {
|
|
|
978
1137
|
this.trapFocus(panel);
|
|
979
1138
|
}
|
|
980
1139
|
}
|
|
1140
|
+
applyInsetVars(el) {
|
|
1141
|
+
const { inset } = this.config;
|
|
1142
|
+
if (!this.config.respectSafeArea) {
|
|
1143
|
+
["top", "right", "bottom", "left"].forEach((edge) => {
|
|
1144
|
+
if (inset[edge] === void 0) el.style.setProperty(`--mushi-${edge}`, "24px");
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
["top", "right", "bottom", "left"].forEach((edge) => {
|
|
1148
|
+
const value = inset[edge];
|
|
1149
|
+
if (value === void 0) return;
|
|
1150
|
+
el.style.setProperty(`--mushi-${edge}`, value === "auto" ? "auto" : `${value}px`);
|
|
1151
|
+
});
|
|
1152
|
+
el.style.setProperty("--mushi-safe-area", this.config.respectSafeArea ? "1" : "0");
|
|
1153
|
+
}
|
|
981
1154
|
renderStep() {
|
|
982
1155
|
switch (this.step) {
|
|
983
1156
|
case "category":
|
|
@@ -1804,6 +1977,8 @@ var Mushi = class {
|
|
|
1804
1977
|
}
|
|
1805
1978
|
};
|
|
1806
1979
|
function createInstance(config) {
|
|
1980
|
+
const bootstrapConfig = config;
|
|
1981
|
+
let activeConfig = config;
|
|
1807
1982
|
const log = config.debug ?? false ? createLogger({ scope: "mushi", level: "debug", format: "pretty" }) : noopLogger;
|
|
1808
1983
|
const apiClient = createApiClient({
|
|
1809
1984
|
projectId: config.projectId,
|
|
@@ -1814,20 +1989,50 @@ function createInstance(config) {
|
|
|
1814
1989
|
const offlineQueue = createOfflineQueue(config.offline);
|
|
1815
1990
|
const rateLimiter = createRateLimiter({ maxBurst: 10, refillRate: 1, refillIntervalMs: 5e3 });
|
|
1816
1991
|
const piiScrubber = createPiiScrubber();
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1992
|
+
let consoleCap = null;
|
|
1993
|
+
let networkCap = null;
|
|
1994
|
+
let perfCap = null;
|
|
1995
|
+
let screenshotCap = null;
|
|
1996
|
+
let elementSelector = null;
|
|
1997
|
+
function syncCaptureModules() {
|
|
1998
|
+
if (activeConfig.capture?.console !== false) {
|
|
1999
|
+
consoleCap ??= createConsoleCapture();
|
|
2000
|
+
} else {
|
|
2001
|
+
consoleCap?.destroy();
|
|
2002
|
+
consoleCap = null;
|
|
2003
|
+
}
|
|
2004
|
+
if (activeConfig.capture?.network !== false) {
|
|
2005
|
+
networkCap ??= createNetworkCapture();
|
|
2006
|
+
} else {
|
|
2007
|
+
networkCap?.destroy();
|
|
2008
|
+
networkCap = null;
|
|
2009
|
+
}
|
|
2010
|
+
if (activeConfig.capture?.performance !== false) {
|
|
2011
|
+
perfCap ??= createPerformanceCapture();
|
|
2012
|
+
} else {
|
|
2013
|
+
perfCap?.destroy();
|
|
2014
|
+
perfCap = null;
|
|
2015
|
+
}
|
|
2016
|
+
screenshotCap = activeConfig.capture?.screenshot !== "off" ? screenshotCap ?? createScreenshotCapture() : null;
|
|
2017
|
+
if (!screenshotCap) pendingScreenshot = null;
|
|
2018
|
+
if (activeConfig.capture?.elementSelector !== false) {
|
|
2019
|
+
elementSelector ??= createElementSelector();
|
|
2020
|
+
} else {
|
|
2021
|
+
elementSelector?.deactivate();
|
|
2022
|
+
elementSelector = null;
|
|
2023
|
+
pendingElement = null;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
1822
2026
|
const listeners = /* @__PURE__ */ new Map();
|
|
1823
2027
|
function emit(type, data) {
|
|
1824
2028
|
listeners.get(type)?.forEach((handler) => handler({ type, data }));
|
|
1825
2029
|
}
|
|
1826
|
-
let userInfo = null;
|
|
1827
|
-
const customMetadata = {};
|
|
1828
2030
|
let pendingScreenshot = null;
|
|
1829
2031
|
let pendingElement = null;
|
|
1830
2032
|
let pendingProactiveTrigger = null;
|
|
2033
|
+
let userInfo = null;
|
|
2034
|
+
const customMetadata = {};
|
|
2035
|
+
syncCaptureModules();
|
|
1831
2036
|
const widget = new MushiWidget(config.widget, {
|
|
1832
2037
|
onSubmit: async ({ category, description, intent }) => {
|
|
1833
2038
|
log.info("Report submitted", { category, intent });
|
|
@@ -1850,13 +2055,13 @@ function createInstance(config) {
|
|
|
1850
2055
|
emit("widget:closed");
|
|
1851
2056
|
},
|
|
1852
2057
|
onScreenshotRequest: async () => {
|
|
1853
|
-
if (!screenshotCap) return;
|
|
2058
|
+
if (!screenshotCap || activeConfig.capture?.screenshot === "off") return;
|
|
1854
2059
|
log.debug("Taking screenshot");
|
|
1855
2060
|
pendingScreenshot = await screenshotCap.take();
|
|
1856
2061
|
widget.setScreenshotAttached(pendingScreenshot !== null);
|
|
1857
2062
|
},
|
|
1858
2063
|
onElementSelectorRequest: async () => {
|
|
1859
|
-
if (!elementSelector) return;
|
|
2064
|
+
if (!elementSelector || activeConfig.capture?.elementSelector === false) return;
|
|
1860
2065
|
log.debug("Element selector activated");
|
|
1861
2066
|
const el = await elementSelector.activate();
|
|
1862
2067
|
if (el) {
|
|
@@ -1910,6 +2115,34 @@ function createInstance(config) {
|
|
|
1910
2115
|
offlineQueue.flush(apiClient).then((result) => {
|
|
1911
2116
|
if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
|
|
1912
2117
|
});
|
|
2118
|
+
function applyRuntimeConfig(runtime) {
|
|
2119
|
+
if (runtime.enabled === false) {
|
|
2120
|
+
activeConfig = bootstrapConfig;
|
|
2121
|
+
clearCachedRuntimeConfig(config.projectId);
|
|
2122
|
+
syncCaptureModules();
|
|
2123
|
+
widget.updateConfig(activeConfig.widget);
|
|
2124
|
+
log.debug("Runtime SDK config disabled; using bootstrap config", { version: runtime.version });
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
activeConfig = mergeRuntimeConfig(activeConfig, runtime);
|
|
2128
|
+
syncCaptureModules();
|
|
2129
|
+
if (runtime.widget) widget.updateConfig(activeConfig.widget);
|
|
2130
|
+
log.debug("Applied runtime SDK config", { version: runtime.version });
|
|
2131
|
+
}
|
|
2132
|
+
if (config.runtimeConfig !== false) {
|
|
2133
|
+
const cached = readCachedRuntimeConfig(config.projectId);
|
|
2134
|
+
if (cached) applyRuntimeConfig(cached);
|
|
2135
|
+
apiClient.getSdkConfig().then((result) => {
|
|
2136
|
+
if (result.ok && result.data) {
|
|
2137
|
+
cacheRuntimeConfig(config.projectId, result.data);
|
|
2138
|
+
applyRuntimeConfig(result.data);
|
|
2139
|
+
} else if (result.error) {
|
|
2140
|
+
log.debug("Runtime SDK config unavailable", result.error);
|
|
2141
|
+
}
|
|
2142
|
+
}).catch((err) => {
|
|
2143
|
+
log.debug("Runtime SDK config fetch failed", { error: err instanceof Error ? err.message : String(err) });
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
1913
2146
|
log.info("Initialized", { projectId: config.projectId });
|
|
1914
2147
|
async function submitReport(category, description, intent) {
|
|
1915
2148
|
const filterResult = preFilter.check(description);
|
|
@@ -1959,9 +2192,9 @@ function createInstance(config) {
|
|
|
1959
2192
|
description: scrubbedDescription,
|
|
1960
2193
|
userIntent: intent,
|
|
1961
2194
|
environment: captureEnvironment(),
|
|
1962
|
-
consoleLogs: consoleCap?.getEntries(),
|
|
1963
|
-
networkLogs: networkCap?.getEntries(),
|
|
1964
|
-
performanceMetrics: perfCap?.getMetrics(),
|
|
2195
|
+
consoleLogs: activeConfig.capture?.console === false ? void 0 : consoleCap?.getEntries(),
|
|
2196
|
+
networkLogs: activeConfig.capture?.network === false ? void 0 : networkCap?.getEntries(),
|
|
2197
|
+
performanceMetrics: activeConfig.capture?.performance === false ? void 0 : perfCap?.getMetrics(),
|
|
1965
2198
|
screenshotDataUrl: pendingScreenshot ?? void 0,
|
|
1966
2199
|
selectedElement: pendingElement ?? void 0,
|
|
1967
2200
|
metadata: {
|
|
@@ -2033,9 +2266,27 @@ function createInstance(config) {
|
|
|
2033
2266
|
open() {
|
|
2034
2267
|
widget.open();
|
|
2035
2268
|
},
|
|
2269
|
+
openWith(category) {
|
|
2270
|
+
widget.open({ category });
|
|
2271
|
+
},
|
|
2272
|
+
show() {
|
|
2273
|
+
widget.showTrigger();
|
|
2274
|
+
},
|
|
2275
|
+
hide() {
|
|
2276
|
+
widget.hideTrigger();
|
|
2277
|
+
},
|
|
2278
|
+
attachTo(selectorOrElement, options) {
|
|
2279
|
+
return widget.attachTo(selectorOrElement, options);
|
|
2280
|
+
},
|
|
2281
|
+
setTrigger(trigger) {
|
|
2282
|
+
widget.setTrigger(trigger);
|
|
2283
|
+
},
|
|
2036
2284
|
close() {
|
|
2037
2285
|
widget.close();
|
|
2038
2286
|
},
|
|
2287
|
+
updateConfig(runtimeConfig) {
|
|
2288
|
+
applyRuntimeConfig(runtimeConfig);
|
|
2289
|
+
},
|
|
2039
2290
|
destroy() {
|
|
2040
2291
|
proactiveTriggers?.destroy();
|
|
2041
2292
|
proactiveManager?.reset();
|
|
@@ -2104,6 +2355,53 @@ function createInstance(config) {
|
|
|
2104
2355
|
};
|
|
2105
2356
|
return sdk;
|
|
2106
2357
|
}
|
|
2358
|
+
function mergeRuntimeConfig(config, runtime) {
|
|
2359
|
+
const nativeTrigger = runtime.native?.triggerMode;
|
|
2360
|
+
const widgetTrigger = runtime.widget?.trigger ?? (nativeTrigger === "none" || nativeTrigger === "shake" ? "manual" : void 0);
|
|
2361
|
+
return {
|
|
2362
|
+
...config,
|
|
2363
|
+
widget: {
|
|
2364
|
+
...config.widget,
|
|
2365
|
+
...runtime.widget,
|
|
2366
|
+
...widgetTrigger ? { trigger: widgetTrigger } : {}
|
|
2367
|
+
},
|
|
2368
|
+
capture: {
|
|
2369
|
+
...config.capture,
|
|
2370
|
+
...runtime.capture
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
function runtimeConfigCacheKey(projectId) {
|
|
2375
|
+
return `mushi:sdk-config:${projectId}`;
|
|
2376
|
+
}
|
|
2377
|
+
function readCachedRuntimeConfig(projectId) {
|
|
2378
|
+
if (typeof localStorage === "undefined") return null;
|
|
2379
|
+
try {
|
|
2380
|
+
const raw = localStorage.getItem(runtimeConfigCacheKey(projectId));
|
|
2381
|
+
if (!raw) return null;
|
|
2382
|
+
const parsed = JSON.parse(raw);
|
|
2383
|
+
return parsed.config ?? null;
|
|
2384
|
+
} catch {
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function cacheRuntimeConfig(projectId, config) {
|
|
2389
|
+
if (typeof localStorage === "undefined") return;
|
|
2390
|
+
try {
|
|
2391
|
+
localStorage.setItem(runtimeConfigCacheKey(projectId), JSON.stringify({
|
|
2392
|
+
cachedAt: Date.now(),
|
|
2393
|
+
config
|
|
2394
|
+
}));
|
|
2395
|
+
} catch {
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
function clearCachedRuntimeConfig(projectId) {
|
|
2399
|
+
if (typeof localStorage === "undefined") return;
|
|
2400
|
+
try {
|
|
2401
|
+
localStorage.removeItem(runtimeConfigCacheKey(projectId));
|
|
2402
|
+
} catch {
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2107
2405
|
function createNoopInstance() {
|
|
2108
2406
|
return {
|
|
2109
2407
|
report: () => {
|
|
@@ -2119,6 +2417,18 @@ function createNoopInstance() {
|
|
|
2119
2417
|
},
|
|
2120
2418
|
close: () => {
|
|
2121
2419
|
},
|
|
2420
|
+
updateConfig: () => {
|
|
2421
|
+
},
|
|
2422
|
+
openWith: () => {
|
|
2423
|
+
},
|
|
2424
|
+
show: () => {
|
|
2425
|
+
},
|
|
2426
|
+
hide: () => {
|
|
2427
|
+
},
|
|
2428
|
+
attachTo: () => () => {
|
|
2429
|
+
},
|
|
2430
|
+
setTrigger: () => {
|
|
2431
|
+
},
|
|
2122
2432
|
destroy: () => {
|
|
2123
2433
|
instance = null;
|
|
2124
2434
|
},
|