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