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