@morscherlab/mint-sdk 1.3.0 → 1.3.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 (70) hide show
  1. package/dist/__tests__/components/AutoGroupModal.anatomy.test.d.ts +1 -0
  2. package/dist/__tests__/components/AutoGroupModal.wiring.test.d.ts +1 -0
  3. package/dist/__tests__/composables/autoGroup/fieldEdits.test.d.ts +1 -0
  4. package/dist/__tests__/composables/autoGroup/golden.test.d.ts +1 -0
  5. package/dist/components/AutoGroupModal.adapter.d.ts +27 -4
  6. package/dist/components/PlateEditor.vue.d.ts +2 -0
  7. package/dist/components/SmartGroup.types.d.ts +22 -0
  8. package/dist/components/SmartGroupFieldRecipe.groups.d.ts +10 -2
  9. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -0
  10. package/dist/components/index.js +1 -1
  11. package/dist/{components-vHfxV2PB.js → components-CEePoD3W.js} +502 -264
  12. package/dist/components-CEePoD3W.js.map +1 -0
  13. package/dist/composables/autoGroup/classify.d.ts +7 -1
  14. package/dist/composables/autoGroup/columns.d.ts +16 -1
  15. package/dist/composables/autoGroup/index.d.ts +5 -7
  16. package/dist/composables/autoGroup/tokenize.d.ts +36 -1
  17. package/dist/composables/index.js +2 -2
  18. package/dist/composables/useAutoGroup.d.ts +13 -2
  19. package/dist/{composables-D6uqp0UF.js → composables-BbHozlA7.js} +2 -2
  20. package/dist/{composables-D6uqp0UF.js.map → composables-BbHozlA7.js.map} +1 -1
  21. package/dist/index.js +3 -3
  22. package/dist/install.js +1 -1
  23. package/dist/permissions.js +2 -2
  24. package/dist/permissions.js.map +1 -1
  25. package/dist/styles.css +71 -82
  26. package/dist/types/auto-group.d.ts +1 -1
  27. package/dist/{validation-Dmp9Q5W0.js → validation-CukfLjy-.js} +323 -187
  28. package/dist/validation-CukfLjy-.js.map +1 -0
  29. package/package.json +2 -2
  30. package/src/__tests__/components/AutoGroupModal.anatomy.test.ts +86 -0
  31. package/src/__tests__/components/AutoGroupModal.wiring.test.ts +53 -0
  32. package/src/__tests__/components/PlateEditor.test.ts +10 -0
  33. package/src/__tests__/components/SampleSelector.test.ts +14 -16
  34. package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +23 -0
  35. package/src/__tests__/components/SmartGroupFieldRecipe.test.ts +176 -0
  36. package/src/__tests__/composables/autoGroup/columns.test.ts +100 -11
  37. package/src/__tests__/composables/autoGroup/fieldEdits.test.ts +74 -0
  38. package/src/__tests__/composables/autoGroup/fingerprint.test.ts +9 -0
  39. package/src/__tests__/composables/autoGroup/golden.test.ts +187 -0
  40. package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +7 -7
  41. package/src/__tests__/composables/autoGroup/tokenLength.test.ts +7 -0
  42. package/src/__tests__/composables/autoGroup/tokenize.test.ts +78 -1
  43. package/src/__tests__/composables/useAutoGroup.test.ts +2 -1
  44. package/src/__tests__/fixtures/auto-group/share-exp021.txt +103 -0
  45. package/src/__tests__/fixtures/auto-group/share-exp031.txt +39 -0
  46. package/src/__tests__/fixtures/auto-group/share-exp114.txt +123 -0
  47. package/src/__tests__/fixtures/auto-group/share-exp214.txt +63 -0
  48. package/src/__tests__/utils/permissions.test.ts +15 -0
  49. package/src/components/AutoGroupModal.adapter.ts +80 -9
  50. package/src/components/AutoGroupModal.vue +41 -8
  51. package/src/components/PlateEditor.vue +3 -0
  52. package/src/components/SmartGroup.types.ts +23 -0
  53. package/src/components/SmartGroupFieldRecipe.groups.ts +14 -2
  54. package/src/components/SmartGroupFieldRecipe.vue +289 -169
  55. package/src/composables/autoGroup/classify.ts +26 -7
  56. package/src/composables/autoGroup/columns.ts +144 -36
  57. package/src/composables/autoGroup/compose.ts +8 -10
  58. package/src/composables/autoGroup/fingerprint.ts +1 -1
  59. package/src/composables/autoGroup/index.ts +5 -12
  60. package/src/composables/autoGroup/tokenLength.ts +8 -0
  61. package/src/composables/autoGroup/tokenize.ts +148 -1
  62. package/src/composables/autoGroup/vocab.json +5 -11
  63. package/src/composables/useAutoGroup.ts +92 -79
  64. package/src/permissions.ts +3 -2
  65. package/src/styles/components/smart-group.css +71 -82
  66. package/src/types/auto-group.ts +1 -0
  67. package/dist/components-vHfxV2PB.js.map +0 -1
  68. package/dist/composables/autoGroup/replicatePreGroup.d.ts +0 -36
  69. package/dist/validation-Dmp9Q5W0.js.map +0 -1
  70. package/src/composables/autoGroup/replicatePreGroup.ts +0 -124
@@ -2,7 +2,7 @@ import { a as tryUseSettingsStore, c as mintApiErrorFromResponse, i as tableDens
2
2
  import { a as SORT_OPTIONS, c as formatExperimentDate, f as BaseModal_default, h as BaseButton_default, l as formatExperimentStatus, m as useEventListener, p as useFocusTrap, r as EXPERIMENT_STATUS_OPTIONS, t as DATE_PRESET_OPTIONS, u as getExperimentStatusVariant } from "./experiment-utils-DTANyVKV.js";
3
3
  import { a as normalizeOptionInput, i as BaseSelect_default, n as Skeleton_default, o as BaseInput_default, r as EmptyState_default, t as ResourcePickerListInternal_default } from "./ResourcePickerListInternal-CFAw4vTZ.js";
4
4
  import { $n as FormulaInput_default, An as getControlDefaults, B as toBioTemplateComponentProps, Cn as SAMPLE_GROUP_TOKENS, Cr as FileUploader_default, F as getBioTemplateComponentProps, Fn as controlsToSettingsSchema, Gn as getFieldRegistryEntry, H as toBioTemplateComponentPropsById, Hr as durationMinutes, In as controlsToSidebarPanels, Jn as UnitInput_default, Kr as formatTime, L as toBioTemplateComponentBindings, Mn as controlsToFormSchema, Pn as controlsToSectionFormSchemas, R as toBioTemplateComponentBindingsById, Rr as TagsInput_default, Sn as SAMPLE_COLOR_SCALE, Sr as _plugin_vue_export_helper_default, Tr as SecretInput_default, V as toBioTemplateComponentPropsByComponent, Vr as compareTime, Xn as MoleculeInput_default, Yn as useConcentrationUnits, Zn as SequenceInput_default, _n as createPlateRowLabels, ai as useListSelection, ar as getInjectedPlatformContext, br as isRecord, ci as BaseRadioGroup_default, di as Tooltip_default, fi as BaseTextarea_default, fn as extractTemplateCollection, gn as createPlateColumnLabels, hn as WELL_PLATE_SIZE_CONFIGS, hr as normalizeJobPercent, ii as MultiSelect_default, li as BaseToggle_default, lr as createGeneratedAnalysisTransport, mn as PLATE_CONFIGS, mr as jobStatusLabel, ni as DatePicker_default, nr as GeneratedPathInput_default, qn as ConcentrationInput_default, ri as useDropdownState, rr as GENERATED_JOBS_API_KEY, si as BaseSlider_default, sr as resolveCurrentExperimentId, tr as useChemicalFormula, ui as BaseCheckbox_default, ur as useGeneratedAnalysis, vn as createWellGrid, wn as SLOT_ORDER, wr as NumberInput_default, xr as useMenuKeyboard, yn as getRectangleWellIds, yr as DateTimePicker_default, zr as TimePicker_default } from "./templates-BU1pet28.js";
5
- import { B as useRackEditor, Bt as useRovingFocus, C as useExperimentSamples, D as getSampleGroupMajorPrefix, Dt as useAppExperiment, E as useSampleGroups, Et as APP_EXPERIMENT_KEY, F as useAutoGroup, G as useDoseCalculator, H as reorderList, J as usePluginJobCenter, Lt as useTextSearch, Mt as usePlatformContext, Nt as useTheme, O as getSampleGroupParentPath, Ot as getInjectedFrontendRevision, Pt as useToast, S as useScheduleDrag, T as useExpansionSet, Tt as mergeControlWorkspaceOptions, U as useListReorder, W as useReorderAnnouncer, X as downloadBlob, a as useExperimentSelector, c as DEFAULT_PRESETS, ct as usePluginWorkspace, d as useReagentSeries, dt as useFormBuilder, f as useGroupAssignment, ht as defineDoseDesignControlModel, i as useAnalysisArtifacts, j as readFileAsText, jt as usePresenceHeartbeat, kt as useRuntimeAlignment, l as DEFAULT_UNITS, lt as DEFAULT_MOBILE_VIEWPORT_QUERY, m as useBioTemplatePackWorkspace, n as pickerFormat, p as useBioTemplatePresetWorkspace, r as pickerReason, s as useProtocolTemplates, st as useRequestSyncState, t as pickerExclusion, ut as useMobileSupportGate, w as useExperimentData, xt as resolveControlModel, yt as useControlWorkspace, z as DEFAULT_COLORS, zt as useSortedItems } from "./validation-Dmp9Q5W0.js";
5
+ import { $ as downloadBlob, At as useAppExperiment, B as GROUPING_FIELD_ROLES, Bt as useTextSearch, C as useExperimentSamples, D as getSampleGroupMajorPrefix, E as useSampleGroups, F as useAutoGroup, Ft as usePlatformContext, G as reorderList, H as classKey, Ht as useSortedItems, It as useTheme, J as useDoseCalculator, K as useListReorder, Lt as useToast, Mt as useRuntimeAlignment, O as getSampleGroupParentPath, Ot as mergeControlWorkspaceOptions, Pt as usePresenceHeartbeat, S as useScheduleDrag, St as useControlWorkspace, T as useExpansionSet, U as useRackEditor, Ut as useRovingFocus, V as isExperimentalKind, Z as usePluginJobCenter, a as useExperimentSelector, c as DEFAULT_PRESETS, d as useReagentSeries, dt as usePluginWorkspace, f as useGroupAssignment, ft as DEFAULT_MOBILE_VIEWPORT_QUERY, i as useAnalysisArtifacts, j as readFileAsText, jt as getInjectedFrontendRevision, kt as APP_EXPERIMENT_KEY, l as DEFAULT_UNITS, m as useBioTemplatePackWorkspace, mt as useFormBuilder, n as pickerFormat, p as useBioTemplatePresetWorkspace, pt as useMobileSupportGate, q as useReorderAnnouncer, r as pickerReason, s as useProtocolTemplates, t as pickerExclusion, ut as useRequestSyncState, vt as defineDoseDesignControlModel, w as useExperimentData, wt as resolveControlModel, z as DEFAULT_COLORS } from "./validation-CukfLjy-.js";
6
6
  import { canAccessByPolicy, hasAllPermissions } from "./permissions.js";
7
7
  import { a as useApi, o as useAuthStore, t as useExperimentStore } from "./experiment-DgllIc3I.js";
8
8
  import { t as ANALYSIS_FILE_ARTIFACT_SCHEMA } from "./analysisArtifactTypes-Cy3D0Ipg.js";
@@ -9445,7 +9445,8 @@ var PlateEditor_default = /* @__PURE__ */ defineComponent({
9445
9445
  type: Boolean,
9446
9446
  default: false
9447
9447
  },
9448
- size: { default: "xs" }
9448
+ size: { default: "xs" },
9449
+ hint: {}
9449
9450
  },
9450
9451
  emits: [
9451
9452
  "update:activeRackId",
@@ -9718,6 +9719,7 @@ var PlateEditor_default = /* @__PURE__ */ defineComponent({
9718
9719
  }
9719
9720
  const hint = computed(() => {
9720
9721
  if (dragMode.value) return "Drag a filled well onto another to move or swap it.";
9722
+ if (props.hint) return props.hint;
9721
9723
  if (!props.groups) return "";
9722
9724
  if (railOn.value) return inspectorOn.value ? "Click a well to edit it on the right. Drag across wells, Ctrl-click to add one or Shift-click for a range." : "Click or drag across wells to select them.";
9723
9725
  return inspectorOn.value ? "Click a well to edit it. Ctrl- or Cmd-click to select several, Shift-click for a range." : "Click or drag across wells to select them.";
@@ -10714,6 +10716,13 @@ function appendQcGroup(groups, route, qcColor, qcCount = 3) {
10714
10716
  function sampleTotalFor(route, realCount = 9, qcCount = 3) {
10715
10717
  return route === "Exclude" ? realCount : realCount + qcCount;
10716
10718
  }
10719
+ /**
10720
+ * True when half or more of the groups have one sample: the design has no
10721
+ * replicate in the names, or a field that is on is not part of the condition.
10722
+ */
10723
+ function mostlySingletons(groups) {
10724
+ return groups.length > 1 && groups.filter((group) => group.count === 1).length * 2 >= groups.length;
10725
+ }
10717
10726
  /** Decide which inline summary callout the Field Recipe should show. */
10718
10727
  function fieldRecipeSummary(input) {
10719
10728
  const groupCount = input.baseGroups.length;
@@ -10725,6 +10734,10 @@ function fieldRecipeSummary(input) {
10725
10734
  kind: "warn",
10726
10735
  groupCount
10727
10736
  };
10737
+ if (mostlySingletons(input.baseGroups)) return {
10738
+ kind: "singletons",
10739
+ groupCount
10740
+ };
10728
10741
  return {
10729
10742
  kind: "info",
10730
10743
  groupCount
@@ -10732,97 +10745,110 @@ function fieldRecipeSummary(input) {
10732
10745
  }
10733
10746
  //#endregion
10734
10747
  //#region src/components/SmartGroupFieldRecipe.vue?vue&type=script&setup=true&lang.ts
10735
- var _hoisted_1$42 = { class: "sg-modal fr" };
10736
- var _hoisted_2$38 = { class: "sg-head" };
10737
- var _hoisted_3$31 = { class: "sg-head__sub" };
10738
- var _hoisted_4$30 = {
10739
- ref: "csvMenuRoot",
10740
- class: "fr-csv"
10741
- };
10742
- var _hoisted_5$28 = ["aria-expanded"];
10743
- var _hoisted_6$26 = {
10744
- key: 0,
10745
- class: "fr-csv-menu",
10746
- role: "menu"
10747
- };
10748
- var _hoisted_7$24 = [
10749
- "disabled",
10750
- "aria-disabled",
10751
- "title"
10752
- ];
10753
- var _hoisted_8$22 = [
10754
- "disabled",
10755
- "aria-disabled",
10756
- "title"
10748
+ var _hoisted_1$42 = { class: "sg-head" };
10749
+ var _hoisted_2$38 = { class: "sg-head__sub" };
10750
+ var _hoisted_3$31 = [
10751
+ "aria-expanded",
10752
+ "onClick",
10753
+ "onKeydown"
10757
10754
  ];
10758
- var _hoisted_9$20 = {
10755
+ var _hoisted_4$30 = {
10759
10756
  class: "sg-mode",
10760
10757
  role: "tablist",
10761
10758
  "aria-label": "Grouping mode"
10762
10759
  };
10763
- var _hoisted_10$19 = { class: "sg-body" };
10764
- var _hoisted_11$17 = {
10760
+ var _hoisted_5$28 = { class: "sg-body" };
10761
+ var _hoisted_6$26 = {
10765
10762
  key: 0,
10766
10763
  class: "fr-dropzone"
10767
10764
  };
10768
- var _hoisted_12$16 = {
10765
+ var _hoisted_7$24 = {
10769
10766
  key: 1,
10770
10767
  class: "fr-loading"
10771
10768
  };
10769
+ var _hoisted_8$22 = {
10770
+ key: 2,
10771
+ class: "fr-blank",
10772
+ "data-test": "recipe-empty"
10773
+ };
10774
+ var _hoisted_9$20 = {
10775
+ key: 0,
10776
+ class: "fr-anatomy"
10777
+ };
10778
+ var _hoisted_10$19 = { class: "fr-anatomy__tokens" };
10779
+ var _hoisted_11$17 = { class: "fr-token__text" };
10780
+ var _hoisted_12$16 = { class: "fr-token__caption" };
10772
10781
  var _hoisted_13$16 = { class: "sg-recipe" };
10773
- var _hoisted_14$16 = { class: "fr-field__head" };
10774
- var _hoisted_15$16 = { class: "sg-field__name" };
10775
- var _hoisted_16$13 = [
10776
- "aria-checked",
10777
- "aria-label",
10778
- "onClick"
10779
- ];
10780
- var _hoisted_17$13 = {
10782
+ var _hoisted_14$16 = {
10781
10783
  key: 0,
10782
- class: "sg-field__order"
10784
+ class: "fr-empty"
10783
10785
  };
10784
- var _hoisted_18$13 = { class: "sg-field__layer" };
10785
- var _hoisted_19$13 = [
10786
- "disabled",
10787
- "aria-label",
10788
- "onClick"
10786
+ var _hoisted_15$16 = [
10787
+ "data-field",
10788
+ "draggable",
10789
+ "onDragstart",
10790
+ "onDragover",
10791
+ "onDrop"
10789
10792
  ];
10790
- var _hoisted_20$12 = [
10791
- "disabled",
10793
+ var _hoisted_16$13 = ["aria-label", "onKeydown"];
10794
+ var _hoisted_17$13 = { class: "fr-row__layer" };
10795
+ var _hoisted_18$13 = [
10792
10796
  "aria-label",
10793
- "onClick"
10797
+ "aria-invalid",
10798
+ "onKeydown",
10799
+ "onBlur"
10794
10800
  ];
10795
- var _hoisted_21$9 = { class: "sg-field__vals" };
10796
- var _hoisted_22$9 = {
10801
+ var _hoisted_19$13 = {
10802
+ key: 1,
10803
+ class: "fr-row__error",
10804
+ role: "alert"
10805
+ };
10806
+ var _hoisted_20$12 = { class: "sg-field__name" };
10807
+ var _hoisted_21$9 = ["aria-label", "onClick"];
10808
+ var _hoisted_22$9 = { class: "sg-field__vals" };
10809
+ var _hoisted_23$7 = {
10810
+ key: 3,
10811
+ class: "fr-row__hint"
10812
+ };
10813
+ var _hoisted_24$7 = { class: "sg-recipe" };
10814
+ var _hoisted_25$7 = ["data-field"];
10815
+ var _hoisted_26$5 = { class: "sg-field__name" };
10816
+ var _hoisted_27$5 = { class: "sg-field__vals" };
10817
+ var _hoisted_28$4 = {
10797
10818
  key: 2,
10798
10819
  class: "sg-summary fr-summary--mute"
10799
10820
  };
10800
- var _hoisted_23$7 = { class: "sg-summary__icon" };
10801
- var _hoisted_24$7 = {
10821
+ var _hoisted_29$4 = { class: "sg-summary__icon" };
10822
+ var _hoisted_30$3 = {
10802
10823
  key: 3,
10803
10824
  class: "sg-summary sg-summary--warn"
10804
10825
  };
10805
- var _hoisted_25$7 = { class: "sg-summary__icon" };
10806
- var _hoisted_26$5 = {
10826
+ var _hoisted_31$3 = { class: "sg-summary__icon" };
10827
+ var _hoisted_32$3 = {
10807
10828
  key: 4,
10829
+ class: "sg-summary sg-summary--warn"
10830
+ };
10831
+ var _hoisted_33$3 = { class: "sg-summary__icon" };
10832
+ var _hoisted_34$2 = {
10833
+ key: 5,
10808
10834
  class: "sg-summary"
10809
10835
  };
10810
- var _hoisted_27$5 = { class: "sg-summary__icon" };
10811
- var _hoisted_28$4 = { class: "sg-body__side" };
10812
- var _hoisted_29$4 = { class: "sg-preview__head" };
10813
- var _hoisted_30$3 = { class: "sg-preview__count" };
10814
- var _hoisted_31$3 = { class: "sg-grouplist" };
10815
- var _hoisted_32$3 = {
10836
+ var _hoisted_35$2 = { class: "sg-summary__icon" };
10837
+ var _hoisted_36$2 = { class: "sg-body__side" };
10838
+ var _hoisted_37$2 = { class: "sg-preview__head" };
10839
+ var _hoisted_38$2 = { class: "sg-preview__count" };
10840
+ var _hoisted_39$2 = { class: "sg-grouplist" };
10841
+ var _hoisted_40$2 = {
10816
10842
  key: 0,
10817
10843
  class: "fr-empty"
10818
10844
  };
10819
- var _hoisted_33$3 = { class: "sg-group__name" };
10820
- var _hoisted_34$2 = { class: "sg-group__count" };
10821
- var _hoisted_35$2 = { class: "sg-qc" };
10822
- var _hoisted_36$2 = { class: "sg-qc__head" };
10823
- var _hoisted_37$2 = ["value"];
10824
- var _hoisted_38$2 = { class: "sg-foot" };
10825
- var _hoisted_39$2 = { class: "sg-foot__hint" };
10845
+ var _hoisted_41$2 = { class: "sg-group__name" };
10846
+ var _hoisted_42$1 = { class: "sg-group__count" };
10847
+ var _hoisted_43$1 = { class: "sg-qc" };
10848
+ var _hoisted_44$1 = { class: "sg-qc__head" };
10849
+ var _hoisted_45$1 = { class: "fr-qc__chips" };
10850
+ var _hoisted_46$1 = { class: "sg-foot" };
10851
+ var _hoisted_47$1 = { class: "sg-foot__hint" };
10826
10852
  var DEFAULT_SAMPLES = [
10827
10853
  {
10828
10854
  Condition: "Control",
@@ -10891,7 +10917,15 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10891
10917
  loading: {
10892
10918
  type: Boolean,
10893
10919
  default: false
10894
- }
10920
+ },
10921
+ anatomy: {},
10922
+ hints: {},
10923
+ qcChips: {},
10924
+ renamable: {
10925
+ type: Boolean,
10926
+ default: false
10927
+ },
10928
+ reservedNames: {}
10895
10929
  },
10896
10930
  emits: [
10897
10931
  "apply",
@@ -10900,6 +10934,7 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10900
10934
  "close",
10901
10935
  "toggle",
10902
10936
  "move",
10937
+ "rename",
10903
10938
  "update:route",
10904
10939
  "csv-file",
10905
10940
  "download-template",
@@ -10926,9 +10961,9 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10926
10961
  check: "M20 6L9 17l-5-5",
10927
10962
  close: "M6 18L18 6M6 6l12 12",
10928
10963
  upload: "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12",
10929
- download: "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3",
10930
10964
  chev: "M6 9l6 6 6-6",
10931
- chevUp: "M18 15l-6-6-6 6"
10965
+ grip: "M9 6h.01M9 12h.01M9 18h.01M15 6h.01M15 12h.01M15 18h.01",
10966
+ pencil: "M12 20h9M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"
10932
10967
  };
10933
10968
  const MAX_BAR = 44;
10934
10969
  /** QC reads the same everywhere: neutral solid ring, never a group color (3a). */
@@ -10948,7 +10983,8 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10948
10983
  if (file) emit("csv-file", file);
10949
10984
  input.value = "";
10950
10985
  }
10951
- function onDragEnter() {
10986
+ function onDragEnter(event) {
10987
+ if (!event.dataTransfer?.types.includes("Files")) return;
10952
10988
  dragDepth += 1;
10953
10989
  dragOver.value = true;
10954
10990
  }
@@ -10962,25 +10998,43 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10962
10998
  const file = event.dataTransfer?.files?.[0];
10963
10999
  if (file && isCsvLike(file)) emit("csv-file", file);
10964
11000
  }
10965
- const { isOpen: csvMenuOpen, close: closeCsvMenu, toggle: toggleCsvMenu } = useDropdownState({
10966
- rootRef: useTemplateRef("csvMenuRoot"),
10967
- triggerRef: useTemplateRef("csvMenuTrigger")
10968
- });
10969
- function chooseUpload() {
10970
- closeCsvMenu();
10971
- pickCsv();
10972
- }
10973
- function chooseDownloadTemplate() {
10974
- if (!props.canDownloadTemplate) return;
10975
- closeCsvMenu();
10976
- emit("download-template");
10977
- }
10978
- function chooseDownloadPrefilled() {
10979
- if (!props.canDownloadTemplate) return;
10980
- closeCsvMenu();
10981
- emit("download-prefilled");
11001
+ const csvMenuItems = computed(() => [
11002
+ {
11003
+ id: "upload",
11004
+ label: "Upload CSV"
11005
+ },
11006
+ {
11007
+ id: "template",
11008
+ label: "Download template",
11009
+ disabled: !props.canDownloadTemplate
11010
+ },
11011
+ {
11012
+ id: "prefilled",
11013
+ label: "Download pre-filled",
11014
+ disabled: !props.canDownloadTemplate
11015
+ }
11016
+ ]);
11017
+ function onCsvMenuSelect(item) {
11018
+ if (item.id === "upload") pickCsv();
11019
+ else if (item.id === "template") emit("download-template");
11020
+ else emit("download-prefilled");
10982
11021
  }
10983
- const onInner = ref(props.initialOn ? [...props.initialOn] : props.fields.map((field) => !field.rep));
11022
+ const QC_ROUTE_OPTIONS = [
11023
+ {
11024
+ value: "Overlay only",
11025
+ label: "Overlay"
11026
+ },
11027
+ {
11028
+ value: "Exclude",
11029
+ label: "Exclude"
11030
+ },
11031
+ {
11032
+ value: "Mix into groups",
11033
+ label: "Mix in"
11034
+ }
11035
+ ];
11036
+ const startLayers = props.enabled === void 0 ? props.layers : void 0;
11037
+ const onInner = ref(props.initialOn ? [...props.initialOn] : startLayers?.map((layer) => layer !== null) ?? props.fields.map((field) => !field.rep));
10984
11038
  const orderInner = ref(onInner.value.flatMap((isOn, i) => isOn ? [i] : []));
10985
11039
  const routeInner = ref(props.initialRoute);
10986
11040
  const controlledEnabled = computed(() => props.enabled !== void 0);
@@ -10988,7 +11042,11 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10988
11042
  const on = computed(() => props.enabled ?? onInner.value);
10989
11043
  const currentRoute = computed(() => props.route ?? routeInner.value);
10990
11044
  const layers = computed(() => {
10991
- if (props.layers !== void 0) return props.layers;
11045
+ if (props.layers !== void 0 && controlledEnabled.value) return props.layers;
11046
+ if (props.layers !== void 0) {
11047
+ let last = Math.max(0, ...props.layers.map((layer) => layer ?? 0));
11048
+ return props.layers.map((layer, i) => on.value[i] ? layer ?? ++last : null);
11049
+ }
10992
11050
  if (controlledEnabled.value) {
10993
11051
  let layer = 0;
10994
11052
  return props.fields.map((_, i) => on.value[i] ? ++layer : null);
@@ -11002,19 +11060,22 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11002
11060
  layer,
11003
11061
  i
11004
11062
  }]).sort((a, b) => a.layer - b.layer).map((entry) => entry.i));
11063
+ const onCards = computed(() => layerOrder.value.map((index) => ({
11064
+ index,
11065
+ field: props.fields[index]
11066
+ })));
11067
+ const offCards = computed(() => props.fields.flatMap((field, index) => layers.value[index] === null ? [{
11068
+ index,
11069
+ field
11070
+ }] : []));
11005
11071
  const computedBase = computed(() => computeFieldGroups(props.samples, props.fields, on.value, props.palette, layerOrder.value));
11006
11072
  const baseGroups = computed(() => props.groups ?? computedBase.value);
11007
11073
  const groups = computed(() => props.groups ?? appendQcGroup(computedBase.value, currentRoute.value, props.qcColor));
11008
11074
  const activeFactors = computed(() => layerOrder.value.map((i) => props.fields[i].key));
11009
11075
  const counts = computed(() => baseGroups.value.map((group) => group.count));
11010
11076
  const uniform = computed(() => counts.value.length > 0 && counts.value.every((count) => count === counts.value[0]));
11011
- const repField = computed(() => props.fields.find((field) => field.rep));
11012
- const repOn = computed(() => props.fields.some((field, i) => field.rep && on.value[i]));
11013
- const repRange = computed(() => {
11014
- const parts = (repField.value?.vals ?? "").split(",").map((part) => part.trim()).filter(Boolean);
11015
- if (parts.length === 0) return "";
11016
- return parts.length > 1 ? `${parts[0]}–${parts[parts.length - 1]}` : parts[0];
11017
- });
11077
+ const repOnField = computed(() => props.fields.find((field, i) => field.rep && on.value[i]));
11078
+ const repOn = computed(() => repOnField.value !== void 0);
11018
11079
  const summary = computed(() => fieldRecipeSummary({
11019
11080
  activeFactors: activeFactors.value,
11020
11081
  repOn: repOn.value,
@@ -11025,12 +11086,23 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11025
11086
  const qcCount = computed(() => props.qcCount ?? Math.max(0, props.headerCount - props.samples.length));
11026
11087
  const realCount = computed(() => Math.max(0, props.headerCount - qcCount.value));
11027
11088
  const sampleTotal = computed(() => sampleTotalFor(currentRoute.value, realCount.value, qcCount.value));
11028
- const qcChip = computed(() => props.qcChip !== void 0 ? props.qcChip : currentRoute.value !== "Mix into groups" ? {
11029
- name: "QC_Pool",
11030
- count: qcCount.value
11031
- } : null);
11089
+ const qcChips = computed(() => {
11090
+ if (props.qcChips !== void 0) return props.qcChips;
11091
+ if (props.qcChip !== void 0) return props.qcChip ? [props.qcChip] : [];
11092
+ return currentRoute.value !== "Mix into groups" ? [{
11093
+ name: "QC_Pool",
11094
+ count: qcCount.value
11095
+ }] : [];
11096
+ });
11032
11097
  const footHint = computed(() => currentRoute.value === "Exclude" ? ` · ${qcCount.value} QC excluded` : currentRoute.value === "Mix into groups" ? " · QC mixed in" : ` · ${qcCount.value} QC overlaid`);
11098
+ const root = ref(null);
11099
+ const controlOf = (index, selector) => root.value?.querySelector(`[data-field="${index}"] ${selector}`) ?? null;
11100
+ async function refocus(index, selector) {
11101
+ await nextTick();
11102
+ controlOf(index, selector)?.focus();
11103
+ }
11033
11104
  function toggle(index) {
11105
+ refocus(index, "[role=\"switch\"]");
11034
11106
  if (controlledEnabled.value) {
11035
11107
  emit("toggle", index);
11036
11108
  return;
@@ -11040,7 +11112,7 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11040
11112
  orderInner.value = wasOn ? orderInner.value.filter((i) => i !== index) : [...orderInner.value, index];
11041
11113
  }
11042
11114
  function moveLayer(index, direction) {
11043
- if (controlledEnabled.value) {
11115
+ if (controlledEnabled.value || props.layers !== void 0) {
11044
11116
  emit("move", index, direction);
11045
11117
  return;
11046
11118
  }
@@ -11052,6 +11124,48 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11052
11124
  [next[from], next[to]] = [next[to], next[from]];
11053
11125
  orderInner.value = next;
11054
11126
  }
11127
+ const reorder = useListReorder({
11128
+ items: () => onCards.value,
11129
+ getId: (card) => card.field.key,
11130
+ getLabel: (card) => card.field.key,
11131
+ itemNoun: "layer",
11132
+ getElement: (id) => {
11133
+ const card = onCards.value.find((candidate) => candidate.field.key === id);
11134
+ return card ? controlOf(card.index, ".fr-row__grip") : null;
11135
+ },
11136
+ onReorder: ({ fromIndex, toIndex }) => {
11137
+ const { index } = onCards.value[fromIndex];
11138
+ const direction = toIndex < fromIndex ? "up" : "down";
11139
+ for (let step = 0; step < Math.abs(toIndex - fromIndex); step++) moveLayer(index, direction);
11140
+ }
11141
+ });
11142
+ const renaming = ref(null);
11143
+ const draftName = ref("");
11144
+ const renameTaken = ref(false);
11145
+ const renameInput = ref([]);
11146
+ async function startRename(card) {
11147
+ renaming.value = card.index;
11148
+ renameTaken.value = false;
11149
+ draftName.value = card.field.key;
11150
+ await nextTick();
11151
+ renameInput.value[0]?.select();
11152
+ }
11153
+ function endRename(card, restoreFocus) {
11154
+ renaming.value = null;
11155
+ renameTaken.value = false;
11156
+ if (restoreFocus) refocus(card.index, ".fr-row__rename");
11157
+ }
11158
+ function commitRename(card, fromBlur = false) {
11159
+ if (renaming.value !== card.index) return;
11160
+ const name = draftName.value.trim();
11161
+ const taken = name !== card.field.key && (props.fields.some((field) => field.key === name) || (props.reservedNames?.includes(name) ?? false));
11162
+ if (taken && !fromBlur) {
11163
+ renameTaken.value = true;
11164
+ return;
11165
+ }
11166
+ endRename(card, !fromBlur);
11167
+ if (name && name !== card.field.key && !taken) emit("rename", card.index, name);
11168
+ }
11055
11169
  function onRouteChange(value) {
11056
11170
  if (controlledRoute.value) {
11057
11171
  emit("update:route", value);
@@ -11069,79 +11183,53 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11069
11183
  });
11070
11184
  }
11071
11185
  return (_ctx, _cache) => {
11072
- return openBlock(), createElementBlock("div", _hoisted_1$42, [
11073
- createElementVNode("div", _hoisted_2$38, [
11186
+ return openBlock(), createElementBlock("div", {
11187
+ ref_key: "root",
11188
+ ref: root,
11189
+ class: "sg-modal fr"
11190
+ }, [
11191
+ createElementVNode("div", _hoisted_1$42, [
11074
11192
  createVNode(Icon, {
11075
11193
  d: ICON.bolt,
11076
11194
  size: 18
11077
11195
  }, null, 8, ["d"]),
11078
11196
  _cache[11] || (_cache[11] = createElementVNode("span", { class: "sg-head__title" }, "Group samples", -1)),
11079
- createElementVNode("span", _hoisted_3$31, [createTextVNode("· " + toDisplayString(__props.headerCount) + " samples", 1), __props.experimentLabel ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(" from " + toDisplayString(__props.experimentLabel), 1)], 64)) : createCommentVNode("", true)]),
11197
+ createElementVNode("span", _hoisted_2$38, [createTextVNode("· " + toDisplayString(__props.headerCount) + " samples", 1), __props.experimentLabel ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(" from " + toDisplayString(__props.experimentLabel), 1)], 64)) : createCommentVNode("", true)]),
11080
11198
  _cache[12] || (_cache[12] = createElementVNode("div", { class: "sg-head__spacer" }, null, -1)),
11081
- createElementVNode("div", _hoisted_4$30, [
11082
- createElementVNode("button", {
11083
- ref: "csvMenuTrigger",
11199
+ createVNode(ActionMenu_default, {
11200
+ label: "CSV",
11201
+ items: csvMenuItems.value,
11202
+ onSelect: onCsvMenuSelect
11203
+ }, {
11204
+ trigger: withCtx(({ open, toggle: toggleMenu, keydown }) => [createElementVNode("button", {
11084
11205
  type: "button",
11085
11206
  class: "fr-upload",
11086
11207
  "aria-haspopup": "menu",
11087
- "aria-expanded": unref(csvMenuOpen),
11088
- onClick: _cache[0] || (_cache[0] = (...args) => unref(toggleCsvMenu) && unref(toggleCsvMenu)(...args))
11208
+ "aria-expanded": open,
11209
+ onClick: toggleMenu,
11210
+ onKeydown: keydown
11089
11211
  }, [
11090
11212
  createVNode(Icon, {
11091
11213
  d: ICON.upload,
11092
11214
  size: 14
11093
11215
  }, null, 8, ["d"]),
11094
- _cache[6] || (_cache[6] = createTextVNode(" CSV ", -1)),
11216
+ _cache[9] || (_cache[9] = createTextVNode(" CSV ", -1)),
11095
11217
  createVNode(Icon, {
11096
11218
  d: ICON.chev,
11097
11219
  size: 13
11098
11220
  }, null, 8, ["d"])
11099
- ], 8, _hoisted_5$28),
11100
- unref(csvMenuOpen) ? (openBlock(), createElementBlock("div", _hoisted_6$26, [
11101
- createElementVNode("button", {
11102
- type: "button",
11103
- class: "fr-csv-item",
11104
- role: "menuitem",
11105
- onClick: chooseUpload
11106
- }, [createVNode(Icon, {
11107
- d: ICON.upload,
11108
- size: 14
11109
- }, null, 8, ["d"]), _cache[7] || (_cache[7] = createTextVNode(" Upload CSV ", -1))]),
11110
- createElementVNode("button", {
11111
- type: "button",
11112
- class: "fr-csv-item",
11113
- role: "menuitem",
11114
- disabled: !__props.canDownloadTemplate,
11115
- "aria-disabled": !__props.canDownloadTemplate,
11116
- title: __props.canDownloadTemplate ? void 0 : "Load samples first",
11117
- onClick: chooseDownloadTemplate
11118
- }, [createVNode(Icon, {
11119
- d: ICON.download,
11120
- size: 14
11121
- }, null, 8, ["d"]), _cache[8] || (_cache[8] = createTextVNode(" Download template ", -1))], 8, _hoisted_7$24),
11122
- createElementVNode("button", {
11123
- type: "button",
11124
- class: "fr-csv-item",
11125
- role: "menuitem",
11126
- disabled: !__props.canDownloadTemplate,
11127
- "aria-disabled": !__props.canDownloadTemplate,
11128
- title: __props.canDownloadTemplate ? void 0 : "Load samples first",
11129
- onClick: chooseDownloadPrefilled
11130
- }, [createVNode(Icon, {
11131
- d: ICON.download,
11132
- size: 14
11133
- }, null, 8, ["d"]), _cache[9] || (_cache[9] = createTextVNode(" Download pre-filled ", -1))], 8, _hoisted_8$22)
11134
- ])) : createCommentVNode("", true),
11135
- createElementVNode("input", {
11136
- ref_key: "fileInput",
11137
- ref: fileInput,
11138
- type: "file",
11139
- accept: ".csv,.tsv,text/csv,text/tab-separated-values",
11140
- hidden: "",
11141
- onChange: onFileChange
11142
- }, null, 544)
11143
- ], 512),
11144
- createElementVNode("div", _hoisted_9$20, [_cache[10] || (_cache[10] = createElementVNode("button", {
11221
+ ], 40, _hoisted_3$31)]),
11222
+ _: 1
11223
+ }, 8, ["items"]),
11224
+ createElementVNode("input", {
11225
+ ref_key: "fileInput",
11226
+ ref: fileInput,
11227
+ type: "file",
11228
+ accept: ".csv,.tsv,text/csv,text/tab-separated-values",
11229
+ hidden: "",
11230
+ onChange: onFileChange
11231
+ }, null, 544),
11232
+ createElementVNode("div", _hoisted_4$30, [_cache[10] || (_cache[10] = createElementVNode("button", {
11145
11233
  type: "button",
11146
11234
  class: "sg-mode__btn sg-mode__btn--on",
11147
11235
  role: "tab",
@@ -11151,104 +11239,166 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11151
11239
  class: "sg-mode__btn",
11152
11240
  role: "tab",
11153
11241
  "aria-selected": false,
11154
- onClick: _cache[1] || (_cache[1] = ($event) => emit("manual"))
11242
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("manual"))
11155
11243
  }, "Manual")]),
11156
11244
  createElementVNode("button", {
11157
11245
  class: "sg-head__close",
11158
11246
  "aria-label": "Close",
11159
- onClick: _cache[2] || (_cache[2] = ($event) => emit("close"))
11247
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("close"))
11160
11248
  }, [createVNode(Icon, {
11161
11249
  d: ICON.close,
11162
11250
  size: 16
11163
11251
  }, null, 8, ["d"])])
11164
11252
  ]),
11165
- createElementVNode("div", _hoisted_10$19, [createElementVNode("div", {
11253
+ createElementVNode("div", _hoisted_5$28, [createElementVNode("div", {
11166
11254
  class: "sg-body__main",
11167
11255
  onDragenter: withModifiers(onDragEnter, ["prevent"]),
11168
- onDragover: _cache[3] || (_cache[3] = withModifiers(() => {}, ["prevent"])),
11256
+ onDragover: _cache[6] || (_cache[6] = withModifiers(() => {}, ["prevent"])),
11169
11257
  onDragleave: withModifiers(onDragLeave, ["prevent"]),
11170
11258
  onDrop: withModifiers(onDrop, ["prevent"])
11171
11259
  }, [
11172
- dragOver.value ? (openBlock(), createElementBlock("div", _hoisted_11$17, [createVNode(Icon, {
11260
+ dragOver.value ? (openBlock(), createElementBlock("div", _hoisted_6$26, [createVNode(Icon, {
11173
11261
  d: ICON.upload,
11174
11262
  size: 22
11175
11263
  }, null, 8, ["d"]), _cache[13] || (_cache[13] = createElementVNode("span", null, "Drop CSV to load samples", -1))])) : createCommentVNode("", true),
11176
- __props.loading ? (openBlock(), createElementBlock("div", _hoisted_12$16, [createVNode(LoadingSpinner_default, {
11264
+ __props.loading ? (openBlock(), createElementBlock("div", _hoisted_7$24, [createVNode(LoadingSpinner_default, {
11177
11265
  size: "md",
11178
11266
  label: "Loading experiment metadata"
11179
11267
  }), _cache[14] || (_cache[14] = createElementVNode("span", null, "Loading experiment metadata…", -1))])) : createCommentVNode("", true),
11180
- _cache[23] || (_cache[23] = createElementVNode("div", { class: "sg-lead" }, [createElementVNode("span", { class: "sg-lead__q" }, "Group by which fields?"), createElementVNode("span", { class: "sg-lead__hint" }, " MINT read the structure of your sample names. Switch on the factors that define your groups — replicate fields are detected and left off. ")], -1)),
11181
- createElementVNode("div", _hoisted_13$16, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.fields, (field, i) => {
11182
- return openBlock(), createElementBlock("div", {
11183
- key: field.key,
11184
- class: normalizeClass(["sg-field", {
11185
- "sg-field--on": on.value[i],
11186
- "fr-field--warn": field.rep && on.value[i]
11187
- }])
11188
- }, [
11189
- createElementVNode("div", _hoisted_14$16, [createElementVNode("span", _hoisted_15$16, toDisplayString(field.key), 1), createElementVNode("span", { class: normalizeClass(["sg-field__role", { "sg-field__role--rep": field.rep }]) }, toDisplayString(field.role), 3)]),
11190
- createElementVNode("button", {
11191
- class: normalizeClass(["sg-switch fr-switch", { on: on.value[i] }]),
11192
- role: "switch",
11193
- "aria-checked": on.value[i],
11194
- "aria-label": "Group by " + field.key,
11195
- onClick: ($event) => toggle(i)
11196
- }, null, 10, _hoisted_16$13),
11197
- on.value[i] && layers.value[i] !== null ? (openBlock(), createElementBlock("div", _hoisted_17$13, [
11198
- createElementVNode("span", _hoisted_18$13, "Layer " + toDisplayString(layers.value[i]), 1),
11268
+ __props.fields.length === 0 && __props.headerCount === 0 && !__props.loading ? (openBlock(), createElementBlock("div", _hoisted_8$22, [
11269
+ _cache[16] || (_cache[16] = createElementVNode("span", { class: "sg-lead__q" }, "No samples to group", -1)),
11270
+ _cache[17] || (_cache[17] = createElementVNode("span", { class: "sg-lead__hint" }, " Upload a CSV with one sample column and one column for each field, or close this dialog and select samples first. ", -1)),
11271
+ createVNode(BaseButton_default, {
11272
+ variant: "secondary",
11273
+ size: "sm",
11274
+ onClick: pickCsv
11275
+ }, {
11276
+ "icon-left": withCtx(() => [createVNode(Icon, {
11277
+ d: ICON.upload,
11278
+ size: 15
11279
+ }, null, 8, ["d"])]),
11280
+ default: withCtx(() => [_cache[15] || (_cache[15] = createTextVNode(" Upload CSV ", -1))]),
11281
+ _: 1
11282
+ })
11283
+ ])) : (openBlock(), createElementBlock(Fragment, { key: 3 }, [
11284
+ __props.anatomy && __props.anatomy.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_9$20, [_cache[18] || (_cache[18] = createElementVNode("span", { class: "fr-label" }, "How MINT read your names", -1)), createElementVNode("div", _hoisted_10$19, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.anatomy, (token, t) => {
11285
+ return openBlock(), createElementBlock("span", {
11286
+ key: t,
11287
+ class: normalizeClass(["fr-token", "fr-token--" + token.state])
11288
+ }, [createElementVNode("span", _hoisted_11$17, toDisplayString(token.text), 1), createElementVNode("span", _hoisted_12$16, toDisplayString(token.caption), 1)], 2);
11289
+ }), 128))])])) : createCommentVNode("", true),
11290
+ _cache[28] || (_cache[28] = createElementVNode("span", { class: "fr-label" }, "Group by", -1)),
11291
+ createElementVNode("div", _hoisted_13$16, [onCards.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_14$16, toDisplayString(__props.fields.length === 0 ? "These sample names have no field to group by." : "No field is on. Switch on a field below."), 1)) : createCommentVNode("", true), (openBlock(true), createElementBlock(Fragment, null, renderList(onCards.value, (card, position) => {
11292
+ return openBlock(), createElementBlock("div", {
11293
+ key: card.field.key,
11294
+ class: normalizeClass(["sg-field sg-field--on fr-row", {
11295
+ "fr-field--warn": card.field.rep,
11296
+ "fr-row--dragging": unref(reorder).isDragging(card.field.key),
11297
+ "fr-row--over": unref(reorder).isDragOver(card.field.key)
11298
+ }]),
11299
+ "data-test": "field-on",
11300
+ "data-field": card.index,
11301
+ draggable: renaming.value !== card.index,
11302
+ onDragstart: ($event) => unref(reorder).onDragStart($event, card, position),
11303
+ onDragover: ($event) => unref(reorder).onDragOver($event, card, position),
11304
+ onDragleave: _cache[4] || (_cache[4] = (...args) => unref(reorder).onDragLeave && unref(reorder).onDragLeave(...args)),
11305
+ onDrop: ($event) => unref(reorder).onDrop($event, card, position),
11306
+ onDragend: _cache[5] || (_cache[5] = (...args) => unref(reorder).onDragEnd && unref(reorder).onDragEnd(...args))
11307
+ }, [
11199
11308
  createElementVNode("button", {
11200
11309
  type: "button",
11201
- class: "sg-field__order-btn",
11202
- disabled: layers.value[i] === 1,
11203
- "aria-label": "Move " + field.key + " up a layer",
11204
- title: "Move up a layer",
11205
- onClick: withModifiers(($event) => moveLayer(i, "up"), ["stop"])
11310
+ class: "fr-row__grip",
11311
+ "aria-label": `Move ${card.field.key}, layer ${position + 1} of ${onCards.value.length}. Press Alt and an arrow key.`,
11312
+ onKeydown: ($event) => unref(reorder).onKeydown($event, card, position)
11206
11313
  }, [createVNode(Icon, {
11207
- d: ICON.chevUp,
11208
- size: 12
11209
- }, null, 8, ["d"])], 8, _hoisted_19$13),
11210
- createElementVNode("button", {
11314
+ d: ICON.grip,
11315
+ size: 14
11316
+ }, null, 8, ["d"])], 40, _hoisted_16$13),
11317
+ createElementVNode("span", _hoisted_17$13, toDisplayString(position + 1), 1),
11318
+ renaming.value === card.index ? withDirectives((openBlock(), createElementBlock("input", {
11319
+ key: 0,
11320
+ ref_for: true,
11321
+ ref_key: "renameInput",
11322
+ ref: renameInput,
11323
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => draftName.value = $event),
11324
+ class: "fr-row__name-input",
11325
+ "aria-label": "New name for " + card.field.key,
11326
+ "aria-invalid": renameTaken.value || void 0,
11327
+ onInput: _cache[3] || (_cache[3] = ($event) => renameTaken.value = false),
11328
+ onKeydown: [withKeys(withModifiers(($event) => commitRename(card), ["prevent"]), ["enter"]), withKeys(withModifiers(($event) => endRename(card, true), ["stop"]), ["esc"])],
11329
+ onBlur: ($event) => commitRename(card, true)
11330
+ }, null, 40, _hoisted_18$13)), [[vModelText, draftName.value]]) : createCommentVNode("", true),
11331
+ renaming.value === card.index && renameTaken.value ? (openBlock(), createElementBlock("span", _hoisted_19$13, " Another field has this name ")) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [createElementVNode("span", _hoisted_20$12, toDisplayString(card.field.key), 1), __props.renamable ? (openBlock(), createElementBlock("button", {
11332
+ key: 0,
11211
11333
  type: "button",
11212
- class: "sg-field__order-btn",
11213
- disabled: layers.value[i] === layerOrder.value.length,
11214
- "aria-label": "Move " + field.key + " down a layer",
11215
- title: "Move down a layer",
11216
- onClick: withModifiers(($event) => moveLayer(i, "down"), ["stop"])
11334
+ class: "fr-row__rename",
11335
+ "aria-label": "Rename " + card.field.key,
11336
+ onClick: ($event) => startRename(card)
11217
11337
  }, [createVNode(Icon, {
11218
- d: ICON.chev,
11338
+ d: ICON.pencil,
11219
11339
  size: 12
11220
- }, null, 8, ["d"])], 8, _hoisted_20$12)
11221
- ])) : createCommentVNode("", true),
11222
- createElementVNode("div", _hoisted_21$9, toDisplayString(field.vals), 1)
11223
- ], 2);
11224
- }), 128))]),
11225
- summary.value.kind === "mute" ? (openBlock(), createElementBlock("div", _hoisted_22$9, [createElementVNode("span", _hoisted_23$7, [createVNode(Icon, {
11226
- d: ICON.info,
11227
- size: 16
11228
- }, null, 8, ["d"])]), _cache[15] || (_cache[15] = createElementVNode("span", null, "Turn on at least one field to form groups.", -1))])) : summary.value.kind === "warn" ? (openBlock(), createElementBlock("div", _hoisted_24$7, [createElementVNode("span", _hoisted_25$7, [createVNode(Icon, {
11229
- d: ICON.warn,
11230
- size: 16
11231
- }, null, 8, ["d"])]), createElementVNode("span", null, [
11232
- _cache[16] || (_cache[16] = createTextVNode(" Including ", -1)),
11233
- createElementVNode("b", null, toDisplayString(repField.value?.key ?? "Replicate"), 1),
11234
- _cache[17] || (_cache[17] = createTextVNode(" makes every sample unique — ", -1)),
11235
- createElementVNode("b", null, toDisplayString(baseGroups.value.length) + " groups of 1", 1),
11236
- _cache[18] || (_cache[18] = createTextVNode(". Turn it off to keep ", -1)),
11237
- createElementVNode("code", null, toDisplayString(repRange.value), 1),
11238
- _cache[19] || (_cache[19] = createTextVNode(" together. ", -1))
11239
- ])])) : (openBlock(), createElementBlock("div", _hoisted_26$5, [createElementVNode("span", _hoisted_27$5, [createVNode(Icon, {
11240
- d: ICON.info,
11241
- size: 16
11242
- }, null, 8, ["d"])]), createElementVNode("span", null, [
11243
- _cache[20] || (_cache[20] = createTextVNode(" Grouping by ", -1)),
11244
- createElementVNode("b", null, toDisplayString(activeFactors.value.join(" × ")), 1),
11245
- _cache[21] || (_cache[21] = createTextVNode(" → ", -1)),
11246
- createElementVNode("b", null, [createTextVNode(toDisplayString(baseGroups.value.length) + " groups", 1), uniform.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(" of " + toDisplayString(counts.value[0]), 1)], 64)) : createCommentVNode("", true)]),
11247
- _cache[22] || (_cache[22] = createTextVNode(". Replicates kept together. ", -1))
11248
- ])]))
11249
- ], 32), createElementVNode("div", _hoisted_28$4, [
11250
- createElementVNode("div", _hoisted_29$4, [_cache[24] || (_cache[24] = createTextVNode(" Resulting groups ", -1)), createElementVNode("span", _hoisted_30$3, toDisplayString(groups.value.length) + " groups · " + toDisplayString(groupedSampleTotal.value) + " samples", 1)]),
11251
- createElementVNode("div", _hoisted_31$3, [groups.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_32$3, "No groups yet — pick a field to start.")) : (openBlock(true), createElementBlock(Fragment, { key: 1 }, renderList(groups.value, (group) => {
11340
+ }, null, 8, ["d"])], 8, _hoisted_21$9)) : createCommentVNode("", true)], 64)),
11341
+ createElementVNode("span", _hoisted_22$9, toDisplayString(card.field.vals), 1),
11342
+ __props.hints?.[card.index] ? (openBlock(), createElementBlock("span", _hoisted_23$7, toDisplayString(__props.hints[card.index]), 1)) : createCommentVNode("", true),
11343
+ createVNode(BaseToggle_default, {
11344
+ size: "sm",
11345
+ "model-value": true,
11346
+ "aria-label": "Group by " + card.field.key,
11347
+ "onUpdate:modelValue": ($event) => toggle(card.index)
11348
+ }, null, 8, ["aria-label", "onUpdate:modelValue"])
11349
+ ], 42, _hoisted_15$16);
11350
+ }), 128))]),
11351
+ createElementVNode("span", {
11352
+ style: normalizeStyle(unref(reorder).announcerStyle),
11353
+ "aria-live": "polite"
11354
+ }, toDisplayString(unref(reorder).announcement.value), 5),
11355
+ offCards.value.length > 0 ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [_cache[19] || (_cache[19] = createElementVNode("span", { class: "fr-label" }, "Left off", -1)), createElementVNode("div", _hoisted_24$7, [(openBlock(true), createElementBlock(Fragment, null, renderList(offCards.value, (card) => {
11356
+ return openBlock(), createElementBlock("div", {
11357
+ key: card.field.key,
11358
+ class: "sg-field fr-row",
11359
+ "data-test": "field-off",
11360
+ "data-field": card.index
11361
+ }, [
11362
+ createElementVNode("span", _hoisted_26$5, toDisplayString(card.field.key), 1),
11363
+ createElementVNode("span", { class: normalizeClass(["sg-field__role", { "sg-field__role--rep": card.field.rep }]) }, toDisplayString(card.field.role), 3),
11364
+ createElementVNode("span", _hoisted_27$5, toDisplayString(card.field.vals), 1),
11365
+ createVNode(BaseToggle_default, {
11366
+ size: "sm",
11367
+ "model-value": false,
11368
+ "aria-label": "Group by " + card.field.key,
11369
+ "onUpdate:modelValue": ($event) => toggle(card.index)
11370
+ }, null, 8, ["aria-label", "onUpdate:modelValue"])
11371
+ ], 8, _hoisted_25$7);
11372
+ }), 128))])], 64)) : createCommentVNode("", true),
11373
+ summary.value.kind === "mute" ? (openBlock(), createElementBlock("div", _hoisted_28$4, [createElementVNode("span", _hoisted_29$4, [createVNode(Icon, {
11374
+ d: ICON.info,
11375
+ size: 16
11376
+ }, null, 8, ["d"])]), _cache[20] || (_cache[20] = createElementVNode("span", null, "Turn on at least one field to form groups.", -1))])) : summary.value.kind === "warn" ? (openBlock(), createElementBlock("div", _hoisted_30$3, [createElementVNode("span", _hoisted_31$3, [createVNode(Icon, {
11377
+ d: ICON.warn,
11378
+ size: 16
11379
+ }, null, 8, ["d"])]), createElementVNode("span", null, [
11380
+ _cache[21] || (_cache[21] = createTextVNode(" Including ", -1)),
11381
+ createElementVNode("b", null, toDisplayString(repOnField.value?.key), 1),
11382
+ _cache[22] || (_cache[22] = createTextVNode(" makes every sample unique — ", -1)),
11383
+ createElementVNode("b", null, toDisplayString(baseGroups.value.length) + " groups of 1", 1),
11384
+ _cache[23] || (_cache[23] = createTextVNode(". Switch it off to keep replicates together. ", -1))
11385
+ ])])) : summary.value.kind === "singletons" ? (openBlock(), createElementBlock("div", _hoisted_32$3, [createElementVNode("span", _hoisted_33$3, [createVNode(Icon, {
11386
+ d: ICON.warn,
11387
+ size: 16
11388
+ }, null, 8, ["d"])]), createElementVNode("span", null, [createElementVNode("b", null, toDisplayString(counts.value.filter((count) => count === 1).length) + " of " + toDisplayString(baseGroups.value.length) + " groups", 1), _cache[24] || (_cache[24] = createTextVNode(" have one sample. Switch off a field to form larger groups. ", -1))])])) : (openBlock(), createElementBlock("div", _hoisted_34$2, [createElementVNode("span", _hoisted_35$2, [createVNode(Icon, {
11389
+ d: ICON.info,
11390
+ size: 16
11391
+ }, null, 8, ["d"])]), createElementVNode("span", null, [
11392
+ _cache[25] || (_cache[25] = createTextVNode(" Grouping by ", -1)),
11393
+ createElementVNode("b", null, toDisplayString(activeFactors.value.join(" × ")), 1),
11394
+ _cache[26] || (_cache[26] = createTextVNode(" → ", -1)),
11395
+ createElementVNode("b", null, [createTextVNode(toDisplayString(baseGroups.value.length) + " groups", 1), uniform.value ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(" of " + toDisplayString(counts.value[0]), 1)], 64)) : createCommentVNode("", true)]),
11396
+ _cache[27] || (_cache[27] = createTextVNode(". ", -1))
11397
+ ])]))
11398
+ ], 64))
11399
+ ], 32), createElementVNode("div", _hoisted_36$2, [
11400
+ createElementVNode("div", _hoisted_37$2, [_cache[29] || (_cache[29] = createTextVNode(" Resulting groups ", -1)), createElementVNode("span", _hoisted_38$2, toDisplayString(groups.value.length) + " groups · " + toDisplayString(groupedSampleTotal.value) + " samples", 1)]),
11401
+ createElementVNode("div", _hoisted_39$2, [groups.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_40$2, "No groups yet — pick a field to start.")) : (openBlock(true), createElementBlock(Fragment, { key: 1 }, renderList(groups.value, (group) => {
11252
11402
  return openBlock(), createElementBlock("div", {
11253
11403
  key: group.name,
11254
11404
  class: "sg-group"
@@ -11257,7 +11407,7 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11257
11407
  class: "sg-dot",
11258
11408
  style: normalizeStyle(group.name === unref("QC_Pool") ? unref(QC_DOT_STYLE) : { background: group.color })
11259
11409
  }, null, 4),
11260
- createElementVNode("span", _hoisted_33$3, toDisplayString(group.name), 1),
11410
+ createElementVNode("span", _hoisted_41$2, toDisplayString(group.name), 1),
11261
11411
  createElementVNode("span", {
11262
11412
  class: "sg-group__bar",
11263
11413
  style: normalizeStyle({
@@ -11265,34 +11415,36 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11265
11415
  "--g": group.color
11266
11416
  })
11267
11417
  }, null, 4),
11268
- createElementVNode("span", _hoisted_34$2, toDisplayString(group.count), 1)
11418
+ createElementVNode("span", _hoisted_42$1, toDisplayString(group.count), 1)
11269
11419
  ]);
11270
11420
  }), 128))]),
11271
- createElementVNode("div", _hoisted_35$2, [createElementVNode("div", _hoisted_36$2, [_cache[26] || (_cache[26] = createElementVNode("span", { class: "sg-qc__title" }, "Quality control", -1)), createElementVNode("select", {
11272
- class: "sg-qc__route",
11273
- value: currentRoute.value,
11274
- onChange: _cache[4] || (_cache[4] = ($event) => onRouteChange($event.target.value))
11275
- }, [..._cache[25] || (_cache[25] = [
11276
- createElementVNode("option", null, "Overlay only", -1),
11277
- createElementVNode("option", null, "Exclude", -1),
11278
- createElementVNode("option", null, "Mix into groups", -1)
11279
- ])], 40, _hoisted_37$2)]), qcChip.value ? (openBlock(), createElementBlock("span", {
11280
- key: 0,
11281
- class: normalizeClass(["sg-qc__chip", { "fr-qc__chip--off": currentRoute.value === "Exclude" }])
11282
- }, [createElementVNode("span", {
11283
- class: "sg-dot",
11284
- style: normalizeStyle(unref(QC_DOT_STYLE))
11285
- }, null, 4), createTextVNode(toDisplayString(qcChip.value.name) + " ×" + toDisplayString(qcChip.value.count), 1)], 2)) : createCommentVNode("", true)])
11421
+ createElementVNode("div", _hoisted_43$1, [createElementVNode("div", _hoisted_44$1, [_cache[30] || (_cache[30] = createElementVNode("span", { class: "sg-qc__title" }, "Quality control", -1)), createVNode(SegmentedControl_default, {
11422
+ class: "fr-qc__route",
11423
+ size: "sm",
11424
+ variant: "card",
11425
+ "aria-label": "Quality control routing",
11426
+ "model-value": currentRoute.value,
11427
+ options: QC_ROUTE_OPTIONS,
11428
+ "onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => onRouteChange($event))
11429
+ }, null, 8, ["model-value"])]), createElementVNode("div", _hoisted_45$1, [(openBlock(true), createElementBlock(Fragment, null, renderList(qcChips.value, (chip) => {
11430
+ return openBlock(), createElementBlock("span", {
11431
+ key: chip.name,
11432
+ class: normalizeClass(["sg-qc__chip", { "fr-qc__chip--off": currentRoute.value === "Exclude" }])
11433
+ }, [createElementVNode("span", {
11434
+ class: "sg-dot",
11435
+ style: normalizeStyle(unref(QC_DOT_STYLE))
11436
+ }, null, 4), createTextVNode(toDisplayString(chip.name) + " ×" + toDisplayString(chip.count), 1)], 2);
11437
+ }), 128))])])
11286
11438
  ])]),
11287
- createElementVNode("div", _hoisted_38$2, [
11288
- createElementVNode("span", _hoisted_39$2, "Applies to " + toDisplayString(sampleTotal.value) + " samples" + toDisplayString(footHint.value), 1),
11289
- _cache[29] || (_cache[29] = createElementVNode("div", { class: "sg-foot__spacer" }, null, -1)),
11439
+ createElementVNode("div", _hoisted_46$1, [
11440
+ createElementVNode("span", _hoisted_47$1, "Applies to " + toDisplayString(sampleTotal.value) + " samples" + toDisplayString(footHint.value), 1),
11441
+ _cache[33] || (_cache[33] = createElementVNode("div", { class: "sg-foot__spacer" }, null, -1)),
11290
11442
  createVNode(BaseButton_default, {
11291
11443
  variant: "ghost",
11292
11444
  size: "sm",
11293
- onClick: _cache[5] || (_cache[5] = ($event) => emit("cancel"))
11445
+ onClick: _cache[8] || (_cache[8] = ($event) => emit("cancel"))
11294
11446
  }, {
11295
- default: withCtx(() => [..._cache[27] || (_cache[27] = [createTextVNode("Cancel", -1)])]),
11447
+ default: withCtx(() => [..._cache[31] || (_cache[31] = [createTextVNode("Cancel", -1)])]),
11296
11448
  _: 1
11297
11449
  }),
11298
11450
  createVNode(BaseButton_default, {
@@ -11305,11 +11457,11 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11305
11457
  d: ICON.check,
11306
11458
  size: 15
11307
11459
  }, null, 8, ["d"])]),
11308
- default: withCtx(() => [_cache[28] || (_cache[28] = createTextVNode(" Apply grouping ", -1))]),
11460
+ default: withCtx(() => [_cache[32] || (_cache[32] = createTextVNode(" Apply grouping ", -1))]),
11309
11461
  _: 1
11310
11462
  }, 8, ["disabled"])
11311
11463
  ])
11312
- ]);
11464
+ ], 512);
11313
11465
  };
11314
11466
  }
11315
11467
  });
@@ -11928,11 +12080,73 @@ function parseCSV(text) {
11928
12080
  }
11929
12081
  //#endregion
11930
12082
  //#region src/components/AutoGroupModal.adapter.ts
12083
+ var PREVIEW_VALUES = 3;
12084
+ /** Value preview of a field card: the first three values, then the count of the rest. */
12085
+ function previewValues(values) {
12086
+ const shown = values.slice(0, PREVIEW_VALUES).join(", ");
12087
+ return values.length > PREVIEW_VALUES ? `${shown} … +${values.length - PREVIEW_VALUES}` : shown;
12088
+ }
12089
+ /** Experiment line of the modal header: `exp157 · 2025-10-13 · LT`. */
12090
+ function experimentLabel(header) {
12091
+ return [
12092
+ header.code,
12093
+ header.date,
12094
+ header.operator
12095
+ ].filter(Boolean).join(" · ");
12096
+ }
12097
+ /**
12098
+ * Names of all columns of the experimental classes. A field cannot take one
12099
+ * of them: the engine merges fields by name.
12100
+ */
12101
+ function engineColumnNames(schemas) {
12102
+ const names = Object.values(schemas).filter((schema) => isExperimentalKind(schema.classKind)).flatMap((schema) => schema.columns.map((column) => column.displayName ?? column.name));
12103
+ return [...new Set(names)];
12104
+ }
12105
+ /** One chip for each QC-type class, with its sample count. */
12106
+ function engineQcChips(classes) {
12107
+ return classes.filter((c) => !isExperimentalKind(c.kind)).map((c) => ({
12108
+ name: c.label,
12109
+ count: c.members.length
12110
+ }));
12111
+ }
12112
+ /**
12113
+ * The tokens of one study sample with the role the engine gave each of them,
12114
+ * so the user sees how the names were read. The sample is the first member of
12115
+ * the largest experimental class. `cards` supplies the on/off state and the
12116
+ * layer of the tokens that are grouping fields.
12117
+ */
12118
+ function engineAnatomy(classes, schemas, tokenized, cards) {
12119
+ const cls = classes.filter((c) => isExperimentalKind(c.kind) && schemas[classKey(c)]).sort((a, b) => b.members.length - a.members.length)[0];
12120
+ if (!cls) return [];
12121
+ const tokens = tokenized[cls.members[0]] ?? [];
12122
+ return schemas[classKey(cls)].columns.flatMap((column) => {
12123
+ const text = column.sourceIndices.map((index) => tokens[index] ?? "").join("_");
12124
+ if (!text) return [];
12125
+ const name = column.displayName ?? column.name;
12126
+ const card = cards.fields.findIndex((field) => field.key === name);
12127
+ if (card === -1) return [{
12128
+ text,
12129
+ caption: /^(Field \d+|Username.*|.* \d+)$/.test(name) ? roleLabel(column.role).toLowerCase() : name,
12130
+ state: "fixed"
12131
+ }];
12132
+ const layer = cards.layers[card];
12133
+ return [layer === null ? {
12134
+ text,
12135
+ caption: name,
12136
+ state: "off"
12137
+ } : {
12138
+ text,
12139
+ caption: String(layer),
12140
+ state: "on"
12141
+ }];
12142
+ });
12143
+ }
11931
12144
  var ROLE_LABELS = {
11932
12145
  "constant": "Constant",
11933
12146
  "factor": "Factor",
11934
12147
  "replicate": "Replicate",
11935
12148
  "run-order": "Run",
12149
+ "id": "Sample ID",
11936
12150
  "numeric": "Numeric",
11937
12151
  "class-tag": "Class tag",
11938
12152
  "ignore": "Ignore"
@@ -11942,14 +12156,15 @@ function roleLabel(role) {
11942
12156
  return role ? ROLE_LABELS[role] : "Factor";
11943
12157
  }
11944
12158
  /**
11945
- * Columns worth offering as grouping toggles: factors and replicates, plus any
11946
- * column already enabled in `groupBy` (so a manual group-by choice still shows).
12159
+ * Columns worth offering as grouping toggles: factors, numerics, replicates and
12160
+ * sample IDs (the last two are off by default), plus any column already enabled
12161
+ * in `groupBy` (so a manual group-by choice still shows).
11947
12162
  * Preserves the engine's column order. `groupBy` is the schema's ORDERED layer
11948
12163
  * list — entry N is hierarchy layer N of the composed group names.
11949
12164
  */
11950
12165
  function candidateColumns(columns, groupBy) {
11951
12166
  const enabled = new Set(groupBy);
11952
- return columns.filter((c) => c.role === "factor" || c.role === "replicate" || enabled.has(c.index));
12167
+ return columns.filter((c) => GROUPING_FIELD_ROLES.has(c.role) || enabled.has(c.index));
11953
12168
  }
11954
12169
  /**
11955
12170
  * Build the batch-wide field recipe from every class schema. The active schema
@@ -11965,6 +12180,7 @@ function engineSchemasToCards(schemas, activeClassKey) {
11965
12180
  const enabledOrder = [];
11966
12181
  const enabled = /* @__PURE__ */ new Set();
11967
12182
  for (const [, schema] of ordered) {
12183
+ if (!isExperimentalKind(schema.classKind)) continue;
11968
12184
  for (const index of schema.groupBy) {
11969
12185
  const column = schema.columns.find((candidate) => candidate.index === index);
11970
12186
  if (!column) continue;
@@ -11984,8 +12200,8 @@ function engineSchemasToCards(schemas, activeClassKey) {
11984
12200
  fields: cards.map(([key, column]) => ({
11985
12201
  key,
11986
12202
  role: roleLabel(column.role),
11987
- vals: column.uniqueValues.join(", "),
11988
- rep: column.role === "replicate"
12203
+ vals: previewValues(column.uniqueValues),
12204
+ rep: column.role === "replicate" || column.role === "id"
11989
12205
  })),
11990
12206
  enabled: cards.map(([key]) => enabled.has(key)),
11991
12207
  layers: cards.map(([key]) => {
@@ -12169,12 +12385,20 @@ var AutoGroupModal_default = /* @__PURE__ */ defineComponent({
12169
12385
  const fieldCards = computed(() => engineSchemasToCards(autoGroup.schemas.value, autoGroup.activeClassKey.value));
12170
12386
  const railGroups = computed(() => engineGroupsToRail(autoGroup.result.value.experimentalGroups ?? autoGroup.groups.value));
12171
12387
  const qcSampleCount = computed(() => autoGroup.classes.value.filter((c) => c.kind !== "biological" && c.kind !== "unknown").reduce((acc, c) => acc + c.members.length, 0));
12172
- const qcChip = computed(() => qcRouting.value !== "mix" && qcSampleCount.value > 0 ? {
12173
- name: "QC_Pool",
12174
- count: qcSampleCount.value
12175
- } : null);
12388
+ const qcChips = computed(() => qcRouting.value === "mix" ? [] : engineQcChips(autoGroup.classes.value));
12176
12389
  const fieldRecipeRoute = computed(() => engineToQcRoute(qcRouting.value));
12177
12390
  const headerCount = computed(() => autoGroup.samples.value.length);
12391
+ const headerLabel = computed(() => experimentLabel(autoGroup.experiment.value));
12392
+ const reservedNames = computed(() => engineColumnNames(autoGroup.schemas.value));
12393
+ const anatomy = computed(() => autoGroup.inputMode.value === "paste" ? engineAnatomy(autoGroup.classes.value, autoGroup.schemas.value, autoGroup.tokenized.value, fieldCards.value) : []);
12394
+ const fieldHints = computed(() => {
12395
+ const needsHelp = mostlySingletons(railGroups.value);
12396
+ return fieldCards.value.fields.map((field, index) => {
12397
+ if (!needsHelp || !fieldCards.value.enabled[index]) return null;
12398
+ const remaining = autoGroup.groupCountWithout(field.key);
12399
+ return `off → ${remaining} ${remaining === 1 ? "group" : "groups"}`;
12400
+ });
12401
+ });
12178
12402
  function onToggleField(cardIndex) {
12179
12403
  const field = fieldCards.value.fields[cardIndex];
12180
12404
  if (!field) return;
@@ -12185,6 +12409,11 @@ var AutoGroupModal_default = /* @__PURE__ */ defineComponent({
12185
12409
  if (!field) return;
12186
12410
  autoGroup.moveGroupByField(field.key, direction);
12187
12411
  }
12412
+ function onRenameField(cardIndex, name) {
12413
+ const field = fieldCards.value.fields[cardIndex];
12414
+ if (!field) return;
12415
+ autoGroup.renameGroupByField(field.key, name);
12416
+ }
12188
12417
  function onRouteChange(route) {
12189
12418
  setQcRouting(qcRouteToEngine(route));
12190
12419
  }
@@ -12293,15 +12522,20 @@ var AutoGroupModal_default = /* @__PURE__ */ defineComponent({
12293
12522
  enabled: fieldCards.value.enabled,
12294
12523
  layers: fieldCards.value.layers,
12295
12524
  groups: railGroups.value,
12296
- "qc-chip": qcChip.value,
12525
+ "qc-chips": qcChips.value,
12297
12526
  route: fieldRecipeRoute.value,
12298
12527
  "header-count": headerCount.value,
12299
12528
  "qc-count": qcSampleCount.value,
12300
12529
  "can-download-template": unref(autoGroup).canDownloadTemplate.value,
12301
12530
  loading: experimentLoading.value,
12302
- "experiment-label": "",
12531
+ "experiment-label": headerLabel.value,
12532
+ anatomy: anatomy.value,
12533
+ hints: fieldHints.value,
12534
+ "reserved-names": reservedNames.value,
12535
+ renamable: "",
12303
12536
  onToggle: onToggleField,
12304
12537
  onMove: onMoveField,
12538
+ onRename: onRenameField,
12305
12539
  "onUpdate:route": onRouteChange,
12306
12540
  onCsvFile,
12307
12541
  onDownloadTemplate,
@@ -12315,12 +12549,16 @@ var AutoGroupModal_default = /* @__PURE__ */ defineComponent({
12315
12549
  "enabled",
12316
12550
  "layers",
12317
12551
  "groups",
12318
- "qc-chip",
12552
+ "qc-chips",
12319
12553
  "route",
12320
12554
  "header-count",
12321
12555
  "qc-count",
12322
12556
  "can-download-template",
12323
- "loading"
12557
+ "loading",
12558
+ "experiment-label",
12559
+ "anatomy",
12560
+ "hints",
12561
+ "reserved-names"
12324
12562
  ])) : (openBlock(), createBlock(SmartGroupManual_default, {
12325
12563
  key: manualKey.value,
12326
12564
  "sample-names": manualSampleNames.value,
@@ -22896,4 +23134,4 @@ var components_exports = /* @__PURE__ */ __exportAll({
22896
23134
  //#endregion
22897
23135
  export { PlateEditor_default as $, SampleLegend_default as A, AppToastContainer_default as At, StatusIndicator_default as B, ChemicalFormula_default as C, AppAvatarMenu_default as Ct, ReagentEditor_default as D, ActionMenu_default as Dt, SampleHierarchyTree_default as E, ThemeToggle_default as Et, SettingsModal_default as F, Calendar_default as Ft, ControlWorkspaceView_default as G, LayoutResizeHandle_default as H, AdductText_default as I, BasePill_default as It, SampleSelector_default as J, ComponentBindingRenderer_default as K, Breadcrumb_default as L, SegmentedControl_default as Lt, BioTemplatePackWorkspaceView_default as M, FormField_default as Mt, BioTemplateExperimentWorkspaceView_default as N, DataFrame_default as Nt, SmartGroupModal_default as O, IconButton_default as Ot, BioTemplateRenderer_default as P, LoadingSpinner_default as Pt, ReagentList_default as Q, Avatar_default as R, BaseTabs_default as Rt, sequenceSamplesRemaining as S, AppTopBar_default as St, ProtocolStepEditor_default as T, CollapsibleCard_default as Tt, AppContainer_default as U, Divider_default as V, DoseDesignWorkspaceView_default as W, SmartGroupManual_default as X, AutoGroupModal_default as Y, SmartGroupFieldRecipe_default as Z, estimateSequenceFinishDate as _, AppLayout_default as _t, ArtifactSaveDialog_default as a, GeneratedPluginView_default as at, formatSequenceRemaining as b, FormActions_default as bt, SectionCard_default as c, GeneratedPlotlyResult_default as ct, ExperimentCodeBadge_default as d, ChartContainer_default as dt, ConfirmDialog_default as et, ExperimentDataViewer_default as f, generatedJobDefaults as ft, SequenceProgressBar_default as g, MobileSupportGate_default as gt, InstrumentStateBadge_default as h, PluginWorkspaceView_default as ht, ExperimentPopover_default as i, DoseCalculator_default as it, BioTemplatePresetWorkspaceView_default as j, AlertBox_default as jt, GroupAssigner_default as k, RuntimeUpdateNotice_default as kt, ResourceCard_default as l, PlotlyChart_default as lt, AuditTrail_default as m, normalizeGeneratedJobInput as mt, FilePicker_default as n, sampleTypeMarker as nt, ArtifactSelectorModal_default as o, JobsStatusTray_default as ot, BatchProgressList_default as p, generatedJobFormSchema as pt, ScheduleCalendar_default as q, FitPanel_default as r, ExperimentTimeline_default as rt, ExperimentSelectorModal_default as s, GeneratedResultRenderer_default as st, components_exports as t, WellPlate_default as tt, TimeRangeInput_default as u, mintPlotlyTemplate as ut, estimateSequenceRemainingSeconds as v, AppSidebar_default as vt, ScientificNumber_default as w, PluginIcon_default as wt, sequenceProgressPercent as x, StepWizard_default as xt, formatSequenceEta as y, FormBuilder_default as yt, ProgressBar_default as z, SearchableSelect_default as zt };
22898
23136
 
22899
- //# sourceMappingURL=components-vHfxV2PB.js.map
23137
+ //# sourceMappingURL=components-CEePoD3W.js.map