@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/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 { bottom: 24px; right: 24px; }
307
- .mushi-trigger.bottom-left { bottom: 24px; left: 24px; }
308
- .mushi-trigger.top-right { top: 24px; right: 24px; }
309
- .mushi-trigger.top-left { top: 24px; left: 24px; }
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 { bottom: 88px; right: 24px; --mushi-origin: bottom right; }
343
- .mushi-panel.bottom-left { bottom: 88px; left: 24px; --mushi-origin: bottom left; }
344
- .mushi-panel.top-right { top: 88px; right: 24px; --mushi-origin: top right; }
345
- .mushi-panel.top-left { top: 88px; left: 24px; --mushi-origin: top left; }
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
- const trigger = document.createElement("button");
959
- trigger.className = `mushi-trigger ${pos}`;
960
- trigger.textContent = this.config.triggerText;
961
- trigger.setAttribute("aria-label", t.widget.trigger);
962
- trigger.setAttribute("aria-haspopup", "dialog");
963
- trigger.setAttribute("aria-expanded", String(this.isOpen));
964
- trigger.style.zIndex = String(this.config.zIndex);
965
- trigger.addEventListener("click", () => {
966
- if (this.isOpen) this.close();
967
- else this.open();
968
- });
969
- this.shadow.appendChild(trigger);
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
- const consoleCap = config.capture?.console !== false ? createConsoleCapture() : null;
1820
- const networkCap = config.capture?.network !== false ? createNetworkCapture() : null;
1821
- const perfCap = config.capture?.performance !== false ? createPerformanceCapture() : null;
1822
- const screenshotCap = config.capture?.screenshot !== "off" ? createScreenshotCapture() : null;
1823
- const elementSelector = config.capture?.elementSelector !== false ? createElementSelector() : null;
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
  },