@morscherlab/mint-sdk 1.0.0-beta.7 → 1.0.0-rc.2

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.
Files changed (163) hide show
  1. package/README.md +9 -1
  2. package/dist/__tests__/components/LcmsSequenceTable.test.d.ts +1 -0
  3. package/dist/__tests__/components/ProgressBar.test.d.ts +1 -0
  4. package/dist/__tests__/components/RackEditor.test.d.ts +1 -0
  5. package/dist/__tests__/components/SequenceProgressBar.test.d.ts +1 -0
  6. package/dist/__tests__/composables/useExperimentSamples.test.d.ts +1 -0
  7. package/dist/__tests__/composables/useProtocolTemplates.test.d.ts +1 -0
  8. package/dist/__tests__/stores/settings.test.d.ts +1 -0
  9. package/dist/__tests__/utils/instrument.test.d.ts +1 -0
  10. package/dist/__tests__/utils/lcms.test.d.ts +1 -0
  11. package/dist/__tests__/utils/permissions.test.d.ts +1 -0
  12. package/dist/__tests__/utils/rack.test.d.ts +1 -0
  13. package/dist/{auth-QQj2kkze.js → auth-B7g4J4ZF.js} +148 -24
  14. package/dist/auth-B7g4J4ZF.js.map +1 -0
  15. package/dist/components/AutoGroupModal.vue.d.ts +1 -1
  16. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  17. package/dist/components/BaseToggle.vue.d.ts +2 -2
  18. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  19. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
  20. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +1 -1
  21. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
  22. package/dist/components/FormulaInput.vue.d.ts +1 -1
  23. package/dist/components/InstrumentAlertLog.vue.d.ts +22 -0
  24. package/dist/components/InstrumentStateBadge.vue.d.ts +11 -0
  25. package/dist/components/InstrumentStatusCard.vue.d.ts +13 -0
  26. package/dist/components/LcmsSequenceTable.vue.d.ts +26 -0
  27. package/dist/components/ProgressBar.vue.d.ts +1 -0
  28. package/dist/components/RackEditor.vue.d.ts +41 -3
  29. package/dist/components/ReagentList.vue.d.ts +1 -1
  30. package/dist/components/SampleSelector.vue.d.ts +5 -2
  31. package/dist/components/SegmentedControl.vue.d.ts +2 -0
  32. package/dist/components/SequenceInput.vue.d.ts +1 -1
  33. package/dist/components/SequenceProgressBar.vue.d.ts +15 -0
  34. package/dist/components/SettingsModal.vue.d.ts +8 -1
  35. package/dist/components/TagsInput.vue.d.ts +1 -1
  36. package/dist/components/WellPlate.vue.d.ts +42 -3
  37. package/dist/components/index.d.ts +5 -0
  38. package/dist/components/index.js +3 -3
  39. package/dist/{components-DihbSJjU.js → components-BhK-dW99.js} +2135 -1075
  40. package/dist/components-BhK-dW99.js.map +1 -0
  41. package/dist/composables/experimentDesignData.d.ts +17 -0
  42. package/dist/composables/index.d.ts +2 -0
  43. package/dist/composables/index.js +4 -4
  44. package/dist/composables/useControlSchema.d.ts +11 -0
  45. package/dist/composables/useExperimentData.d.ts +11 -3
  46. package/dist/composables/useExperimentSamples.d.ts +42 -0
  47. package/dist/composables/usePlatformContext.d.ts +54 -0
  48. package/dist/{composables-BcgZ6diz.js → composables-Bg7CFuNz.js} +5 -3
  49. package/dist/composables-Bg7CFuNz.js.map +1 -0
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.js +168 -6
  52. package/dist/index.js.map +1 -0
  53. package/dist/install.js +2 -2
  54. package/dist/instrument.d.ts +7 -0
  55. package/dist/lcms.d.ts +27 -0
  56. package/dist/permissions.d.ts +46 -0
  57. package/dist/stores/auth.d.ts +74 -2
  58. package/dist/stores/index.js +1 -1
  59. package/dist/styles.css +3186 -1070
  60. package/dist/templates/builders.d.ts +7 -3
  61. package/dist/templates/index.d.ts +2 -2
  62. package/dist/templates/index.js +2 -2
  63. package/dist/templates/presets.d.ts +12 -0
  64. package/dist/templates/types.d.ts +16 -1
  65. package/dist/{templates-Cyt0Suwf.js → templates-BorLR_7p.js} +324 -10
  66. package/dist/templates-BorLR_7p.js.map +1 -0
  67. package/dist/types/auth.d.ts +2 -0
  68. package/dist/types/components.d.ts +32 -3
  69. package/dist/types/form-builder.d.ts +2 -1
  70. package/dist/types/index.d.ts +4 -1
  71. package/dist/types/instrument.d.ts +56 -0
  72. package/dist/types/platform.d.ts +3 -0
  73. package/dist/{useExperimentData-CM6Y0u5L.js → useProtocolTemplates-n6AJqSqv.js} +627 -380
  74. package/dist/useProtocolTemplates-n6AJqSqv.js.map +1 -0
  75. package/dist/utils/rack.d.ts +47 -0
  76. package/package.json +1 -1
  77. package/src/__tests__/components/AppTopBar.test.ts +15 -0
  78. package/src/__tests__/components/BaseTabs.test.ts +15 -0
  79. package/src/__tests__/components/GroupAssigner.test.ts +18 -0
  80. package/src/__tests__/components/LcmsSequenceTable.test.ts +57 -0
  81. package/src/__tests__/components/ProgressBar.test.ts +18 -0
  82. package/src/__tests__/components/RackEditor.test.ts +125 -0
  83. package/src/__tests__/components/SampleSelector.test.ts +25 -0
  84. package/src/__tests__/components/SegmentedControl.test.ts +45 -0
  85. package/src/__tests__/components/SequenceProgressBar.test.ts +39 -0
  86. package/src/__tests__/components/SettingsModal.test.ts +83 -2
  87. package/src/__tests__/composables/useApi.test.ts +45 -0
  88. package/src/__tests__/composables/useAuth.test.ts +20 -0
  89. package/src/__tests__/composables/useControlSchema.test.ts +4 -0
  90. package/src/__tests__/composables/useExperimentData.test.ts +23 -0
  91. package/src/__tests__/composables/useExperimentSamples.test.ts +91 -0
  92. package/src/__tests__/composables/useProtocolTemplates.test.ts +64 -0
  93. package/src/__tests__/stores/settings.test.ts +78 -0
  94. package/src/__tests__/templates/templates.test.ts +86 -0
  95. package/src/__tests__/utils/instrument.test.ts +47 -0
  96. package/src/__tests__/utils/lcms.test.ts +73 -0
  97. package/src/__tests__/utils/permissions.test.ts +50 -0
  98. package/src/__tests__/utils/rack.test.ts +120 -0
  99. package/src/components/AppAvatarMenu.vue +6 -3
  100. package/src/components/AppTopBar.vue +16 -10
  101. package/src/components/AuditTrail.vue +1 -1
  102. package/src/components/BaseTabs.vue +22 -1
  103. package/src/components/Calendar.vue +6 -2
  104. package/src/components/ConcentrationInput.vue +3 -2
  105. package/src/components/GroupAssigner.vue +8 -3
  106. package/src/components/InstrumentAlertLog.vue +191 -0
  107. package/src/components/InstrumentStateBadge.vue +50 -0
  108. package/src/components/InstrumentStatusCard.vue +188 -0
  109. package/src/components/LcmsSequenceTable.vue +191 -0
  110. package/src/components/NumberInput.vue +5 -3
  111. package/src/components/ProgressBar.vue +3 -0
  112. package/src/components/RackEditor.vue +73 -2
  113. package/src/components/SampleHierarchyTree.vue +3 -2
  114. package/src/components/SampleSelector.vue +28 -9
  115. package/src/components/SegmentedControl.story.vue +17 -0
  116. package/src/components/SegmentedControl.vue +14 -3
  117. package/src/components/SequenceProgressBar.vue +71 -0
  118. package/src/components/SettingsModal.vue +49 -2
  119. package/src/components/UnitInput.vue +6 -2
  120. package/src/components/WellPlate.vue +145 -24
  121. package/src/components/index.ts +5 -0
  122. package/src/components/internal/WellEditPopupInternal.vue +1 -0
  123. package/src/composables/experimentDesignData.ts +182 -0
  124. package/src/composables/index.ts +14 -0
  125. package/src/composables/useApi.ts +113 -16
  126. package/src/composables/useAuth.ts +4 -0
  127. package/src/composables/useAutoGroup.ts +18 -9
  128. package/src/composables/useControlSchema.ts +21 -0
  129. package/src/composables/useExperimentData.ts +57 -16
  130. package/src/composables/useExperimentSamples.ts +142 -0
  131. package/src/composables/useProtocolTemplates.ts +13 -1
  132. package/src/composables/useRackEditor.ts +3 -2
  133. package/src/index.ts +27 -0
  134. package/src/instrument.ts +90 -0
  135. package/src/lcms.ts +108 -0
  136. package/src/permissions.ts +143 -0
  137. package/src/stores/auth.ts +79 -26
  138. package/src/stores/settings.ts +10 -0
  139. package/src/styles/components/instrument-monitor.css +478 -0
  140. package/src/styles/components/lcms-sequence-table.css +189 -0
  141. package/src/styles/components/sequence-progress-bar.css +63 -0
  142. package/src/styles/components/settings-modal.css +9 -0
  143. package/src/styles/components/tabs.css +9 -0
  144. package/src/styles/components/well-edit-popup.css +7 -1
  145. package/src/styles/components/well-plate.css +5 -0
  146. package/src/styles/index.css +3 -0
  147. package/src/templates/builders.ts +201 -0
  148. package/src/templates/controlSchemas.ts +68 -0
  149. package/src/templates/index.ts +2 -0
  150. package/src/templates/presets.ts +23 -0
  151. package/src/templates/types.ts +17 -0
  152. package/src/types/auth.ts +3 -0
  153. package/src/types/components.ts +45 -3
  154. package/src/types/form-builder.ts +2 -1
  155. package/src/types/index.ts +35 -0
  156. package/src/types/instrument.ts +61 -0
  157. package/src/types/platform.ts +4 -0
  158. package/src/utils/rack.ts +209 -0
  159. package/dist/auth-QQj2kkze.js.map +0 -1
  160. package/dist/components-DihbSJjU.js.map +0 -1
  161. package/dist/composables-BcgZ6diz.js.map +0 -1
  162. package/dist/templates-Cyt0Suwf.js.map +0 -1
  163. package/dist/useExperimentData-CM6Y0u5L.js.map +0 -1
@@ -1,6 +1,6 @@
1
- import { Bn as useConcentrationUnits, Bt as extractTemplateCollection, Fn as useControlWorkspace, In as getFieldRegistryEntry, Ln as getTypeDefault, Nt as createTemplateCollection, _ as toBioTemplateComponentPropsByComponent$1, _t as createBioTemplatePresetCollectionFromControls, an as getBioTemplatePackInfo, b as toBioTemplateComponentUsage, d as getBioTemplateComponentProps$1, g as toBioTemplateComponentProps, gt as createBioTemplatePresetCollection, h as toBioTemplateComponentImports, ht as createBioTemplatePackCollection, i as createBioTemplateControlToolkit, m as toBioTemplateComponentBindingsById, p as toBioTemplateComponentBindings, tn as getBioTemplatePresetInfo, u as getBioTemplateComponentBindings, v as toBioTemplateComponentPropsById, y as toBioTemplateComponentSnippets, zt as ensureTemplateFromCollection } from "./templates-Cyt0Suwf.js";
2
- import { r as useSettingsStore, t as useAuthStore } from "./auth-QQj2kkze.js";
3
- import { computed, effectScope, getCurrentScope, onMounted, onScopeDispose, onUnmounted, provide, reactive, readonly, ref, shallowRef, toRaw, toValue, watch } from "vue";
1
+ import { Bt as ensureTemplateFromCollection, In as useControlWorkspace, Ln as getFieldRegistryEntry, Pt as createTemplateCollection, Rn as getTypeDefault, Vn as useConcentrationUnits, Vt as extractTemplateCollection, _ as toBioTemplateComponentPropsByComponent$1, _t as createBioTemplatePresetCollectionFromControls, b as toBioTemplateComponentUsage, d as getBioTemplateComponentProps$1, g as toBioTemplateComponentProps, gt as createBioTemplatePresetCollection, h as toBioTemplateComponentImports, ht as createBioTemplatePackCollection, i as createBioTemplateControlToolkit, m as toBioTemplateComponentBindingsById, nn as getBioTemplatePresetInfo, on as getBioTemplatePackInfo, p as toBioTemplateComponentBindings, u as getBioTemplateComponentBindings, v as toBioTemplateComponentPropsById, y as toBioTemplateComponentSnippets } from "./templates-BorLR_7p.js";
2
+ import { g as useSettingsStore, t as useAuthStore } from "./auth-B7g4J4ZF.js";
3
+ import { computed, effectScope, getCurrentScope, inject, onMounted, onScopeDispose, onUnmounted, provide, reactive, readonly, ref, shallowRef, toRaw, toValue, watch } from "vue";
4
4
  import axios from "axios";
5
5
  //#region src/composables/useSortedItems.ts
6
6
  /** Shared sorting for SDK tables and lists with stable empty-value handling. */
@@ -673,6 +673,223 @@ function useFormBuilder(schema, initialData, enhancements) {
673
673
  };
674
674
  }
675
675
  //#endregion
676
+ //#region src/composables/usePlatformContext.ts
677
+ var DEFAULT_CONTEXT = {
678
+ isIntegrated: false,
679
+ theme: "system"
680
+ };
681
+ var platformContext = ref({ ...DEFAULT_CONTEXT });
682
+ var inferredOrigins = /* @__PURE__ */ new Set();
683
+ var allowedOrigins = /* @__PURE__ */ new Set();
684
+ var allowAnyOrigin = false;
685
+ var initialized = false;
686
+ var listenerCount = 0;
687
+ var nextConsumerId = 0;
688
+ var currentHandler = null;
689
+ var consumerOrigins = /* @__PURE__ */ new Map();
690
+ var allowAnyOriginConsumers = /* @__PURE__ */ new Set();
691
+ /**
692
+ * Derive origin from URL (protocol + host)
693
+ */
694
+ function getOriginFromUrl(url) {
695
+ try {
696
+ return new URL(url).origin;
697
+ } catch {
698
+ return null;
699
+ }
700
+ }
701
+ function normalizeAllowedOrigins(origins) {
702
+ const normalized = /* @__PURE__ */ new Set();
703
+ if (!origins) return normalized;
704
+ for (const origin of origins) normalized.add(getOriginFromUrl(origin) || origin);
705
+ return normalized;
706
+ }
707
+ function recomputeOriginPolicy() {
708
+ allowedOrigins = new Set(inferredOrigins);
709
+ for (const origins of consumerOrigins.values()) for (const origin of origins) allowedOrigins.add(origin);
710
+ allowAnyOrigin = allowAnyOriginConsumers.size > 0;
711
+ }
712
+ function resetPlatformContextState() {
713
+ inferredOrigins = /* @__PURE__ */ new Set();
714
+ allowedOrigins = /* @__PURE__ */ new Set();
715
+ allowAnyOrigin = false;
716
+ initialized = false;
717
+ listenerCount = 0;
718
+ currentHandler = null;
719
+ consumerOrigins.clear();
720
+ allowAnyOriginConsumers.clear();
721
+ platformContext.value = { ...DEFAULT_CONTEXT };
722
+ }
723
+ /**
724
+ * Check if an origin is allowed for postMessage communication
725
+ */
726
+ function isOriginAllowed(origin) {
727
+ if (allowAnyOrigin) {
728
+ console.warn("[MINT SDK] postMessage origin validation disabled - only use in development");
729
+ return true;
730
+ }
731
+ if (origin === window.location.origin) return true;
732
+ return allowedOrigins.has(origin);
733
+ }
734
+ /**
735
+ * Platform context composable for plugin integration with MINT Platform.
736
+ *
737
+ * Provides secure communication with the parent platform via postMessage.
738
+ *
739
+ * @param options - Configuration options
740
+ * @param options.allowedOrigins - List of allowed origins for postMessage
741
+ * @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)
742
+ *
743
+ * @example
744
+ * ```typescript
745
+ * // Basic usage - derives origin from platform injection
746
+ * const { isIntegrated, user, theme } = usePlatformContext()
747
+ *
748
+ * // With explicit allowed origins
749
+ * const { isIntegrated } = usePlatformContext({
750
+ * allowedOrigins: ['https://mint.example.com']
751
+ * })
752
+ *
753
+ * // Development mode (UNSAFE)
754
+ * const { isIntegrated } = usePlatformContext({
755
+ * allowAnyOrigin: import.meta.env.DEV
756
+ * })
757
+ * ```
758
+ */
759
+ /** Connects a plugin to the MINT platform via postMessage, exposing user, theme, and experiment context. */
760
+ function usePlatformContext(options = {}) {
761
+ const consumerId = ++nextConsumerId;
762
+ const instanceOrigins = normalizeAllowedOrigins(options.allowedOrigins);
763
+ const instanceAllowAnyOrigin = options.allowAnyOrigin === true;
764
+ function detectPlatform() {
765
+ const detectedOrigins = /* @__PURE__ */ new Set();
766
+ const platformData = window.__MINT_PLATFORM__;
767
+ if (platformData) {
768
+ platformContext.value = {
769
+ ...platformData,
770
+ isIntegrated: true
771
+ };
772
+ if (platformData.platformOrigin) detectedOrigins.add(platformData.platformOrigin);
773
+ else if (platformData.platformApiUrl) {
774
+ const origin = getOriginFromUrl(platformData.platformApiUrl);
775
+ if (origin) detectedOrigins.add(origin);
776
+ }
777
+ } else {
778
+ const urlParams = new URLSearchParams(window.location.search);
779
+ const hasPluginParam = urlParams.has("mint-plugin");
780
+ const platformOrigin = urlParams.get("mint-origin");
781
+ if (platformOrigin) {
782
+ const origin = getOriginFromUrl(platformOrigin);
783
+ if (origin) detectedOrigins.add(origin);
784
+ }
785
+ platformContext.value = {
786
+ isIntegrated: hasPluginParam,
787
+ theme: (() => {
788
+ try {
789
+ const s = localStorage.getItem("mint-settings");
790
+ if (s) return JSON.parse(s).theme || "system";
791
+ } catch {}
792
+ return "system";
793
+ })(),
794
+ platformOrigin: platformOrigin || void 0
795
+ };
796
+ }
797
+ inferredOrigins = detectedOrigins;
798
+ }
799
+ function handlePlatformMessage(event) {
800
+ if (event.source !== window.parent) return;
801
+ if (!isOriginAllowed(event.origin)) {
802
+ console.warn(`[MINT SDK] Rejected postMessage from untrusted origin: ${event.origin}`);
803
+ return;
804
+ }
805
+ try {
806
+ const platformEvent = event.data;
807
+ if (!platformEvent.type?.startsWith("mint:")) return;
808
+ switch (platformEvent.type) {
809
+ case "mint:theme-changed":
810
+ platformContext.value.theme = platformEvent.payload;
811
+ break;
812
+ case "mint:user-changed":
813
+ platformContext.value.user = platformEvent.payload;
814
+ break;
815
+ }
816
+ } catch {}
817
+ }
818
+ /**
819
+ * Send a message to the parent platform.
820
+ * Uses validated target origin for security.
821
+ */
822
+ function sendToPlatform(event) {
823
+ if (!platformContext.value.isIntegrated || window.parent === window) return;
824
+ let targetOrigin;
825
+ if (platformContext.value.platformOrigin) targetOrigin = platformContext.value.platformOrigin;
826
+ else if (allowedOrigins.size > 0) targetOrigin = allowedOrigins.values().next().value;
827
+ else if (allowAnyOrigin) {
828
+ targetOrigin = "*";
829
+ console.warn("[MINT SDK] Using wildcard origin for postMessage - only use in development");
830
+ } else {
831
+ console.warn("[MINT SDK] Cannot send postMessage: no platform origin configured");
832
+ return;
833
+ }
834
+ window.parent.postMessage(event, targetOrigin);
835
+ }
836
+ /**
837
+ * Request navigation to a path in the platform.
838
+ */
839
+ function navigate(path) {
840
+ sendToPlatform({
841
+ type: "mint:navigate",
842
+ payload: path
843
+ });
844
+ }
845
+ /**
846
+ * Show a notification in the platform.
847
+ */
848
+ function notify(message, type = "info") {
849
+ sendToPlatform({
850
+ type: "mint:notification",
851
+ payload: {
852
+ message,
853
+ type
854
+ }
855
+ });
856
+ }
857
+ onMounted(() => {
858
+ consumerOrigins.set(consumerId, instanceOrigins);
859
+ if (instanceAllowAnyOrigin) allowAnyOriginConsumers.add(consumerId);
860
+ if (!initialized) {
861
+ detectPlatform();
862
+ currentHandler = handlePlatformMessage;
863
+ window.addEventListener("message", handlePlatformMessage);
864
+ initialized = true;
865
+ }
866
+ listenerCount++;
867
+ recomputeOriginPolicy();
868
+ });
869
+ onUnmounted(() => {
870
+ consumerOrigins.delete(consumerId);
871
+ allowAnyOriginConsumers.delete(consumerId);
872
+ listenerCount = Math.max(0, listenerCount - 1);
873
+ if (listenerCount === 0) {
874
+ if (currentHandler) window.removeEventListener("message", currentHandler);
875
+ resetPlatformContextState();
876
+ return;
877
+ }
878
+ recomputeOriginPolicy();
879
+ });
880
+ return {
881
+ context: platformContext,
882
+ isIntegrated: computed(() => platformContext.value.isIntegrated),
883
+ plugin: computed(() => platformContext.value.plugin),
884
+ user: computed(() => platformContext.value.user),
885
+ theme: computed(() => platformContext.value.theme),
886
+ features: computed(() => platformContext.value.features),
887
+ navigate,
888
+ notify,
889
+ sendToPlatform
890
+ };
891
+ }
892
+ //#endregion
676
893
  //#region src/composables/experiment-utils.ts
677
894
  function formatExperimentDate(dateStr) {
678
895
  try {
@@ -784,10 +1001,56 @@ var apiClientInstance = null;
784
1001
  var interceptorAttached = false;
785
1002
  function joinUrlPath(baseUrl, path) {
786
1003
  if (!path) return baseUrl;
1004
+ if (path.startsWith("?") || path.startsWith("#")) return `${baseUrl.replace(/\/+$/, "")}${path}`;
787
1005
  const normalizedBase = baseUrl.replace(/\/+$/, "");
788
1006
  const normalizedPath = path.replace(/^\/+/, "/");
789
1007
  return `${normalizedBase}${normalizedPath.startsWith("/") ? normalizedPath : `/${normalizedPath}`}`;
790
1008
  }
1009
+ function getBasePath(baseUrl) {
1010
+ if (!baseUrl) return "/";
1011
+ try {
1012
+ const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost";
1013
+ return new URL(baseUrl, origin).pathname.replace(/\/+$/, "") || "/";
1014
+ } catch {
1015
+ return baseUrl.replace(/^https?:\/\/[^/]+/i, "").replace(/\/+$/, "") || "/";
1016
+ }
1017
+ }
1018
+ function normalizeRequestUrl(baseUrl, url) {
1019
+ if (!url || /^https?:\/\//.test(url)) return url;
1020
+ const basePath = getBasePath(baseUrl);
1021
+ if (basePath === "/") return url;
1022
+ const normalizedUrl = url.startsWith("/") ? url : `/${url}`;
1023
+ if (normalizedUrl === basePath || normalizedUrl.startsWith(`${basePath}?`) || normalizedUrl.startsWith(`${basePath}#`)) return normalizedUrl.slice(basePath.length);
1024
+ if (normalizedUrl.startsWith(`${basePath}/`)) return normalizedUrl.slice(basePath.length) || "";
1025
+ return url;
1026
+ }
1027
+ function asMutableHeaders(headers) {
1028
+ return headers ? headers : null;
1029
+ }
1030
+ function hasAuthorizationHeader(headers) {
1031
+ const bag = asMutableHeaders(headers);
1032
+ if (!bag) return false;
1033
+ if (typeof bag.has === "function") return bag.has("Authorization");
1034
+ return Object.keys(bag).some((key) => key.toLowerCase() === "authorization");
1035
+ }
1036
+ function setAuthorizationHeader(headers, value) {
1037
+ const bag = asMutableHeaders(headers);
1038
+ if (!bag) return;
1039
+ if (typeof bag.set === "function") {
1040
+ bag.set("Authorization", value);
1041
+ return;
1042
+ }
1043
+ bag.Authorization = value;
1044
+ }
1045
+ function deleteAuthorizationHeader(headers) {
1046
+ const bag = asMutableHeaders(headers);
1047
+ if (!bag) return;
1048
+ if (typeof bag.delete === "function") {
1049
+ bag.delete("Authorization");
1050
+ return;
1051
+ }
1052
+ for (const key of Object.keys(bag)) if (key.toLowerCase() === "authorization") delete bag[key];
1053
+ }
791
1054
  function getApiClient() {
792
1055
  if (!apiClientInstance) apiClientInstance = axios.create({ headers: { "Content-Type": "application/json" } });
793
1056
  return apiClientInstance;
@@ -800,37 +1063,50 @@ function useApi(options = {}) {
800
1063
  if (!authStore.isInitialized) authStore.initialize();
801
1064
  if (!interceptorAttached) {
802
1065
  apiClient.interceptors.request.use((config) => {
803
- if (authStore.token && config.headers && !config.headers.Authorization) config.headers.Authorization = `Bearer ${authStore.token}`;
1066
+ const request = config;
1067
+ if (request._mintSkipAuth) {
1068
+ delete request._mintSkipAuth;
1069
+ deleteAuthorizationHeader(request.headers);
1070
+ return request;
1071
+ }
1072
+ const currentAuthStore = useAuthStore();
1073
+ if (currentAuthStore.token && config.headers && !hasAuthorizationHeader(config.headers)) setAuthorizationHeader(config.headers, `Bearer ${currentAuthStore.token}`);
804
1074
  return config;
805
1075
  });
806
1076
  interceptorAttached = true;
807
1077
  }
1078
+ function getBaseUrl() {
1079
+ return options.baseUrl ?? settingsStore.getApiBaseUrl();
1080
+ }
1081
+ function normalizeUrl(url) {
1082
+ return normalizeRequestUrl(getBaseUrl(), url);
1083
+ }
808
1084
  function requestConfig(config) {
809
1085
  const base = {
810
- baseURL: options.baseUrl ?? settingsStore.getApiBaseUrl(),
1086
+ baseURL: getBaseUrl(),
811
1087
  timeout: options.timeout ?? settingsStore.requestTimeout,
812
1088
  ...config
813
1089
  };
814
- if (options.withAuth === false) base.headers = {
815
- ...base.headers,
816
- Authorization: void 0
817
- };
1090
+ if (options.withAuth === false) {
1091
+ base._mintSkipAuth = true;
1092
+ deleteAuthorizationHeader(base.headers);
1093
+ }
818
1094
  return base;
819
1095
  }
820
1096
  async function get(url, config) {
821
- return (await apiClient.get(url, requestConfig(config))).data;
1097
+ return (await apiClient.get(normalizeUrl(url), requestConfig(config))).data;
822
1098
  }
823
1099
  async function post(url, data, config) {
824
- return (await apiClient.post(url, data, requestConfig(config))).data;
1100
+ return (await apiClient.post(normalizeUrl(url), data, requestConfig(config))).data;
825
1101
  }
826
1102
  async function put(url, data, config) {
827
- return (await apiClient.put(url, data, requestConfig(config))).data;
1103
+ return (await apiClient.put(normalizeUrl(url), data, requestConfig(config))).data;
828
1104
  }
829
1105
  async function patch(url, data, config) {
830
- return (await apiClient.patch(url, data, requestConfig(config))).data;
1106
+ return (await apiClient.patch(normalizeUrl(url), data, requestConfig(config))).data;
831
1107
  }
832
1108
  async function del(url, config) {
833
- return (await apiClient.delete(url, requestConfig(config))).data;
1109
+ return (await apiClient.delete(normalizeUrl(url), requestConfig(config))).data;
834
1110
  }
835
1111
  async function upload(url, file, fieldName = "file", additionalData) {
836
1112
  const formData = new FormData();
@@ -838,10 +1114,10 @@ function useApi(options = {}) {
838
1114
  if (additionalData) Object.entries(additionalData).forEach(([key, value]) => {
839
1115
  if (value !== void 0 && value !== null) formData.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
840
1116
  });
841
- return (await apiClient.post(url, formData, requestConfig({ headers: { "Content-Type": void 0 } }))).data;
1117
+ return (await apiClient.post(normalizeUrl(url), formData, requestConfig({ headers: { "Content-Type": void 0 } }))).data;
842
1118
  }
843
1119
  async function download(url, filename) {
844
- const response = await apiClient.get(url, requestConfig({ responseType: "blob" }));
1120
+ const response = await apiClient.get(normalizeUrl(url), requestConfig({ responseType: "blob" }));
845
1121
  const blob = new Blob([response.data]);
846
1122
  const blobUrl = URL.createObjectURL(blob);
847
1123
  if (filename) {
@@ -857,7 +1133,8 @@ function useApi(options = {}) {
857
1133
  return blobUrl;
858
1134
  }
859
1135
  function buildUrl(path) {
860
- return joinUrlPath(options.baseUrl ?? settingsStore.getApiBaseUrl(), path);
1136
+ const baseUrl = getBaseUrl();
1137
+ return joinUrlPath(baseUrl, normalizeRequestUrl(baseUrl, path));
861
1138
  }
862
1139
  function buildWsUrl(path) {
863
1140
  return joinUrlPath(settingsStore.getWsBaseUrl(), path);
@@ -1132,269 +1409,52 @@ function useExperimentSelector(options = {}) {
1132
1409
  const groups = /* @__PURE__ */ new Map();
1133
1410
  for (const exp of experiments.value) {
1134
1411
  const key = exp.project_name ?? exp.project ?? "No project";
1135
- const list = groups.get(key);
1136
- if (list) list.push(exp);
1137
- else groups.set(key, [exp]);
1138
- }
1139
- return [...groups.entries()].sort(([a], [b]) => {
1140
- if (a === "No project") return 1;
1141
- if (b === "No project") return -1;
1142
- return a.localeCompare(b);
1143
- });
1144
- });
1145
- const searchWatch = useDebouncedWatch(() => filters.search, () => {
1146
- page.value = 0;
1147
- fetchExperiments();
1148
- }, { delay: 300 });
1149
- watch(() => [
1150
- filters.status,
1151
- filters.project,
1152
- filters.experimentType,
1153
- filters.datePreset,
1154
- sortKey.value
1155
- ], () => {
1156
- searchWatch.cancel();
1157
- page.value = 0;
1158
- fetchExperiments();
1159
- });
1160
- if (immediate) fetchExperiments();
1161
- return {
1162
- experiments,
1163
- total,
1164
- selectedExperiment,
1165
- filters,
1166
- isLoading,
1167
- error,
1168
- lastLoadedAt,
1169
- page,
1170
- hasMore,
1171
- sortKey,
1172
- experimentTypes,
1173
- projects,
1174
- groupedByProject,
1175
- fetch: fetchExperiments,
1176
- loadMore,
1177
- reset,
1178
- select,
1179
- clear,
1180
- fetchFilterOptions
1181
- };
1182
- }
1183
- //#endregion
1184
- //#region src/composables/usePlatformContext.ts
1185
- var DEFAULT_CONTEXT = {
1186
- isIntegrated: false,
1187
- theme: "system"
1188
- };
1189
- var platformContext = ref({ ...DEFAULT_CONTEXT });
1190
- var inferredOrigins = /* @__PURE__ */ new Set();
1191
- var allowedOrigins = /* @__PURE__ */ new Set();
1192
- var allowAnyOrigin = false;
1193
- var initialized = false;
1194
- var listenerCount = 0;
1195
- var nextConsumerId = 0;
1196
- var currentHandler = null;
1197
- var consumerOrigins = /* @__PURE__ */ new Map();
1198
- var allowAnyOriginConsumers = /* @__PURE__ */ new Set();
1199
- /**
1200
- * Derive origin from URL (protocol + host)
1201
- */
1202
- function getOriginFromUrl(url) {
1203
- try {
1204
- return new URL(url).origin;
1205
- } catch {
1206
- return null;
1207
- }
1208
- }
1209
- function normalizeAllowedOrigins(origins) {
1210
- const normalized = /* @__PURE__ */ new Set();
1211
- if (!origins) return normalized;
1212
- for (const origin of origins) normalized.add(getOriginFromUrl(origin) || origin);
1213
- return normalized;
1214
- }
1215
- function recomputeOriginPolicy() {
1216
- allowedOrigins = new Set(inferredOrigins);
1217
- for (const origins of consumerOrigins.values()) for (const origin of origins) allowedOrigins.add(origin);
1218
- allowAnyOrigin = allowAnyOriginConsumers.size > 0;
1219
- }
1220
- function resetPlatformContextState() {
1221
- inferredOrigins = /* @__PURE__ */ new Set();
1222
- allowedOrigins = /* @__PURE__ */ new Set();
1223
- allowAnyOrigin = false;
1224
- initialized = false;
1225
- listenerCount = 0;
1226
- currentHandler = null;
1227
- consumerOrigins.clear();
1228
- allowAnyOriginConsumers.clear();
1229
- platformContext.value = { ...DEFAULT_CONTEXT };
1230
- }
1231
- /**
1232
- * Check if an origin is allowed for postMessage communication
1233
- */
1234
- function isOriginAllowed(origin) {
1235
- if (allowAnyOrigin) {
1236
- console.warn("[MINT SDK] postMessage origin validation disabled - only use in development");
1237
- return true;
1238
- }
1239
- if (origin === window.location.origin) return true;
1240
- return allowedOrigins.has(origin);
1241
- }
1242
- /**
1243
- * Platform context composable for plugin integration with MINT Platform.
1244
- *
1245
- * Provides secure communication with the parent platform via postMessage.
1246
- *
1247
- * @param options - Configuration options
1248
- * @param options.allowedOrigins - List of allowed origins for postMessage
1249
- * @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)
1250
- *
1251
- * @example
1252
- * ```typescript
1253
- * // Basic usage - derives origin from platform injection
1254
- * const { isIntegrated, user, theme } = usePlatformContext()
1255
- *
1256
- * // With explicit allowed origins
1257
- * const { isIntegrated } = usePlatformContext({
1258
- * allowedOrigins: ['https://mint.example.com']
1259
- * })
1260
- *
1261
- * // Development mode (UNSAFE)
1262
- * const { isIntegrated } = usePlatformContext({
1263
- * allowAnyOrigin: import.meta.env.DEV
1264
- * })
1265
- * ```
1266
- */
1267
- /** Connects a plugin to the MINT platform via postMessage, exposing user, theme, and experiment context. */
1268
- function usePlatformContext(options = {}) {
1269
- const consumerId = ++nextConsumerId;
1270
- const instanceOrigins = normalizeAllowedOrigins(options.allowedOrigins);
1271
- const instanceAllowAnyOrigin = options.allowAnyOrigin === true;
1272
- function detectPlatform() {
1273
- const detectedOrigins = /* @__PURE__ */ new Set();
1274
- const platformData = window.__MINT_PLATFORM__;
1275
- if (platformData) {
1276
- platformContext.value = {
1277
- ...platformData,
1278
- isIntegrated: true
1279
- };
1280
- if (platformData.platformOrigin) detectedOrigins.add(platformData.platformOrigin);
1281
- else if (platformData.platformApiUrl) {
1282
- const origin = getOriginFromUrl(platformData.platformApiUrl);
1283
- if (origin) detectedOrigins.add(origin);
1284
- }
1285
- } else {
1286
- const urlParams = new URLSearchParams(window.location.search);
1287
- const hasPluginParam = urlParams.has("mint-plugin");
1288
- const platformOrigin = urlParams.get("mint-origin");
1289
- if (platformOrigin) {
1290
- const origin = getOriginFromUrl(platformOrigin);
1291
- if (origin) detectedOrigins.add(origin);
1292
- }
1293
- platformContext.value = {
1294
- isIntegrated: hasPluginParam,
1295
- theme: (() => {
1296
- try {
1297
- const s = localStorage.getItem("mint-settings");
1298
- if (s) return JSON.parse(s).theme || "system";
1299
- } catch {}
1300
- return "system";
1301
- })(),
1302
- platformOrigin: platformOrigin || void 0
1303
- };
1304
- }
1305
- inferredOrigins = detectedOrigins;
1306
- }
1307
- function handlePlatformMessage(event) {
1308
- if (event.source !== window.parent) return;
1309
- if (!isOriginAllowed(event.origin)) {
1310
- console.warn(`[MINT SDK] Rejected postMessage from untrusted origin: ${event.origin}`);
1311
- return;
1312
- }
1313
- try {
1314
- const platformEvent = event.data;
1315
- if (!platformEvent.type?.startsWith("mint:")) return;
1316
- switch (platformEvent.type) {
1317
- case "mint:theme-changed":
1318
- platformContext.value.theme = platformEvent.payload;
1319
- break;
1320
- case "mint:user-changed":
1321
- platformContext.value.user = platformEvent.payload;
1322
- break;
1323
- }
1324
- } catch {}
1325
- }
1326
- /**
1327
- * Send a message to the parent platform.
1328
- * Uses validated target origin for security.
1329
- */
1330
- function sendToPlatform(event) {
1331
- if (!platformContext.value.isIntegrated || window.parent === window) return;
1332
- let targetOrigin;
1333
- if (platformContext.value.platformOrigin) targetOrigin = platformContext.value.platformOrigin;
1334
- else if (allowedOrigins.size > 0) targetOrigin = allowedOrigins.values().next().value;
1335
- else if (allowAnyOrigin) {
1336
- targetOrigin = "*";
1337
- console.warn("[MINT SDK] Using wildcard origin for postMessage - only use in development");
1338
- } else {
1339
- console.warn("[MINT SDK] Cannot send postMessage: no platform origin configured");
1340
- return;
1341
- }
1342
- window.parent.postMessage(event, targetOrigin);
1343
- }
1344
- /**
1345
- * Request navigation to a path in the platform.
1346
- */
1347
- function navigate(path) {
1348
- sendToPlatform({
1349
- type: "mint:navigate",
1350
- payload: path
1351
- });
1352
- }
1353
- /**
1354
- * Show a notification in the platform.
1355
- */
1356
- function notify(message, type = "info") {
1357
- sendToPlatform({
1358
- type: "mint:notification",
1359
- payload: {
1360
- message,
1361
- type
1362
- }
1363
- });
1364
- }
1365
- onMounted(() => {
1366
- consumerOrigins.set(consumerId, instanceOrigins);
1367
- if (instanceAllowAnyOrigin) allowAnyOriginConsumers.add(consumerId);
1368
- if (!initialized) {
1369
- detectPlatform();
1370
- currentHandler = handlePlatformMessage;
1371
- window.addEventListener("message", handlePlatformMessage);
1372
- initialized = true;
1373
- }
1374
- listenerCount++;
1375
- recomputeOriginPolicy();
1376
- });
1377
- onUnmounted(() => {
1378
- consumerOrigins.delete(consumerId);
1379
- allowAnyOriginConsumers.delete(consumerId);
1380
- listenerCount = Math.max(0, listenerCount - 1);
1381
- if (listenerCount === 0) {
1382
- if (currentHandler) window.removeEventListener("message", currentHandler);
1383
- resetPlatformContextState();
1384
- return;
1412
+ const list = groups.get(key);
1413
+ if (list) list.push(exp);
1414
+ else groups.set(key, [exp]);
1385
1415
  }
1386
- recomputeOriginPolicy();
1416
+ return [...groups.entries()].sort(([a], [b]) => {
1417
+ if (a === "No project") return 1;
1418
+ if (b === "No project") return -1;
1419
+ return a.localeCompare(b);
1420
+ });
1421
+ });
1422
+ const searchWatch = useDebouncedWatch(() => filters.search, () => {
1423
+ page.value = 0;
1424
+ fetchExperiments();
1425
+ }, { delay: 300 });
1426
+ watch(() => [
1427
+ filters.status,
1428
+ filters.project,
1429
+ filters.experimentType,
1430
+ filters.datePreset,
1431
+ sortKey.value
1432
+ ], () => {
1433
+ searchWatch.cancel();
1434
+ page.value = 0;
1435
+ fetchExperiments();
1387
1436
  });
1437
+ if (immediate) fetchExperiments();
1388
1438
  return {
1389
- context: platformContext,
1390
- isIntegrated: computed(() => platformContext.value.isIntegrated),
1391
- plugin: computed(() => platformContext.value.plugin),
1392
- user: computed(() => platformContext.value.user),
1393
- theme: computed(() => platformContext.value.theme),
1394
- features: computed(() => platformContext.value.features),
1395
- navigate,
1396
- notify,
1397
- sendToPlatform
1439
+ experiments,
1440
+ total,
1441
+ selectedExperiment,
1442
+ filters,
1443
+ isLoading,
1444
+ error,
1445
+ lastLoadedAt,
1446
+ page,
1447
+ hasMore,
1448
+ sortKey,
1449
+ experimentTypes,
1450
+ projects,
1451
+ groupedByProject,
1452
+ fetch: fetchExperiments,
1453
+ loadMore,
1454
+ reset,
1455
+ select,
1456
+ clear,
1457
+ fetchFilterOptions
1398
1458
  };
1399
1459
  }
1400
1460
  //#endregion
@@ -2038,6 +2098,108 @@ function useWellPlateEditor(initialState, options = {}) {
2038
2098
  };
2039
2099
  }
2040
2100
  //#endregion
2101
+ //#region src/composables/experimentDesignData.ts
2102
+ function isRecord(value) {
2103
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2104
+ }
2105
+ function isPlatformDesignDataResponse(value) {
2106
+ return "data" in value && ("experiment_id" in value || "plugin_id" in value || "schema_version" in value || "updated_at" in value);
2107
+ }
2108
+ /** Return the plugin-defined design_data payload from common platform and plugin response shapes. */
2109
+ function unwrapExperimentDesignData(rawData) {
2110
+ if (!rawData) return null;
2111
+ if (isRecord(rawData.data) && isPlatformDesignDataResponse(rawData)) return rawData.data;
2112
+ if (isRecord(rawData.design_data)) return rawData.design_data;
2113
+ if (isRecord(rawData.designData)) return rawData.designData;
2114
+ return rawData;
2115
+ }
2116
+ function shouldIncludeSample(record, options) {
2117
+ if (options.includeControls) return true;
2118
+ const rawType = record.sample_type ?? record.sampleType ?? record.type ?? record.well_type ?? record.wellType;
2119
+ const type = typeof rawType === "string" ? rawType.toLowerCase() : "";
2120
+ return !["blank", "qc"].includes(type);
2121
+ }
2122
+ function readSampleValue(record) {
2123
+ const candidates = [
2124
+ record.sample_name,
2125
+ record.sampleName,
2126
+ record.sample_id,
2127
+ record.sampleId,
2128
+ record.name,
2129
+ record.id,
2130
+ record.label
2131
+ ];
2132
+ for (const candidate of candidates) {
2133
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
2134
+ if (typeof candidate === "number" && Number.isFinite(candidate)) return String(candidate);
2135
+ }
2136
+ return null;
2137
+ }
2138
+ function readSampleLabel(record, fallback) {
2139
+ const label = record.name ?? record.label ?? record.sample_name ?? record.sampleName;
2140
+ return typeof label === "string" && label.trim() ? label : fallback;
2141
+ }
2142
+ function readSampleDescription(record) {
2143
+ const parts = [
2144
+ record.group,
2145
+ record.batch,
2146
+ record.batch_id,
2147
+ record.batchId,
2148
+ record.plate_id,
2149
+ record.plateId,
2150
+ record.well_id,
2151
+ record.wellId
2152
+ ].filter((value) => typeof value === "string" && value.trim().length > 0 || typeof value === "number");
2153
+ return parts.length > 0 ? parts.map(String).join(" / ") : void 0;
2154
+ }
2155
+ function addSampleRecord(options, seen, value, extractOptions) {
2156
+ if (typeof value === "string") {
2157
+ const trimmed = value.trim();
2158
+ if (trimmed && !seen.has(trimmed)) {
2159
+ seen.add(trimmed);
2160
+ options.push({
2161
+ value: trimmed,
2162
+ label: trimmed
2163
+ });
2164
+ }
2165
+ return;
2166
+ }
2167
+ if (!isRecord(value) || !shouldIncludeSample(value, extractOptions)) return;
2168
+ const sampleValue = readSampleValue(value);
2169
+ if (!sampleValue || seen.has(sampleValue)) return;
2170
+ seen.add(sampleValue);
2171
+ options.push({
2172
+ value: sampleValue,
2173
+ label: readSampleLabel(value, sampleValue),
2174
+ description: readSampleDescription(value)
2175
+ });
2176
+ }
2177
+ function addSampleArray(options, seen, value, extractOptions) {
2178
+ if (!Array.isArray(value)) return;
2179
+ for (const sample of value) addSampleRecord(options, seen, sample, extractOptions);
2180
+ }
2181
+ /** Extract selectable sample options from raw or wrapped experiment design_data. */
2182
+ function extractSampleOptionsFromDesignData(rawData, options = {}) {
2183
+ const designData = unwrapExperimentDesignData(rawData);
2184
+ if (!designData) return [];
2185
+ const sampleOptions = [];
2186
+ const seen = /* @__PURE__ */ new Set();
2187
+ addSampleArray(sampleOptions, seen, designData.samples, options);
2188
+ if (isRecord(designData.templates)) for (const template of Object.values(designData.templates)) {
2189
+ if (!isRecord(template)) continue;
2190
+ addSampleArray(sampleOptions, seen, (isRecord(template.data) ? template.data : template).samples, options);
2191
+ }
2192
+ if (Array.isArray(designData.plates)) for (const plate of designData.plates) {
2193
+ if (!isRecord(plate)) continue;
2194
+ addSampleArray(sampleOptions, seen, plate.samples, options);
2195
+ }
2196
+ return sampleOptions;
2197
+ }
2198
+ /** Extract SampleSelector-compatible string values from raw or wrapped experiment design_data. */
2199
+ function extractSampleNamesFromDesignData(rawData, options = {}) {
2200
+ return extractSampleOptionsFromDesignData(rawData, options).map((sample) => sample.value);
2201
+ }
2202
+ //#endregion
2041
2203
  //#region src/composables/useAutoGroup.ts
2042
2204
  var DEFAULT_COLORS = [
2043
2205
  "#3B82F6",
@@ -2213,8 +2375,9 @@ function computeGroups(allSamples, columns, enabledFields, outlierActions, delim
2213
2375
  const keyParts = [];
2214
2376
  for (const idx of enabledIndices) if (idx < row.length && idx < columns.length) keyParts.push(row[idx]);
2215
2377
  const groupKey = keyParts.join(" / ");
2216
- if (!groupMap.has(groupKey)) groupMap.set(groupKey, []);
2217
- groupMap.get(groupKey).push(sample);
2378
+ const group = groupMap.get(groupKey);
2379
+ if (group) group.push(sample);
2380
+ else groupMap.set(groupKey, [sample]);
2218
2381
  const fields = {};
2219
2382
  for (const col of columns) if (col.index < row.length) fields[col.name] = row[col.index];
2220
2383
  metadata.push({
@@ -2262,7 +2425,9 @@ function computeGroups(allSamples, columns, enabledFields, outlierActions, delim
2262
2425
  * Returns null if no samples with conditions are found.
2263
2426
  */
2264
2427
  function extractSamplesFromDesignData(rawData) {
2265
- const samples = rawData.samples;
2428
+ const designData = unwrapExperimentDesignData(rawData);
2429
+ if (!designData) return null;
2430
+ const samples = designData.samples;
2266
2431
  if (!Array.isArray(samples) || samples.length === 0) return null;
2267
2432
  const allConditionKeys = [];
2268
2433
  const keySet = /* @__PURE__ */ new Set();
@@ -2299,8 +2464,9 @@ function computeGroupsFromCsv(csvData, columns, enabledFields) {
2299
2464
  for (const row of csvData.rows) {
2300
2465
  const sampleName = row[csvData.sampleColumn];
2301
2466
  const groupKey = enabledCols.map((col) => row[col.originalName ?? col.name]).join(" / ");
2302
- if (!groupMap.has(groupKey)) groupMap.set(groupKey, []);
2303
- groupMap.get(groupKey).push(sampleName);
2467
+ const group = groupMap.get(groupKey);
2468
+ if (group) group.push(sampleName);
2469
+ else groupMap.set(groupKey, [sampleName]);
2304
2470
  const fields = {};
2305
2471
  for (const col of columns) fields[col.name] = row[col.originalName ?? col.name];
2306
2472
  metadata.push({
@@ -2339,7 +2505,8 @@ function useAutoGroup() {
2339
2505
  const enabledFields = ref(/* @__PURE__ */ new Set());
2340
2506
  const isTabularMode = computed(() => (inputMode.value === "csv" || inputMode.value === "experiment") && csvData.value !== null);
2341
2507
  const samples = computed(() => {
2342
- if (isTabularMode.value && csvData.value) return csvData.value.rows.map((r) => r[csvData.value.sampleColumn]);
2508
+ const data = csvData.value;
2509
+ if (isTabularMode.value && data) return data.rows.map((r) => r[data.sampleColumn]);
2343
2510
  return rawText.value.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
2344
2511
  });
2345
2512
  const hasOutliers = computed(() => outliers.value.length > 0);
@@ -2709,6 +2876,169 @@ function normalizeIds(ids) {
2709
2876
  return [...new Set(ids.filter(Boolean))];
2710
2877
  }
2711
2878
  //#endregion
2879
+ //#region src/composables/platformContextHelpers.ts
2880
+ function getInjectedPlatformContext() {
2881
+ if (typeof window === "undefined") return void 0;
2882
+ return window.__MINT_PLATFORM__;
2883
+ }
2884
+ function getInjectedExperimentContext() {
2885
+ return getInjectedPlatformContext();
2886
+ }
2887
+ function currentExperimentFromContext(context = getInjectedExperimentContext()) {
2888
+ const candidate = context?.currentExperiment ?? context?.experiment;
2889
+ return candidate && typeof candidate === "object" ? candidate : void 0;
2890
+ }
2891
+ function currentExperimentIdFromContext(context = getInjectedExperimentContext()) {
2892
+ return parseExperimentId(context?.currentExperimentId ?? context?.experimentId ?? context?.currentExperiment ?? context?.experiment);
2893
+ }
2894
+ function currentExperimentIdFromUrl() {
2895
+ if (typeof window === "undefined") return void 0;
2896
+ const params = new URLSearchParams(window.location.search);
2897
+ return parseExperimentId(params.get("experimentId") ?? params.get("experiment_id")) ?? experimentIdFromPath(window.location.pathname) ?? experimentIdFromPath(window.location.hash.replace(/^#\/?/, "/"));
2898
+ }
2899
+ function resolveCurrentExperimentId(context = getInjectedExperimentContext()) {
2900
+ return currentExperimentIdFromContext(context) ?? currentExperimentIdFromUrl();
2901
+ }
2902
+ function parseExperimentId(value) {
2903
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2904
+ if (typeof value === "string" && value.trim()) {
2905
+ const parsed = Number(value);
2906
+ return Number.isFinite(parsed) ? parsed : void 0;
2907
+ }
2908
+ if (value && typeof value === "object" && "id" in value) return parseExperimentId(value.id);
2909
+ }
2910
+ function experimentIdFromPath(path) {
2911
+ const segments = path.split("/").filter(Boolean);
2912
+ for (let index = 0; index < segments.length - 1; index += 1) {
2913
+ const segment = segments[index]?.toLowerCase();
2914
+ if (segment === "experiment" || segment === "experiments") return parseExperimentId(decodeURIComponent(segments[index + 1] ?? ""));
2915
+ }
2916
+ }
2917
+ //#endregion
2918
+ //#region src/composables/useExperimentData.ts
2919
+ /** Fetches and normalises experiment output data (tree, table, summary) from the platform API. */
2920
+ function useExperimentData(options = {}) {
2921
+ const data = shallowRef(null);
2922
+ const request = useRequestSyncState("Failed to fetch experiment data");
2923
+ const isLoading = request.loading;
2924
+ const error = request.error;
2925
+ const lastLoadedAt = request.lastLoadedAt;
2926
+ let lastExperimentId = null;
2927
+ let api = null;
2928
+ function getApi() {
2929
+ api ??= useApi({ baseUrl: options.apiBaseUrl });
2930
+ return api;
2931
+ }
2932
+ const designData = computed(() => unwrapExperimentDesignData(data.value));
2933
+ const sampleNames = computed(() => extractSampleNamesFromDesignData(designData.value));
2934
+ const sampleOptions = computed(() => extractSampleOptionsFromDesignData(designData.value));
2935
+ const treeData = computed(() => {
2936
+ if (!designData.value) return [];
2937
+ const tree = designData.value.tree_data ?? designData.value.treeData;
2938
+ return Array.isArray(tree) ? tree : [];
2939
+ });
2940
+ const tableData = computed(() => {
2941
+ if (!designData.value) return [];
2942
+ const table = designData.value.table_data ?? designData.value.tableData;
2943
+ return Array.isArray(table) ? table : [];
2944
+ });
2945
+ const summaryData = computed(() => {
2946
+ if (!designData.value) return null;
2947
+ const summary = designData.value.summary_data ?? designData.value.summaryData;
2948
+ if (summary && typeof summary === "object" && "metadata" in summary) return summary;
2949
+ return null;
2950
+ });
2951
+ async function fetchData(experimentId) {
2952
+ lastExperimentId = experimentId;
2953
+ try {
2954
+ data.value = await request.run(() => getApi().get(`/experiments/${experimentId}/data`), {
2955
+ success: "load",
2956
+ errorMessage: "Failed to fetch experiment data"
2957
+ });
2958
+ return data.value;
2959
+ } catch {
2960
+ data.value = null;
2961
+ return null;
2962
+ }
2963
+ }
2964
+ async function refresh() {
2965
+ if (lastExperimentId !== null) await fetchData(lastExperimentId);
2966
+ }
2967
+ function clear() {
2968
+ data.value = null;
2969
+ lastExperimentId = null;
2970
+ request.clearError();
2971
+ }
2972
+ return {
2973
+ data,
2974
+ designData,
2975
+ sampleNames,
2976
+ sampleOptions,
2977
+ treeData,
2978
+ tableData,
2979
+ summaryData,
2980
+ isLoading,
2981
+ error,
2982
+ lastLoadedAt,
2983
+ fetch: fetchData,
2984
+ refresh,
2985
+ clear
2986
+ };
2987
+ }
2988
+ //#endregion
2989
+ //#region src/composables/useExperimentSamples.ts
2990
+ /** Loads experiment design_data and derives samples for SampleSelector-style UIs. */
2991
+ function useExperimentSamples(options = {}) {
2992
+ const appExperiment = options.syncAppExperiment === false ? void 0 : inject(APP_EXPERIMENT_KEY, void 0);
2993
+ const experimentData = useExperimentData({ apiBaseUrl: options.apiBaseUrl });
2994
+ const experimentId = computed(() => {
2995
+ return parseExperimentId(toValue(options.experimentId)) ?? parseExperimentId(appExperiment?.experimentId.value);
2996
+ });
2997
+ const explicitDesignData = computed(() => unwrapExperimentDesignData(toValue(options.designData) ?? null));
2998
+ const designData = computed(() => explicitDesignData.value ?? experimentData.designData.value);
2999
+ const extractOptions = computed(() => ({ includeControls: options.includeControls }));
3000
+ const samples = computed(() => extractSampleNamesFromDesignData(designData.value, extractOptions.value));
3001
+ const sampleOptions = computed(() => extractSampleOptionsFromDesignData(designData.value, extractOptions.value));
3002
+ const enabled = computed(() => toValue(options.enabled) ?? true);
3003
+ async function fetchSamples(id = experimentId.value) {
3004
+ const parsedId = parseExperimentId(id);
3005
+ if (parsedId === void 0) {
3006
+ experimentData.clear();
3007
+ return null;
3008
+ }
3009
+ return experimentData.fetch(parsedId);
3010
+ }
3011
+ watch(() => ({
3012
+ designData: explicitDesignData.value,
3013
+ enabled: enabled.value,
3014
+ experimentId: experimentId.value
3015
+ }), ({ designData: currentDesignData, enabled: isEnabled, experimentId: currentExperimentId }) => {
3016
+ if (options.immediate === false || !isEnabled) return;
3017
+ if (currentDesignData) {
3018
+ experimentData.clear();
3019
+ return;
3020
+ }
3021
+ if (currentExperimentId === void 0) {
3022
+ experimentData.clear();
3023
+ return;
3024
+ }
3025
+ experimentData.fetch(currentExperimentId);
3026
+ }, { immediate: options.immediate !== false });
3027
+ return {
3028
+ experimentId,
3029
+ data: experimentData.data,
3030
+ designData,
3031
+ samples,
3032
+ sampleOptions,
3033
+ isLoading: experimentData.isLoading,
3034
+ error: experimentData.error,
3035
+ lastLoadedAt: experimentData.lastLoadedAt,
3036
+ fetch: fetchSamples,
3037
+ refresh: experimentData.refresh,
3038
+ clear: experimentData.clear
3039
+ };
3040
+ }
3041
+ //#endregion
2712
3042
  //#region src/composables/useScheduleDrag.ts
2713
3043
  /** Handles pointer-driven create, move, and resize drag interactions for the ScheduleCalendar grid. */
2714
3044
  function useScheduleDrag(options) {
@@ -2889,45 +3219,6 @@ function clampRange(start, end, min, max) {
2889
3219
  };
2890
3220
  }
2891
3221
  //#endregion
2892
- //#region src/composables/platformContextHelpers.ts
2893
- function getInjectedPlatformContext() {
2894
- if (typeof window === "undefined") return void 0;
2895
- return window.__MINT_PLATFORM__;
2896
- }
2897
- function getInjectedExperimentContext() {
2898
- return getInjectedPlatformContext();
2899
- }
2900
- function currentExperimentFromContext(context = getInjectedExperimentContext()) {
2901
- const candidate = context?.currentExperiment ?? context?.experiment;
2902
- return candidate && typeof candidate === "object" ? candidate : void 0;
2903
- }
2904
- function currentExperimentIdFromContext(context = getInjectedExperimentContext()) {
2905
- return parseExperimentId(context?.currentExperimentId ?? context?.experimentId ?? context?.currentExperiment ?? context?.experiment);
2906
- }
2907
- function currentExperimentIdFromUrl() {
2908
- if (typeof window === "undefined") return void 0;
2909
- const params = new URLSearchParams(window.location.search);
2910
- return parseExperimentId(params.get("experimentId") ?? params.get("experiment_id")) ?? experimentIdFromPath(window.location.pathname) ?? experimentIdFromPath(window.location.hash.replace(/^#\/?/, "/"));
2911
- }
2912
- function resolveCurrentExperimentId(context = getInjectedExperimentContext()) {
2913
- return currentExperimentIdFromContext(context) ?? currentExperimentIdFromUrl();
2914
- }
2915
- function parseExperimentId(value) {
2916
- if (typeof value === "number" && Number.isFinite(value)) return value;
2917
- if (typeof value === "string" && value.trim()) {
2918
- const parsed = Number(value);
2919
- return Number.isFinite(parsed) ? parsed : void 0;
2920
- }
2921
- if (value && typeof value === "object" && "id" in value) return parseExperimentId(value.id);
2922
- }
2923
- function experimentIdFromPath(path) {
2924
- const segments = path.split("/").filter(Boolean);
2925
- for (let index = 0; index < segments.length - 1; index += 1) {
2926
- const segment = segments[index]?.toLowerCase();
2927
- if (segment === "experiment" || segment === "experiments") return parseExperimentId(decodeURIComponent(segments[index + 1] ?? ""));
2928
- }
2929
- }
2930
- //#endregion
2931
3222
  //#region src/composables/useExperimentSave.ts
2932
3223
  /** Persists experiment design, analysis, and built-in template preset data through the platform API. */
2933
3224
  function useExperimentSave(options = {}) {
@@ -3737,8 +4028,9 @@ function useRackEditor(initialRacks, options) {
3737
4028
  return count;
3738
4029
  });
3739
4030
  function reset() {
3740
- racks.value = [createDefaultRack("Rack 1", 0)];
3741
- activeRackId.value = racks.value[0].id;
4031
+ const rack = createDefaultRack("Rack 1", 0);
4032
+ racks.value = [rack];
4033
+ activeRackId.value = rack.id;
3742
4034
  }
3743
4035
  return {
3744
4036
  racks,
@@ -4246,10 +4538,18 @@ var BUILT_IN_TEMPLATES = [
4246
4538
  }
4247
4539
  ];
4248
4540
  var STORAGE_KEY = "mint-custom-protocol-templates";
4541
+ function isStepTemplate(value) {
4542
+ if (!value || typeof value !== "object") return false;
4543
+ const candidate = value;
4544
+ return typeof candidate.id === "string" && typeof candidate.type === "string" && typeof candidate.name === "string" && Array.isArray(candidate.parameters);
4545
+ }
4249
4546
  function loadCustomTemplates() {
4250
4547
  try {
4251
4548
  const stored = localStorage.getItem(STORAGE_KEY);
4252
- if (stored) return JSON.parse(stored);
4549
+ if (stored) {
4550
+ const parsed = JSON.parse(stored);
4551
+ return Array.isArray(parsed) ? parsed.filter(isStepTemplate) : [];
4552
+ }
4253
4553
  } catch {}
4254
4554
  return [];
4255
4555
  }
@@ -4356,59 +4656,6 @@ function useProtocolTemplates() {
4356
4656
  };
4357
4657
  }
4358
4658
  //#endregion
4359
- //#region src/composables/useExperimentData.ts
4360
- /** Fetches and normalises experiment output data (tree, table, summary) from the platform API. */
4361
- function useExperimentData(options = {}) {
4362
- const api = useApi({ baseUrl: options.apiBaseUrl });
4363
- const data = ref(null);
4364
- const request = useRequestSyncState("Failed to fetch experiment data");
4365
- const isLoading = request.loading;
4366
- const error = request.error;
4367
- const lastLoadedAt = request.lastLoadedAt;
4368
- let lastExperimentId = null;
4369
- const treeData = computed(() => {
4370
- if (!data.value) return [];
4371
- const tree = data.value.tree_data ?? data.value.treeData;
4372
- return Array.isArray(tree) ? tree : [];
4373
- });
4374
- const tableData = computed(() => {
4375
- if (!data.value) return [];
4376
- const table = data.value.table_data ?? data.value.tableData;
4377
- return Array.isArray(table) ? table : [];
4378
- });
4379
- const summaryData = computed(() => {
4380
- if (!data.value) return null;
4381
- const summary = data.value.summary_data ?? data.value.summaryData;
4382
- if (summary && typeof summary === "object" && "metadata" in summary) return summary;
4383
- return null;
4384
- });
4385
- async function fetchData(experimentId) {
4386
- lastExperimentId = experimentId;
4387
- try {
4388
- data.value = await request.run(() => api.get(`/experiments/${experimentId}/data`), {
4389
- success: "load",
4390
- errorMessage: "Failed to fetch experiment data"
4391
- });
4392
- } catch {
4393
- data.value = null;
4394
- }
4395
- }
4396
- async function refresh() {
4397
- if (lastExperimentId !== null) await fetchData(lastExperimentId);
4398
- }
4399
- return {
4400
- data,
4401
- treeData,
4402
- tableData,
4403
- summaryData,
4404
- isLoading,
4405
- error,
4406
- lastLoadedAt,
4407
- fetch: fetchData,
4408
- refresh
4409
- };
4410
- }
4411
- //#endregion
4412
- export { useTheme as $, useAutoGroup as A, DATE_PRESET_OPTIONS as B, useSampleGroups as C, DEFAULT_COLORS as D, hslToHex as E, usePlatformContext as F, datePresetToISO as G, EXPERIMENT_STATUS_OPTIONS as H, useExperimentSelector as I, getExperimentStatusVariant as J, formatExperimentDate as K, useRequestSyncState as L, useDoseCalculator as M, APP_EXPERIMENT_KEY as N, extractSamplesFromDesignData as O, useAppExperiment as P, useForm as Q, useDebouncedWatch as R, useExpansionSet as S, hexToHsl as T, EXPERIMENT_STATUS_VARIANT_MAP as U, EXPERIMENT_STATUS_LABELS as V, SORT_OPTIONS as W, evaluateCondition as X, resolveExperimentCode as Y, useFormBuilder as Z, useExperimentSave as _, generateDilutionSeries as a, useSortedItems as at, resolveCurrentExperimentId as b, useRackEditor as c, useBioTemplateWorkspace as d, useToast as et, getBioTemplateComponentProps as f, useTemplateCollection as g, useBioTemplateControls as h, DEFAULT_UNITS as i, compareSortValues as it, useWellPlateEditor as j, parseCSV as k, useBioTemplatePresetWorkspace as l, useBioTemplateComponents as m, useProtocolTemplates as n, normalizeSearchQuery as nt, useReagentSeries as o, toBioTemplateComponentPropsByComponent as p, formatExperimentStatus as q, DEFAULT_PRESETS as r, useTextSearch as rt, useGroupAssignment as s, useExperimentData as t, candidateMatchesSearch as tt, useBioTemplatePackWorkspace as u, currentExperimentFromContext as v, deriveShade as w, useScheduleDrag as x, getInjectedPlatformContext as y, useApi as z };
4659
+ export { usePlatformContext as $, parseCSV as A, useRequestSyncState as B, useExpansionSet as C, hslToHex as D, hexToHsl as E, useWellPlateEditor as F, EXPERIMENT_STATUS_OPTIONS as G, useApi as H, useDoseCalculator as I, datePresetToISO as J, EXPERIMENT_STATUS_VARIANT_MAP as K, APP_EXPERIMENT_KEY as L, extractSampleNamesFromDesignData as M, extractSampleOptionsFromDesignData as N, DEFAULT_COLORS as O, unwrapExperimentDesignData as P, resolveExperimentCode as Q, useAppExperiment as R, resolveCurrentExperimentId as S, deriveShade as T, DATE_PRESET_OPTIONS as U, useDebouncedWatch as V, EXPERIMENT_STATUS_LABELS as W, formatExperimentStatus as X, formatExperimentDate as Y, getExperimentStatusVariant as Z, useScheduleDrag as _, useReagentSeries as a, candidateMatchesSearch as at, currentExperimentFromContext as b, useBioTemplatePresetWorkspace as c, compareSortValues as ct, getBioTemplateComponentProps as d, evaluateCondition as et, toBioTemplateComponentPropsByComponent as f, useExperimentSave as g, useTemplateCollection as h, generateDilutionSeries as i, useToast as it, useAutoGroup as j, extractSamplesFromDesignData as k, useBioTemplatePackWorkspace as l, useSortedItems as lt, useBioTemplateControls as m, DEFAULT_PRESETS as n, useForm as nt, useGroupAssignment as o, normalizeSearchQuery as ot, useBioTemplateComponents as p, SORT_OPTIONS as q, DEFAULT_UNITS as r, useTheme as rt, useRackEditor as s, useTextSearch as st, useProtocolTemplates as t, useFormBuilder as tt, useBioTemplateWorkspace as u, useExperimentSamples as v, useSampleGroups as w, getInjectedPlatformContext as x, useExperimentData as y, useExperimentSelector as z };
4413
4660
 
4414
- //# sourceMappingURL=useExperimentData-CM6Y0u5L.js.map
4661
+ //# sourceMappingURL=useProtocolTemplates-n6AJqSqv.js.map