@morscherlab/mint-sdk 1.3.0-beta.9 → 1.3.1

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/SmartGroup.types.d.ts +22 -0
  7. package/dist/components/SmartGroupFieldRecipe.groups.d.ts +10 -2
  8. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -0
  9. package/dist/components/index.js +1 -1
  10. package/dist/{components-GDDvof8D.js → components-bbanLd1u.js} +510 -287
  11. package/dist/components-bbanLd1u.js.map +1 -0
  12. package/dist/composables/autoGroup/classify.d.ts +7 -1
  13. package/dist/composables/autoGroup/columns.d.ts +16 -1
  14. package/dist/composables/autoGroup/index.d.ts +5 -7
  15. package/dist/composables/autoGroup/tokenize.d.ts +36 -1
  16. package/dist/composables/index.js +2 -2
  17. package/dist/composables/useAutoGroup.d.ts +13 -2
  18. package/dist/{composables-D6uqp0UF.js → composables-BbHozlA7.js} +2 -2
  19. package/dist/{composables-D6uqp0UF.js.map → composables-BbHozlA7.js.map} +1 -1
  20. package/dist/index.js +3 -3
  21. package/dist/install.js +1 -1
  22. package/dist/permissions.js +2 -2
  23. package/dist/permissions.js.map +1 -1
  24. package/dist/styles.css +129 -96
  25. package/dist/types/auto-group.d.ts +1 -1
  26. package/dist/{validation-Dmp9Q5W0.js → validation-CukfLjy-.js} +323 -187
  27. package/dist/validation-CukfLjy-.js.map +1 -0
  28. package/package.json +2 -2
  29. package/src/__tests__/components/AppTopBarPageSelector.test.ts +4 -4
  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/SampleSelector.test.ts +14 -16
  33. package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +23 -0
  34. package/src/__tests__/components/SmartGroupFieldRecipe.test.ts +176 -0
  35. package/src/__tests__/composables/autoGroup/columns.test.ts +100 -11
  36. package/src/__tests__/composables/autoGroup/fieldEdits.test.ts +74 -0
  37. package/src/__tests__/composables/autoGroup/fingerprint.test.ts +9 -0
  38. package/src/__tests__/composables/autoGroup/golden.test.ts +187 -0
  39. package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +7 -7
  40. package/src/__tests__/composables/autoGroup/tokenLength.test.ts +7 -0
  41. package/src/__tests__/composables/autoGroup/tokenize.test.ts +78 -1
  42. package/src/__tests__/composables/useAutoGroup.test.ts +2 -1
  43. package/src/__tests__/fixtures/auto-group/share-exp021.txt +103 -0
  44. package/src/__tests__/fixtures/auto-group/share-exp031.txt +39 -0
  45. package/src/__tests__/fixtures/auto-group/share-exp114.txt +123 -0
  46. package/src/__tests__/fixtures/auto-group/share-exp214.txt +63 -0
  47. package/src/__tests__/utils/permissions.test.ts +15 -0
  48. package/src/components/AutoGroupModal.adapter.ts +80 -9
  49. package/src/components/AutoGroupModal.vue +41 -8
  50. package/src/components/SmartGroup.types.ts +23 -0
  51. package/src/components/SmartGroupFieldRecipe.groups.ts +14 -2
  52. package/src/components/SmartGroupFieldRecipe.vue +289 -169
  53. package/src/components/internal/AppTopBarPageSelectorInternal.vue +3 -6
  54. package/src/composables/autoGroup/classify.ts +26 -7
  55. package/src/composables/autoGroup/columns.ts +144 -36
  56. package/src/composables/autoGroup/compose.ts +8 -10
  57. package/src/composables/autoGroup/fingerprint.ts +1 -1
  58. package/src/composables/autoGroup/index.ts +5 -12
  59. package/src/composables/autoGroup/tokenLength.ts +8 -0
  60. package/src/composables/autoGroup/tokenize.ts +148 -1
  61. package/src/composables/autoGroup/vocab.json +5 -11
  62. package/src/composables/useAutoGroup.ts +92 -79
  63. package/src/permissions.ts +3 -2
  64. package/src/styles/components/app-page-selector.css +58 -14
  65. package/src/styles/components/smart-group.css +71 -82
  66. package/src/types/auto-group.ts +1 -0
  67. package/dist/components-GDDvof8D.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";
@@ -2866,7 +2866,7 @@ var _hoisted_4$49 = {
2866
2866
  };
2867
2867
  var _hoisted_5$47 = {
2868
2868
  key: 0,
2869
- class: "mint-popover mint-page-selector__menu",
2869
+ class: "mint-page-selector__menu",
2870
2870
  role: "menu"
2871
2871
  };
2872
2872
  var _hoisted_6$42 = {
@@ -2937,40 +2937,27 @@ var AppTopBarPageSelectorInternal_default = /* @__PURE__ */ defineComponent({
2937
2937
  "stroke-linecap": "round",
2938
2938
  "stroke-linejoin": "round"
2939
2939
  }, [..._cache[1] || (_cache[1] = [createElementVNode("path", { d: "m6 9 6 6 6-6" }, null, -1)])], 2))])
2940
- ], 10, _hoisted_1$66), unref(isOpen) ? (openBlock(), createElementBlock("div", _hoisted_5$47, [_cache[3] || (_cache[3] = createElementVNode("div", { class: "mint-page-selector__menu-title" }, "Go to", -1)), (openBlock(true), createElementBlock(Fragment, null, renderList(normalizedPages.value, (page) => {
2940
+ ], 10, _hoisted_1$66), unref(isOpen) ? (openBlock(), createElementBlock("div", _hoisted_5$47, [_cache[2] || (_cache[2] = createElementVNode("div", { class: "mint-page-selector__menu-title" }, "Go to", -1)), (openBlock(true), createElementBlock(Fragment, null, renderList(normalizedPages.value, (page) => {
2941
2941
  return openBlock(), createBlock(ActionItemInternal_default, {
2942
2942
  key: page.id,
2943
2943
  href: page.href,
2944
2944
  to: page.to,
2945
2945
  disabled: page.disabled,
2946
2946
  class: normalizeClass([
2947
- "mint-menu-item mint-page-selector__item",
2948
- { "mint-menu-item--selected mint-page-selector__item--active": page.id === current.value?.id },
2947
+ "mint-page-selector__item",
2948
+ { "mint-page-selector__item--active": page.id === current.value?.id },
2949
2949
  { "mint-page-selector__item--disabled": page.disabled }
2950
2950
  ]),
2951
2951
  role: "menuitem",
2952
2952
  onClick: ($event) => handleSelect(page)
2953
2953
  }, {
2954
- default: withCtx(() => [
2955
- createElementVNode("span", { class: normalizeClass(["mint-page-selector__item-icon", { "mint-page-selector__item-icon--metadata": unref(isPluginIconFormat)(page.icon) }]) }, [renderSlot(_ctx.$slots, "item-icon", { page }, () => [unref(isPluginIconFormat)(page.icon) ? (openBlock(), createBlock(PluginIcon_default, {
2956
- key: 0,
2957
- class: "mint-page-selector__metadata-icon",
2958
- icon: page.icon,
2959
- size: "sm",
2960
- variant: "solid"
2961
- }, null, 8, ["icon"])) : (openBlock(), createElementBlock("span", _hoisted_6$42, toDisplayString(page.label.charAt(0)), 1))])], 2),
2962
- createElementVNode("span", _hoisted_7$38, toDisplayString(page.label), 1),
2963
- _cache[2] || (_cache[2] = createElementVNode("svg", {
2964
- class: "mint-menu-item__check",
2965
- viewBox: "0 0 16 16",
2966
- fill: "none",
2967
- stroke: "currentColor",
2968
- "stroke-width": "2",
2969
- "stroke-linecap": "round",
2970
- "stroke-linejoin": "round",
2971
- "aria-hidden": "true"
2972
- }, [createElementVNode("path", { d: "M3.5 8.5l3 3 6-7" })], -1))
2973
- ]),
2954
+ default: withCtx(() => [createElementVNode("span", { class: normalizeClass(["mint-page-selector__item-icon", { "mint-page-selector__item-icon--metadata": unref(isPluginIconFormat)(page.icon) }]) }, [renderSlot(_ctx.$slots, "item-icon", { page }, () => [unref(isPluginIconFormat)(page.icon) ? (openBlock(), createBlock(PluginIcon_default, {
2955
+ key: 0,
2956
+ class: "mint-page-selector__metadata-icon",
2957
+ icon: page.icon,
2958
+ size: "sm",
2959
+ variant: "solid"
2960
+ }, null, 8, ["icon"])) : (openBlock(), createElementBlock("span", _hoisted_6$42, toDisplayString(page.label.charAt(0)), 1))])], 2), createElementVNode("span", _hoisted_7$38, toDisplayString(page.label), 1)]),
2974
2961
  _: 2
2975
2962
  }, 1032, [
2976
2963
  "href",
@@ -10727,6 +10714,13 @@ function appendQcGroup(groups, route, qcColor, qcCount = 3) {
10727
10714
  function sampleTotalFor(route, realCount = 9, qcCount = 3) {
10728
10715
  return route === "Exclude" ? realCount : realCount + qcCount;
10729
10716
  }
10717
+ /**
10718
+ * True when half or more of the groups have one sample: the design has no
10719
+ * replicate in the names, or a field that is on is not part of the condition.
10720
+ */
10721
+ function mostlySingletons(groups) {
10722
+ return groups.length > 1 && groups.filter((group) => group.count === 1).length * 2 >= groups.length;
10723
+ }
10730
10724
  /** Decide which inline summary callout the Field Recipe should show. */
10731
10725
  function fieldRecipeSummary(input) {
10732
10726
  const groupCount = input.baseGroups.length;
@@ -10738,6 +10732,10 @@ function fieldRecipeSummary(input) {
10738
10732
  kind: "warn",
10739
10733
  groupCount
10740
10734
  };
10735
+ if (mostlySingletons(input.baseGroups)) return {
10736
+ kind: "singletons",
10737
+ groupCount
10738
+ };
10741
10739
  return {
10742
10740
  kind: "info",
10743
10741
  groupCount
@@ -10745,97 +10743,110 @@ function fieldRecipeSummary(input) {
10745
10743
  }
10746
10744
  //#endregion
10747
10745
  //#region src/components/SmartGroupFieldRecipe.vue?vue&type=script&setup=true&lang.ts
10748
- var _hoisted_1$42 = { class: "sg-modal fr" };
10749
- var _hoisted_2$38 = { class: "sg-head" };
10750
- var _hoisted_3$31 = { class: "sg-head__sub" };
10751
- var _hoisted_4$30 = {
10752
- ref: "csvMenuRoot",
10753
- class: "fr-csv"
10754
- };
10755
- var _hoisted_5$28 = ["aria-expanded"];
10756
- var _hoisted_6$26 = {
10757
- key: 0,
10758
- class: "fr-csv-menu",
10759
- role: "menu"
10760
- };
10761
- var _hoisted_7$24 = [
10762
- "disabled",
10763
- "aria-disabled",
10764
- "title"
10765
- ];
10766
- var _hoisted_8$22 = [
10767
- "disabled",
10768
- "aria-disabled",
10769
- "title"
10746
+ var _hoisted_1$42 = { class: "sg-head" };
10747
+ var _hoisted_2$38 = { class: "sg-head__sub" };
10748
+ var _hoisted_3$31 = [
10749
+ "aria-expanded",
10750
+ "onClick",
10751
+ "onKeydown"
10770
10752
  ];
10771
- var _hoisted_9$20 = {
10753
+ var _hoisted_4$30 = {
10772
10754
  class: "sg-mode",
10773
10755
  role: "tablist",
10774
10756
  "aria-label": "Grouping mode"
10775
10757
  };
10776
- var _hoisted_10$19 = { class: "sg-body" };
10777
- var _hoisted_11$17 = {
10758
+ var _hoisted_5$28 = { class: "sg-body" };
10759
+ var _hoisted_6$26 = {
10778
10760
  key: 0,
10779
10761
  class: "fr-dropzone"
10780
10762
  };
10781
- var _hoisted_12$16 = {
10763
+ var _hoisted_7$24 = {
10782
10764
  key: 1,
10783
10765
  class: "fr-loading"
10784
10766
  };
10767
+ var _hoisted_8$22 = {
10768
+ key: 2,
10769
+ class: "fr-blank",
10770
+ "data-test": "recipe-empty"
10771
+ };
10772
+ var _hoisted_9$20 = {
10773
+ key: 0,
10774
+ class: "fr-anatomy"
10775
+ };
10776
+ var _hoisted_10$19 = { class: "fr-anatomy__tokens" };
10777
+ var _hoisted_11$17 = { class: "fr-token__text" };
10778
+ var _hoisted_12$16 = { class: "fr-token__caption" };
10785
10779
  var _hoisted_13$16 = { class: "sg-recipe" };
10786
- var _hoisted_14$16 = { class: "fr-field__head" };
10787
- var _hoisted_15$16 = { class: "sg-field__name" };
10788
- var _hoisted_16$13 = [
10789
- "aria-checked",
10790
- "aria-label",
10791
- "onClick"
10792
- ];
10793
- var _hoisted_17$13 = {
10780
+ var _hoisted_14$16 = {
10794
10781
  key: 0,
10795
- class: "sg-field__order"
10782
+ class: "fr-empty"
10796
10783
  };
10797
- var _hoisted_18$13 = { class: "sg-field__layer" };
10798
- var _hoisted_19$13 = [
10799
- "disabled",
10800
- "aria-label",
10801
- "onClick"
10784
+ var _hoisted_15$16 = [
10785
+ "data-field",
10786
+ "draggable",
10787
+ "onDragstart",
10788
+ "onDragover",
10789
+ "onDrop"
10802
10790
  ];
10803
- var _hoisted_20$12 = [
10804
- "disabled",
10791
+ var _hoisted_16$13 = ["aria-label", "onKeydown"];
10792
+ var _hoisted_17$13 = { class: "fr-row__layer" };
10793
+ var _hoisted_18$13 = [
10805
10794
  "aria-label",
10806
- "onClick"
10795
+ "aria-invalid",
10796
+ "onKeydown",
10797
+ "onBlur"
10807
10798
  ];
10808
- var _hoisted_21$9 = { class: "sg-field__vals" };
10809
- var _hoisted_22$9 = {
10799
+ var _hoisted_19$13 = {
10800
+ key: 1,
10801
+ class: "fr-row__error",
10802
+ role: "alert"
10803
+ };
10804
+ var _hoisted_20$12 = { class: "sg-field__name" };
10805
+ var _hoisted_21$9 = ["aria-label", "onClick"];
10806
+ var _hoisted_22$9 = { class: "sg-field__vals" };
10807
+ var _hoisted_23$7 = {
10808
+ key: 3,
10809
+ class: "fr-row__hint"
10810
+ };
10811
+ var _hoisted_24$7 = { class: "sg-recipe" };
10812
+ var _hoisted_25$7 = ["data-field"];
10813
+ var _hoisted_26$5 = { class: "sg-field__name" };
10814
+ var _hoisted_27$5 = { class: "sg-field__vals" };
10815
+ var _hoisted_28$4 = {
10810
10816
  key: 2,
10811
10817
  class: "sg-summary fr-summary--mute"
10812
10818
  };
10813
- var _hoisted_23$7 = { class: "sg-summary__icon" };
10814
- var _hoisted_24$7 = {
10819
+ var _hoisted_29$4 = { class: "sg-summary__icon" };
10820
+ var _hoisted_30$3 = {
10815
10821
  key: 3,
10816
10822
  class: "sg-summary sg-summary--warn"
10817
10823
  };
10818
- var _hoisted_25$7 = { class: "sg-summary__icon" };
10819
- var _hoisted_26$5 = {
10824
+ var _hoisted_31$3 = { class: "sg-summary__icon" };
10825
+ var _hoisted_32$3 = {
10820
10826
  key: 4,
10827
+ class: "sg-summary sg-summary--warn"
10828
+ };
10829
+ var _hoisted_33$3 = { class: "sg-summary__icon" };
10830
+ var _hoisted_34$2 = {
10831
+ key: 5,
10821
10832
  class: "sg-summary"
10822
10833
  };
10823
- var _hoisted_27$5 = { class: "sg-summary__icon" };
10824
- var _hoisted_28$4 = { class: "sg-body__side" };
10825
- var _hoisted_29$4 = { class: "sg-preview__head" };
10826
- var _hoisted_30$3 = { class: "sg-preview__count" };
10827
- var _hoisted_31$3 = { class: "sg-grouplist" };
10828
- var _hoisted_32$3 = {
10834
+ var _hoisted_35$2 = { class: "sg-summary__icon" };
10835
+ var _hoisted_36$2 = { class: "sg-body__side" };
10836
+ var _hoisted_37$2 = { class: "sg-preview__head" };
10837
+ var _hoisted_38$2 = { class: "sg-preview__count" };
10838
+ var _hoisted_39$2 = { class: "sg-grouplist" };
10839
+ var _hoisted_40$2 = {
10829
10840
  key: 0,
10830
10841
  class: "fr-empty"
10831
10842
  };
10832
- var _hoisted_33$3 = { class: "sg-group__name" };
10833
- var _hoisted_34$2 = { class: "sg-group__count" };
10834
- var _hoisted_35$2 = { class: "sg-qc" };
10835
- var _hoisted_36$2 = { class: "sg-qc__head" };
10836
- var _hoisted_37$2 = ["value"];
10837
- var _hoisted_38$2 = { class: "sg-foot" };
10838
- var _hoisted_39$2 = { class: "sg-foot__hint" };
10843
+ var _hoisted_41$2 = { class: "sg-group__name" };
10844
+ var _hoisted_42$1 = { class: "sg-group__count" };
10845
+ var _hoisted_43$1 = { class: "sg-qc" };
10846
+ var _hoisted_44$1 = { class: "sg-qc__head" };
10847
+ var _hoisted_45$1 = { class: "fr-qc__chips" };
10848
+ var _hoisted_46$1 = { class: "sg-foot" };
10849
+ var _hoisted_47$1 = { class: "sg-foot__hint" };
10839
10850
  var DEFAULT_SAMPLES = [
10840
10851
  {
10841
10852
  Condition: "Control",
@@ -10904,7 +10915,15 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10904
10915
  loading: {
10905
10916
  type: Boolean,
10906
10917
  default: false
10907
- }
10918
+ },
10919
+ anatomy: {},
10920
+ hints: {},
10921
+ qcChips: {},
10922
+ renamable: {
10923
+ type: Boolean,
10924
+ default: false
10925
+ },
10926
+ reservedNames: {}
10908
10927
  },
10909
10928
  emits: [
10910
10929
  "apply",
@@ -10913,6 +10932,7 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10913
10932
  "close",
10914
10933
  "toggle",
10915
10934
  "move",
10935
+ "rename",
10916
10936
  "update:route",
10917
10937
  "csv-file",
10918
10938
  "download-template",
@@ -10939,9 +10959,9 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10939
10959
  check: "M20 6L9 17l-5-5",
10940
10960
  close: "M6 18L18 6M6 6l12 12",
10941
10961
  upload: "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M17 8l-5-5-5 5M12 3v12",
10942
- download: "M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3",
10943
10962
  chev: "M6 9l6 6 6-6",
10944
- chevUp: "M18 15l-6-6-6 6"
10963
+ grip: "M9 6h.01M9 12h.01M9 18h.01M15 6h.01M15 12h.01M15 18h.01",
10964
+ pencil: "M12 20h9M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"
10945
10965
  };
10946
10966
  const MAX_BAR = 44;
10947
10967
  /** QC reads the same everywhere: neutral solid ring, never a group color (3a). */
@@ -10961,7 +10981,8 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10961
10981
  if (file) emit("csv-file", file);
10962
10982
  input.value = "";
10963
10983
  }
10964
- function onDragEnter() {
10984
+ function onDragEnter(event) {
10985
+ if (!event.dataTransfer?.types.includes("Files")) return;
10965
10986
  dragDepth += 1;
10966
10987
  dragOver.value = true;
10967
10988
  }
@@ -10975,25 +10996,43 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
10975
10996
  const file = event.dataTransfer?.files?.[0];
10976
10997
  if (file && isCsvLike(file)) emit("csv-file", file);
10977
10998
  }
10978
- const { isOpen: csvMenuOpen, close: closeCsvMenu, toggle: toggleCsvMenu } = useDropdownState({
10979
- rootRef: useTemplateRef("csvMenuRoot"),
10980
- triggerRef: useTemplateRef("csvMenuTrigger")
10981
- });
10982
- function chooseUpload() {
10983
- closeCsvMenu();
10984
- pickCsv();
10985
- }
10986
- function chooseDownloadTemplate() {
10987
- if (!props.canDownloadTemplate) return;
10988
- closeCsvMenu();
10989
- emit("download-template");
10990
- }
10991
- function chooseDownloadPrefilled() {
10992
- if (!props.canDownloadTemplate) return;
10993
- closeCsvMenu();
10994
- emit("download-prefilled");
10999
+ const csvMenuItems = computed(() => [
11000
+ {
11001
+ id: "upload",
11002
+ label: "Upload CSV"
11003
+ },
11004
+ {
11005
+ id: "template",
11006
+ label: "Download template",
11007
+ disabled: !props.canDownloadTemplate
11008
+ },
11009
+ {
11010
+ id: "prefilled",
11011
+ label: "Download pre-filled",
11012
+ disabled: !props.canDownloadTemplate
11013
+ }
11014
+ ]);
11015
+ function onCsvMenuSelect(item) {
11016
+ if (item.id === "upload") pickCsv();
11017
+ else if (item.id === "template") emit("download-template");
11018
+ else emit("download-prefilled");
10995
11019
  }
10996
- const onInner = ref(props.initialOn ? [...props.initialOn] : props.fields.map((field) => !field.rep));
11020
+ const QC_ROUTE_OPTIONS = [
11021
+ {
11022
+ value: "Overlay only",
11023
+ label: "Overlay"
11024
+ },
11025
+ {
11026
+ value: "Exclude",
11027
+ label: "Exclude"
11028
+ },
11029
+ {
11030
+ value: "Mix into groups",
11031
+ label: "Mix in"
11032
+ }
11033
+ ];
11034
+ const startLayers = props.enabled === void 0 ? props.layers : void 0;
11035
+ const onInner = ref(props.initialOn ? [...props.initialOn] : startLayers?.map((layer) => layer !== null) ?? props.fields.map((field) => !field.rep));
10997
11036
  const orderInner = ref(onInner.value.flatMap((isOn, i) => isOn ? [i] : []));
10998
11037
  const routeInner = ref(props.initialRoute);
10999
11038
  const controlledEnabled = computed(() => props.enabled !== void 0);
@@ -11001,7 +11040,11 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11001
11040
  const on = computed(() => props.enabled ?? onInner.value);
11002
11041
  const currentRoute = computed(() => props.route ?? routeInner.value);
11003
11042
  const layers = computed(() => {
11004
- if (props.layers !== void 0) return props.layers;
11043
+ if (props.layers !== void 0 && controlledEnabled.value) return props.layers;
11044
+ if (props.layers !== void 0) {
11045
+ let last = Math.max(0, ...props.layers.map((layer) => layer ?? 0));
11046
+ return props.layers.map((layer, i) => on.value[i] ? layer ?? ++last : null);
11047
+ }
11005
11048
  if (controlledEnabled.value) {
11006
11049
  let layer = 0;
11007
11050
  return props.fields.map((_, i) => on.value[i] ? ++layer : null);
@@ -11015,19 +11058,22 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11015
11058
  layer,
11016
11059
  i
11017
11060
  }]).sort((a, b) => a.layer - b.layer).map((entry) => entry.i));
11061
+ const onCards = computed(() => layerOrder.value.map((index) => ({
11062
+ index,
11063
+ field: props.fields[index]
11064
+ })));
11065
+ const offCards = computed(() => props.fields.flatMap((field, index) => layers.value[index] === null ? [{
11066
+ index,
11067
+ field
11068
+ }] : []));
11018
11069
  const computedBase = computed(() => computeFieldGroups(props.samples, props.fields, on.value, props.palette, layerOrder.value));
11019
11070
  const baseGroups = computed(() => props.groups ?? computedBase.value);
11020
11071
  const groups = computed(() => props.groups ?? appendQcGroup(computedBase.value, currentRoute.value, props.qcColor));
11021
11072
  const activeFactors = computed(() => layerOrder.value.map((i) => props.fields[i].key));
11022
11073
  const counts = computed(() => baseGroups.value.map((group) => group.count));
11023
11074
  const uniform = computed(() => counts.value.length > 0 && counts.value.every((count) => count === counts.value[0]));
11024
- const repField = computed(() => props.fields.find((field) => field.rep));
11025
- const repOn = computed(() => props.fields.some((field, i) => field.rep && on.value[i]));
11026
- const repRange = computed(() => {
11027
- const parts = (repField.value?.vals ?? "").split(",").map((part) => part.trim()).filter(Boolean);
11028
- if (parts.length === 0) return "";
11029
- return parts.length > 1 ? `${parts[0]}–${parts[parts.length - 1]}` : parts[0];
11030
- });
11075
+ const repOnField = computed(() => props.fields.find((field, i) => field.rep && on.value[i]));
11076
+ const repOn = computed(() => repOnField.value !== void 0);
11031
11077
  const summary = computed(() => fieldRecipeSummary({
11032
11078
  activeFactors: activeFactors.value,
11033
11079
  repOn: repOn.value,
@@ -11038,12 +11084,23 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11038
11084
  const qcCount = computed(() => props.qcCount ?? Math.max(0, props.headerCount - props.samples.length));
11039
11085
  const realCount = computed(() => Math.max(0, props.headerCount - qcCount.value));
11040
11086
  const sampleTotal = computed(() => sampleTotalFor(currentRoute.value, realCount.value, qcCount.value));
11041
- const qcChip = computed(() => props.qcChip !== void 0 ? props.qcChip : currentRoute.value !== "Mix into groups" ? {
11042
- name: "QC_Pool",
11043
- count: qcCount.value
11044
- } : null);
11087
+ const qcChips = computed(() => {
11088
+ if (props.qcChips !== void 0) return props.qcChips;
11089
+ if (props.qcChip !== void 0) return props.qcChip ? [props.qcChip] : [];
11090
+ return currentRoute.value !== "Mix into groups" ? [{
11091
+ name: "QC_Pool",
11092
+ count: qcCount.value
11093
+ }] : [];
11094
+ });
11045
11095
  const footHint = computed(() => currentRoute.value === "Exclude" ? ` · ${qcCount.value} QC excluded` : currentRoute.value === "Mix into groups" ? " · QC mixed in" : ` · ${qcCount.value} QC overlaid`);
11096
+ const root = ref(null);
11097
+ const controlOf = (index, selector) => root.value?.querySelector(`[data-field="${index}"] ${selector}`) ?? null;
11098
+ async function refocus(index, selector) {
11099
+ await nextTick();
11100
+ controlOf(index, selector)?.focus();
11101
+ }
11046
11102
  function toggle(index) {
11103
+ refocus(index, "[role=\"switch\"]");
11047
11104
  if (controlledEnabled.value) {
11048
11105
  emit("toggle", index);
11049
11106
  return;
@@ -11053,7 +11110,7 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11053
11110
  orderInner.value = wasOn ? orderInner.value.filter((i) => i !== index) : [...orderInner.value, index];
11054
11111
  }
11055
11112
  function moveLayer(index, direction) {
11056
- if (controlledEnabled.value) {
11113
+ if (controlledEnabled.value || props.layers !== void 0) {
11057
11114
  emit("move", index, direction);
11058
11115
  return;
11059
11116
  }
@@ -11065,6 +11122,48 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11065
11122
  [next[from], next[to]] = [next[to], next[from]];
11066
11123
  orderInner.value = next;
11067
11124
  }
11125
+ const reorder = useListReorder({
11126
+ items: () => onCards.value,
11127
+ getId: (card) => card.field.key,
11128
+ getLabel: (card) => card.field.key,
11129
+ itemNoun: "layer",
11130
+ getElement: (id) => {
11131
+ const card = onCards.value.find((candidate) => candidate.field.key === id);
11132
+ return card ? controlOf(card.index, ".fr-row__grip") : null;
11133
+ },
11134
+ onReorder: ({ fromIndex, toIndex }) => {
11135
+ const { index } = onCards.value[fromIndex];
11136
+ const direction = toIndex < fromIndex ? "up" : "down";
11137
+ for (let step = 0; step < Math.abs(toIndex - fromIndex); step++) moveLayer(index, direction);
11138
+ }
11139
+ });
11140
+ const renaming = ref(null);
11141
+ const draftName = ref("");
11142
+ const renameTaken = ref(false);
11143
+ const renameInput = ref([]);
11144
+ async function startRename(card) {
11145
+ renaming.value = card.index;
11146
+ renameTaken.value = false;
11147
+ draftName.value = card.field.key;
11148
+ await nextTick();
11149
+ renameInput.value[0]?.select();
11150
+ }
11151
+ function endRename(card, restoreFocus) {
11152
+ renaming.value = null;
11153
+ renameTaken.value = false;
11154
+ if (restoreFocus) refocus(card.index, ".fr-row__rename");
11155
+ }
11156
+ function commitRename(card, fromBlur = false) {
11157
+ if (renaming.value !== card.index) return;
11158
+ const name = draftName.value.trim();
11159
+ const taken = name !== card.field.key && (props.fields.some((field) => field.key === name) || (props.reservedNames?.includes(name) ?? false));
11160
+ if (taken && !fromBlur) {
11161
+ renameTaken.value = true;
11162
+ return;
11163
+ }
11164
+ endRename(card, !fromBlur);
11165
+ if (name && name !== card.field.key && !taken) emit("rename", card.index, name);
11166
+ }
11068
11167
  function onRouteChange(value) {
11069
11168
  if (controlledRoute.value) {
11070
11169
  emit("update:route", value);
@@ -11082,79 +11181,53 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11082
11181
  });
11083
11182
  }
11084
11183
  return (_ctx, _cache) => {
11085
- return openBlock(), createElementBlock("div", _hoisted_1$42, [
11086
- createElementVNode("div", _hoisted_2$38, [
11184
+ return openBlock(), createElementBlock("div", {
11185
+ ref_key: "root",
11186
+ ref: root,
11187
+ class: "sg-modal fr"
11188
+ }, [
11189
+ createElementVNode("div", _hoisted_1$42, [
11087
11190
  createVNode(Icon, {
11088
11191
  d: ICON.bolt,
11089
11192
  size: 18
11090
11193
  }, null, 8, ["d"]),
11091
11194
  _cache[11] || (_cache[11] = createElementVNode("span", { class: "sg-head__title" }, "Group samples", -1)),
11092
- 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)]),
11195
+ 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)]),
11093
11196
  _cache[12] || (_cache[12] = createElementVNode("div", { class: "sg-head__spacer" }, null, -1)),
11094
- createElementVNode("div", _hoisted_4$30, [
11095
- createElementVNode("button", {
11096
- ref: "csvMenuTrigger",
11197
+ createVNode(ActionMenu_default, {
11198
+ label: "CSV",
11199
+ items: csvMenuItems.value,
11200
+ onSelect: onCsvMenuSelect
11201
+ }, {
11202
+ trigger: withCtx(({ open, toggle: toggleMenu, keydown }) => [createElementVNode("button", {
11097
11203
  type: "button",
11098
11204
  class: "fr-upload",
11099
11205
  "aria-haspopup": "menu",
11100
- "aria-expanded": unref(csvMenuOpen),
11101
- onClick: _cache[0] || (_cache[0] = (...args) => unref(toggleCsvMenu) && unref(toggleCsvMenu)(...args))
11206
+ "aria-expanded": open,
11207
+ onClick: toggleMenu,
11208
+ onKeydown: keydown
11102
11209
  }, [
11103
11210
  createVNode(Icon, {
11104
11211
  d: ICON.upload,
11105
11212
  size: 14
11106
11213
  }, null, 8, ["d"]),
11107
- _cache[6] || (_cache[6] = createTextVNode(" CSV ", -1)),
11214
+ _cache[9] || (_cache[9] = createTextVNode(" CSV ", -1)),
11108
11215
  createVNode(Icon, {
11109
11216
  d: ICON.chev,
11110
11217
  size: 13
11111
11218
  }, null, 8, ["d"])
11112
- ], 8, _hoisted_5$28),
11113
- unref(csvMenuOpen) ? (openBlock(), createElementBlock("div", _hoisted_6$26, [
11114
- createElementVNode("button", {
11115
- type: "button",
11116
- class: "fr-csv-item",
11117
- role: "menuitem",
11118
- onClick: chooseUpload
11119
- }, [createVNode(Icon, {
11120
- d: ICON.upload,
11121
- size: 14
11122
- }, null, 8, ["d"]), _cache[7] || (_cache[7] = createTextVNode(" Upload CSV ", -1))]),
11123
- createElementVNode("button", {
11124
- type: "button",
11125
- class: "fr-csv-item",
11126
- role: "menuitem",
11127
- disabled: !__props.canDownloadTemplate,
11128
- "aria-disabled": !__props.canDownloadTemplate,
11129
- title: __props.canDownloadTemplate ? void 0 : "Load samples first",
11130
- onClick: chooseDownloadTemplate
11131
- }, [createVNode(Icon, {
11132
- d: ICON.download,
11133
- size: 14
11134
- }, null, 8, ["d"]), _cache[8] || (_cache[8] = createTextVNode(" Download template ", -1))], 8, _hoisted_7$24),
11135
- createElementVNode("button", {
11136
- type: "button",
11137
- class: "fr-csv-item",
11138
- role: "menuitem",
11139
- disabled: !__props.canDownloadTemplate,
11140
- "aria-disabled": !__props.canDownloadTemplate,
11141
- title: __props.canDownloadTemplate ? void 0 : "Load samples first",
11142
- onClick: chooseDownloadPrefilled
11143
- }, [createVNode(Icon, {
11144
- d: ICON.download,
11145
- size: 14
11146
- }, null, 8, ["d"]), _cache[9] || (_cache[9] = createTextVNode(" Download pre-filled ", -1))], 8, _hoisted_8$22)
11147
- ])) : createCommentVNode("", true),
11148
- createElementVNode("input", {
11149
- ref_key: "fileInput",
11150
- ref: fileInput,
11151
- type: "file",
11152
- accept: ".csv,.tsv,text/csv,text/tab-separated-values",
11153
- hidden: "",
11154
- onChange: onFileChange
11155
- }, null, 544)
11156
- ], 512),
11157
- createElementVNode("div", _hoisted_9$20, [_cache[10] || (_cache[10] = createElementVNode("button", {
11219
+ ], 40, _hoisted_3$31)]),
11220
+ _: 1
11221
+ }, 8, ["items"]),
11222
+ createElementVNode("input", {
11223
+ ref_key: "fileInput",
11224
+ ref: fileInput,
11225
+ type: "file",
11226
+ accept: ".csv,.tsv,text/csv,text/tab-separated-values",
11227
+ hidden: "",
11228
+ onChange: onFileChange
11229
+ }, null, 544),
11230
+ createElementVNode("div", _hoisted_4$30, [_cache[10] || (_cache[10] = createElementVNode("button", {
11158
11231
  type: "button",
11159
11232
  class: "sg-mode__btn sg-mode__btn--on",
11160
11233
  role: "tab",
@@ -11164,104 +11237,166 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11164
11237
  class: "sg-mode__btn",
11165
11238
  role: "tab",
11166
11239
  "aria-selected": false,
11167
- onClick: _cache[1] || (_cache[1] = ($event) => emit("manual"))
11240
+ onClick: _cache[0] || (_cache[0] = ($event) => emit("manual"))
11168
11241
  }, "Manual")]),
11169
11242
  createElementVNode("button", {
11170
11243
  class: "sg-head__close",
11171
11244
  "aria-label": "Close",
11172
- onClick: _cache[2] || (_cache[2] = ($event) => emit("close"))
11245
+ onClick: _cache[1] || (_cache[1] = ($event) => emit("close"))
11173
11246
  }, [createVNode(Icon, {
11174
11247
  d: ICON.close,
11175
11248
  size: 16
11176
11249
  }, null, 8, ["d"])])
11177
11250
  ]),
11178
- createElementVNode("div", _hoisted_10$19, [createElementVNode("div", {
11251
+ createElementVNode("div", _hoisted_5$28, [createElementVNode("div", {
11179
11252
  class: "sg-body__main",
11180
11253
  onDragenter: withModifiers(onDragEnter, ["prevent"]),
11181
- onDragover: _cache[3] || (_cache[3] = withModifiers(() => {}, ["prevent"])),
11254
+ onDragover: _cache[6] || (_cache[6] = withModifiers(() => {}, ["prevent"])),
11182
11255
  onDragleave: withModifiers(onDragLeave, ["prevent"]),
11183
11256
  onDrop: withModifiers(onDrop, ["prevent"])
11184
11257
  }, [
11185
- dragOver.value ? (openBlock(), createElementBlock("div", _hoisted_11$17, [createVNode(Icon, {
11258
+ dragOver.value ? (openBlock(), createElementBlock("div", _hoisted_6$26, [createVNode(Icon, {
11186
11259
  d: ICON.upload,
11187
11260
  size: 22
11188
11261
  }, null, 8, ["d"]), _cache[13] || (_cache[13] = createElementVNode("span", null, "Drop CSV to load samples", -1))])) : createCommentVNode("", true),
11189
- __props.loading ? (openBlock(), createElementBlock("div", _hoisted_12$16, [createVNode(LoadingSpinner_default, {
11262
+ __props.loading ? (openBlock(), createElementBlock("div", _hoisted_7$24, [createVNode(LoadingSpinner_default, {
11190
11263
  size: "md",
11191
11264
  label: "Loading experiment metadata"
11192
11265
  }), _cache[14] || (_cache[14] = createElementVNode("span", null, "Loading experiment metadata…", -1))])) : createCommentVNode("", true),
11193
- _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)),
11194
- createElementVNode("div", _hoisted_13$16, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.fields, (field, i) => {
11195
- return openBlock(), createElementBlock("div", {
11196
- key: field.key,
11197
- class: normalizeClass(["sg-field", {
11198
- "sg-field--on": on.value[i],
11199
- "fr-field--warn": field.rep && on.value[i]
11200
- }])
11201
- }, [
11202
- 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)]),
11203
- createElementVNode("button", {
11204
- class: normalizeClass(["sg-switch fr-switch", { on: on.value[i] }]),
11205
- role: "switch",
11206
- "aria-checked": on.value[i],
11207
- "aria-label": "Group by " + field.key,
11208
- onClick: ($event) => toggle(i)
11209
- }, null, 10, _hoisted_16$13),
11210
- on.value[i] && layers.value[i] !== null ? (openBlock(), createElementBlock("div", _hoisted_17$13, [
11211
- createElementVNode("span", _hoisted_18$13, "Layer " + toDisplayString(layers.value[i]), 1),
11266
+ __props.fields.length === 0 && __props.headerCount === 0 && !__props.loading ? (openBlock(), createElementBlock("div", _hoisted_8$22, [
11267
+ _cache[16] || (_cache[16] = createElementVNode("span", { class: "sg-lead__q" }, "No samples to group", -1)),
11268
+ _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)),
11269
+ createVNode(BaseButton_default, {
11270
+ variant: "secondary",
11271
+ size: "sm",
11272
+ onClick: pickCsv
11273
+ }, {
11274
+ "icon-left": withCtx(() => [createVNode(Icon, {
11275
+ d: ICON.upload,
11276
+ size: 15
11277
+ }, null, 8, ["d"])]),
11278
+ default: withCtx(() => [_cache[15] || (_cache[15] = createTextVNode(" Upload CSV ", -1))]),
11279
+ _: 1
11280
+ })
11281
+ ])) : (openBlock(), createElementBlock(Fragment, { key: 3 }, [
11282
+ __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) => {
11283
+ return openBlock(), createElementBlock("span", {
11284
+ key: t,
11285
+ class: normalizeClass(["fr-token", "fr-token--" + token.state])
11286
+ }, [createElementVNode("span", _hoisted_11$17, toDisplayString(token.text), 1), createElementVNode("span", _hoisted_12$16, toDisplayString(token.caption), 1)], 2);
11287
+ }), 128))])])) : createCommentVNode("", true),
11288
+ _cache[28] || (_cache[28] = createElementVNode("span", { class: "fr-label" }, "Group by", -1)),
11289
+ 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) => {
11290
+ return openBlock(), createElementBlock("div", {
11291
+ key: card.field.key,
11292
+ class: normalizeClass(["sg-field sg-field--on fr-row", {
11293
+ "fr-field--warn": card.field.rep,
11294
+ "fr-row--dragging": unref(reorder).isDragging(card.field.key),
11295
+ "fr-row--over": unref(reorder).isDragOver(card.field.key)
11296
+ }]),
11297
+ "data-test": "field-on",
11298
+ "data-field": card.index,
11299
+ draggable: renaming.value !== card.index,
11300
+ onDragstart: ($event) => unref(reorder).onDragStart($event, card, position),
11301
+ onDragover: ($event) => unref(reorder).onDragOver($event, card, position),
11302
+ onDragleave: _cache[4] || (_cache[4] = (...args) => unref(reorder).onDragLeave && unref(reorder).onDragLeave(...args)),
11303
+ onDrop: ($event) => unref(reorder).onDrop($event, card, position),
11304
+ onDragend: _cache[5] || (_cache[5] = (...args) => unref(reorder).onDragEnd && unref(reorder).onDragEnd(...args))
11305
+ }, [
11212
11306
  createElementVNode("button", {
11213
11307
  type: "button",
11214
- class: "sg-field__order-btn",
11215
- disabled: layers.value[i] === 1,
11216
- "aria-label": "Move " + field.key + " up a layer",
11217
- title: "Move up a layer",
11218
- onClick: withModifiers(($event) => moveLayer(i, "up"), ["stop"])
11308
+ class: "fr-row__grip",
11309
+ "aria-label": `Move ${card.field.key}, layer ${position + 1} of ${onCards.value.length}. Press Alt and an arrow key.`,
11310
+ onKeydown: ($event) => unref(reorder).onKeydown($event, card, position)
11219
11311
  }, [createVNode(Icon, {
11220
- d: ICON.chevUp,
11221
- size: 12
11222
- }, null, 8, ["d"])], 8, _hoisted_19$13),
11223
- createElementVNode("button", {
11312
+ d: ICON.grip,
11313
+ size: 14
11314
+ }, null, 8, ["d"])], 40, _hoisted_16$13),
11315
+ createElementVNode("span", _hoisted_17$13, toDisplayString(position + 1), 1),
11316
+ renaming.value === card.index ? withDirectives((openBlock(), createElementBlock("input", {
11317
+ key: 0,
11318
+ ref_for: true,
11319
+ ref_key: "renameInput",
11320
+ ref: renameInput,
11321
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => draftName.value = $event),
11322
+ class: "fr-row__name-input",
11323
+ "aria-label": "New name for " + card.field.key,
11324
+ "aria-invalid": renameTaken.value || void 0,
11325
+ onInput: _cache[3] || (_cache[3] = ($event) => renameTaken.value = false),
11326
+ onKeydown: [withKeys(withModifiers(($event) => commitRename(card), ["prevent"]), ["enter"]), withKeys(withModifiers(($event) => endRename(card, true), ["stop"]), ["esc"])],
11327
+ onBlur: ($event) => commitRename(card, true)
11328
+ }, null, 40, _hoisted_18$13)), [[vModelText, draftName.value]]) : createCommentVNode("", true),
11329
+ 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", {
11330
+ key: 0,
11224
11331
  type: "button",
11225
- class: "sg-field__order-btn",
11226
- disabled: layers.value[i] === layerOrder.value.length,
11227
- "aria-label": "Move " + field.key + " down a layer",
11228
- title: "Move down a layer",
11229
- onClick: withModifiers(($event) => moveLayer(i, "down"), ["stop"])
11332
+ class: "fr-row__rename",
11333
+ "aria-label": "Rename " + card.field.key,
11334
+ onClick: ($event) => startRename(card)
11230
11335
  }, [createVNode(Icon, {
11231
- d: ICON.chev,
11336
+ d: ICON.pencil,
11232
11337
  size: 12
11233
- }, null, 8, ["d"])], 8, _hoisted_20$12)
11234
- ])) : createCommentVNode("", true),
11235
- createElementVNode("div", _hoisted_21$9, toDisplayString(field.vals), 1)
11236
- ], 2);
11237
- }), 128))]),
11238
- summary.value.kind === "mute" ? (openBlock(), createElementBlock("div", _hoisted_22$9, [createElementVNode("span", _hoisted_23$7, [createVNode(Icon, {
11239
- d: ICON.info,
11240
- size: 16
11241
- }, 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, {
11242
- d: ICON.warn,
11243
- size: 16
11244
- }, null, 8, ["d"])]), createElementVNode("span", null, [
11245
- _cache[16] || (_cache[16] = createTextVNode(" Including ", -1)),
11246
- createElementVNode("b", null, toDisplayString(repField.value?.key ?? "Replicate"), 1),
11247
- _cache[17] || (_cache[17] = createTextVNode(" makes every sample unique — ", -1)),
11248
- createElementVNode("b", null, toDisplayString(baseGroups.value.length) + " groups of 1", 1),
11249
- _cache[18] || (_cache[18] = createTextVNode(". Turn it off to keep ", -1)),
11250
- createElementVNode("code", null, toDisplayString(repRange.value), 1),
11251
- _cache[19] || (_cache[19] = createTextVNode(" together. ", -1))
11252
- ])])) : (openBlock(), createElementBlock("div", _hoisted_26$5, [createElementVNode("span", _hoisted_27$5, [createVNode(Icon, {
11253
- d: ICON.info,
11254
- size: 16
11255
- }, null, 8, ["d"])]), createElementVNode("span", null, [
11256
- _cache[20] || (_cache[20] = createTextVNode(" Grouping by ", -1)),
11257
- createElementVNode("b", null, toDisplayString(activeFactors.value.join(" × ")), 1),
11258
- _cache[21] || (_cache[21] = createTextVNode(" → ", -1)),
11259
- 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)]),
11260
- _cache[22] || (_cache[22] = createTextVNode(". Replicates kept together. ", -1))
11261
- ])]))
11262
- ], 32), createElementVNode("div", _hoisted_28$4, [
11263
- 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)]),
11264
- 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) => {
11338
+ }, null, 8, ["d"])], 8, _hoisted_21$9)) : createCommentVNode("", true)], 64)),
11339
+ createElementVNode("span", _hoisted_22$9, toDisplayString(card.field.vals), 1),
11340
+ __props.hints?.[card.index] ? (openBlock(), createElementBlock("span", _hoisted_23$7, toDisplayString(__props.hints[card.index]), 1)) : createCommentVNode("", true),
11341
+ createVNode(BaseToggle_default, {
11342
+ size: "sm",
11343
+ "model-value": true,
11344
+ "aria-label": "Group by " + card.field.key,
11345
+ "onUpdate:modelValue": ($event) => toggle(card.index)
11346
+ }, null, 8, ["aria-label", "onUpdate:modelValue"])
11347
+ ], 42, _hoisted_15$16);
11348
+ }), 128))]),
11349
+ createElementVNode("span", {
11350
+ style: normalizeStyle(unref(reorder).announcerStyle),
11351
+ "aria-live": "polite"
11352
+ }, toDisplayString(unref(reorder).announcement.value), 5),
11353
+ 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) => {
11354
+ return openBlock(), createElementBlock("div", {
11355
+ key: card.field.key,
11356
+ class: "sg-field fr-row",
11357
+ "data-test": "field-off",
11358
+ "data-field": card.index
11359
+ }, [
11360
+ createElementVNode("span", _hoisted_26$5, toDisplayString(card.field.key), 1),
11361
+ createElementVNode("span", { class: normalizeClass(["sg-field__role", { "sg-field__role--rep": card.field.rep }]) }, toDisplayString(card.field.role), 3),
11362
+ createElementVNode("span", _hoisted_27$5, toDisplayString(card.field.vals), 1),
11363
+ createVNode(BaseToggle_default, {
11364
+ size: "sm",
11365
+ "model-value": false,
11366
+ "aria-label": "Group by " + card.field.key,
11367
+ "onUpdate:modelValue": ($event) => toggle(card.index)
11368
+ }, null, 8, ["aria-label", "onUpdate:modelValue"])
11369
+ ], 8, _hoisted_25$7);
11370
+ }), 128))])], 64)) : createCommentVNode("", true),
11371
+ summary.value.kind === "mute" ? (openBlock(), createElementBlock("div", _hoisted_28$4, [createElementVNode("span", _hoisted_29$4, [createVNode(Icon, {
11372
+ d: ICON.info,
11373
+ size: 16
11374
+ }, 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, {
11375
+ d: ICON.warn,
11376
+ size: 16
11377
+ }, null, 8, ["d"])]), createElementVNode("span", null, [
11378
+ _cache[21] || (_cache[21] = createTextVNode(" Including ", -1)),
11379
+ createElementVNode("b", null, toDisplayString(repOnField.value?.key), 1),
11380
+ _cache[22] || (_cache[22] = createTextVNode(" makes every sample unique — ", -1)),
11381
+ createElementVNode("b", null, toDisplayString(baseGroups.value.length) + " groups of 1", 1),
11382
+ _cache[23] || (_cache[23] = createTextVNode(". Switch it off to keep replicates together. ", -1))
11383
+ ])])) : summary.value.kind === "singletons" ? (openBlock(), createElementBlock("div", _hoisted_32$3, [createElementVNode("span", _hoisted_33$3, [createVNode(Icon, {
11384
+ d: ICON.warn,
11385
+ size: 16
11386
+ }, 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, {
11387
+ d: ICON.info,
11388
+ size: 16
11389
+ }, null, 8, ["d"])]), createElementVNode("span", null, [
11390
+ _cache[25] || (_cache[25] = createTextVNode(" Grouping by ", -1)),
11391
+ createElementVNode("b", null, toDisplayString(activeFactors.value.join(" × ")), 1),
11392
+ _cache[26] || (_cache[26] = createTextVNode(" → ", -1)),
11393
+ 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)]),
11394
+ _cache[27] || (_cache[27] = createTextVNode(". ", -1))
11395
+ ])]))
11396
+ ], 64))
11397
+ ], 32), createElementVNode("div", _hoisted_36$2, [
11398
+ 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)]),
11399
+ 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) => {
11265
11400
  return openBlock(), createElementBlock("div", {
11266
11401
  key: group.name,
11267
11402
  class: "sg-group"
@@ -11270,7 +11405,7 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11270
11405
  class: "sg-dot",
11271
11406
  style: normalizeStyle(group.name === unref("QC_Pool") ? unref(QC_DOT_STYLE) : { background: group.color })
11272
11407
  }, null, 4),
11273
- createElementVNode("span", _hoisted_33$3, toDisplayString(group.name), 1),
11408
+ createElementVNode("span", _hoisted_41$2, toDisplayString(group.name), 1),
11274
11409
  createElementVNode("span", {
11275
11410
  class: "sg-group__bar",
11276
11411
  style: normalizeStyle({
@@ -11278,34 +11413,36 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11278
11413
  "--g": group.color
11279
11414
  })
11280
11415
  }, null, 4),
11281
- createElementVNode("span", _hoisted_34$2, toDisplayString(group.count), 1)
11416
+ createElementVNode("span", _hoisted_42$1, toDisplayString(group.count), 1)
11282
11417
  ]);
11283
11418
  }), 128))]),
11284
- 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", {
11285
- class: "sg-qc__route",
11286
- value: currentRoute.value,
11287
- onChange: _cache[4] || (_cache[4] = ($event) => onRouteChange($event.target.value))
11288
- }, [..._cache[25] || (_cache[25] = [
11289
- createElementVNode("option", null, "Overlay only", -1),
11290
- createElementVNode("option", null, "Exclude", -1),
11291
- createElementVNode("option", null, "Mix into groups", -1)
11292
- ])], 40, _hoisted_37$2)]), qcChip.value ? (openBlock(), createElementBlock("span", {
11293
- key: 0,
11294
- class: normalizeClass(["sg-qc__chip", { "fr-qc__chip--off": currentRoute.value === "Exclude" }])
11295
- }, [createElementVNode("span", {
11296
- class: "sg-dot",
11297
- style: normalizeStyle(unref(QC_DOT_STYLE))
11298
- }, null, 4), createTextVNode(toDisplayString(qcChip.value.name) + " ×" + toDisplayString(qcChip.value.count), 1)], 2)) : createCommentVNode("", true)])
11419
+ 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, {
11420
+ class: "fr-qc__route",
11421
+ size: "sm",
11422
+ variant: "card",
11423
+ "aria-label": "Quality control routing",
11424
+ "model-value": currentRoute.value,
11425
+ options: QC_ROUTE_OPTIONS,
11426
+ "onUpdate:modelValue": _cache[7] || (_cache[7] = ($event) => onRouteChange($event))
11427
+ }, null, 8, ["model-value"])]), createElementVNode("div", _hoisted_45$1, [(openBlock(true), createElementBlock(Fragment, null, renderList(qcChips.value, (chip) => {
11428
+ return openBlock(), createElementBlock("span", {
11429
+ key: chip.name,
11430
+ class: normalizeClass(["sg-qc__chip", { "fr-qc__chip--off": currentRoute.value === "Exclude" }])
11431
+ }, [createElementVNode("span", {
11432
+ class: "sg-dot",
11433
+ style: normalizeStyle(unref(QC_DOT_STYLE))
11434
+ }, null, 4), createTextVNode(toDisplayString(chip.name) + " ×" + toDisplayString(chip.count), 1)], 2);
11435
+ }), 128))])])
11299
11436
  ])]),
11300
- createElementVNode("div", _hoisted_38$2, [
11301
- createElementVNode("span", _hoisted_39$2, "Applies to " + toDisplayString(sampleTotal.value) + " samples" + toDisplayString(footHint.value), 1),
11302
- _cache[29] || (_cache[29] = createElementVNode("div", { class: "sg-foot__spacer" }, null, -1)),
11437
+ createElementVNode("div", _hoisted_46$1, [
11438
+ createElementVNode("span", _hoisted_47$1, "Applies to " + toDisplayString(sampleTotal.value) + " samples" + toDisplayString(footHint.value), 1),
11439
+ _cache[33] || (_cache[33] = createElementVNode("div", { class: "sg-foot__spacer" }, null, -1)),
11303
11440
  createVNode(BaseButton_default, {
11304
11441
  variant: "ghost",
11305
11442
  size: "sm",
11306
- onClick: _cache[5] || (_cache[5] = ($event) => emit("cancel"))
11443
+ onClick: _cache[8] || (_cache[8] = ($event) => emit("cancel"))
11307
11444
  }, {
11308
- default: withCtx(() => [..._cache[27] || (_cache[27] = [createTextVNode("Cancel", -1)])]),
11445
+ default: withCtx(() => [..._cache[31] || (_cache[31] = [createTextVNode("Cancel", -1)])]),
11309
11446
  _: 1
11310
11447
  }),
11311
11448
  createVNode(BaseButton_default, {
@@ -11318,11 +11455,11 @@ var SmartGroupFieldRecipe_default = /* @__PURE__ */ defineComponent({
11318
11455
  d: ICON.check,
11319
11456
  size: 15
11320
11457
  }, null, 8, ["d"])]),
11321
- default: withCtx(() => [_cache[28] || (_cache[28] = createTextVNode(" Apply grouping ", -1))]),
11458
+ default: withCtx(() => [_cache[32] || (_cache[32] = createTextVNode(" Apply grouping ", -1))]),
11322
11459
  _: 1
11323
11460
  }, 8, ["disabled"])
11324
11461
  ])
11325
- ]);
11462
+ ], 512);
11326
11463
  };
11327
11464
  }
11328
11465
  });
@@ -11941,11 +12078,73 @@ function parseCSV(text) {
11941
12078
  }
11942
12079
  //#endregion
11943
12080
  //#region src/components/AutoGroupModal.adapter.ts
12081
+ var PREVIEW_VALUES = 3;
12082
+ /** Value preview of a field card: the first three values, then the count of the rest. */
12083
+ function previewValues(values) {
12084
+ const shown = values.slice(0, PREVIEW_VALUES).join(", ");
12085
+ return values.length > PREVIEW_VALUES ? `${shown} … +${values.length - PREVIEW_VALUES}` : shown;
12086
+ }
12087
+ /** Experiment line of the modal header: `exp157 · 2025-10-13 · LT`. */
12088
+ function experimentLabel(header) {
12089
+ return [
12090
+ header.code,
12091
+ header.date,
12092
+ header.operator
12093
+ ].filter(Boolean).join(" · ");
12094
+ }
12095
+ /**
12096
+ * Names of all columns of the experimental classes. A field cannot take one
12097
+ * of them: the engine merges fields by name.
12098
+ */
12099
+ function engineColumnNames(schemas) {
12100
+ const names = Object.values(schemas).filter((schema) => isExperimentalKind(schema.classKind)).flatMap((schema) => schema.columns.map((column) => column.displayName ?? column.name));
12101
+ return [...new Set(names)];
12102
+ }
12103
+ /** One chip for each QC-type class, with its sample count. */
12104
+ function engineQcChips(classes) {
12105
+ return classes.filter((c) => !isExperimentalKind(c.kind)).map((c) => ({
12106
+ name: c.label,
12107
+ count: c.members.length
12108
+ }));
12109
+ }
12110
+ /**
12111
+ * The tokens of one study sample with the role the engine gave each of them,
12112
+ * so the user sees how the names were read. The sample is the first member of
12113
+ * the largest experimental class. `cards` supplies the on/off state and the
12114
+ * layer of the tokens that are grouping fields.
12115
+ */
12116
+ function engineAnatomy(classes, schemas, tokenized, cards) {
12117
+ const cls = classes.filter((c) => isExperimentalKind(c.kind) && schemas[classKey(c)]).sort((a, b) => b.members.length - a.members.length)[0];
12118
+ if (!cls) return [];
12119
+ const tokens = tokenized[cls.members[0]] ?? [];
12120
+ return schemas[classKey(cls)].columns.flatMap((column) => {
12121
+ const text = column.sourceIndices.map((index) => tokens[index] ?? "").join("_");
12122
+ if (!text) return [];
12123
+ const name = column.displayName ?? column.name;
12124
+ const card = cards.fields.findIndex((field) => field.key === name);
12125
+ if (card === -1) return [{
12126
+ text,
12127
+ caption: /^(Field \d+|Username.*|.* \d+)$/.test(name) ? roleLabel(column.role).toLowerCase() : name,
12128
+ state: "fixed"
12129
+ }];
12130
+ const layer = cards.layers[card];
12131
+ return [layer === null ? {
12132
+ text,
12133
+ caption: name,
12134
+ state: "off"
12135
+ } : {
12136
+ text,
12137
+ caption: String(layer),
12138
+ state: "on"
12139
+ }];
12140
+ });
12141
+ }
11944
12142
  var ROLE_LABELS = {
11945
12143
  "constant": "Constant",
11946
12144
  "factor": "Factor",
11947
12145
  "replicate": "Replicate",
11948
12146
  "run-order": "Run",
12147
+ "id": "Sample ID",
11949
12148
  "numeric": "Numeric",
11950
12149
  "class-tag": "Class tag",
11951
12150
  "ignore": "Ignore"
@@ -11955,14 +12154,15 @@ function roleLabel(role) {
11955
12154
  return role ? ROLE_LABELS[role] : "Factor";
11956
12155
  }
11957
12156
  /**
11958
- * Columns worth offering as grouping toggles: factors and replicates, plus any
11959
- * column already enabled in `groupBy` (so a manual group-by choice still shows).
12157
+ * Columns worth offering as grouping toggles: factors, numerics, replicates and
12158
+ * sample IDs (the last two are off by default), plus any column already enabled
12159
+ * in `groupBy` (so a manual group-by choice still shows).
11960
12160
  * Preserves the engine's column order. `groupBy` is the schema's ORDERED layer
11961
12161
  * list — entry N is hierarchy layer N of the composed group names.
11962
12162
  */
11963
12163
  function candidateColumns(columns, groupBy) {
11964
12164
  const enabled = new Set(groupBy);
11965
- return columns.filter((c) => c.role === "factor" || c.role === "replicate" || enabled.has(c.index));
12165
+ return columns.filter((c) => GROUPING_FIELD_ROLES.has(c.role) || enabled.has(c.index));
11966
12166
  }
11967
12167
  /**
11968
12168
  * Build the batch-wide field recipe from every class schema. The active schema
@@ -11978,6 +12178,7 @@ function engineSchemasToCards(schemas, activeClassKey) {
11978
12178
  const enabledOrder = [];
11979
12179
  const enabled = /* @__PURE__ */ new Set();
11980
12180
  for (const [, schema] of ordered) {
12181
+ if (!isExperimentalKind(schema.classKind)) continue;
11981
12182
  for (const index of schema.groupBy) {
11982
12183
  const column = schema.columns.find((candidate) => candidate.index === index);
11983
12184
  if (!column) continue;
@@ -11997,8 +12198,8 @@ function engineSchemasToCards(schemas, activeClassKey) {
11997
12198
  fields: cards.map(([key, column]) => ({
11998
12199
  key,
11999
12200
  role: roleLabel(column.role),
12000
- vals: column.uniqueValues.join(", "),
12001
- rep: column.role === "replicate"
12201
+ vals: previewValues(column.uniqueValues),
12202
+ rep: column.role === "replicate" || column.role === "id"
12002
12203
  })),
12003
12204
  enabled: cards.map(([key]) => enabled.has(key)),
12004
12205
  layers: cards.map(([key]) => {
@@ -12182,12 +12383,20 @@ var AutoGroupModal_default = /* @__PURE__ */ defineComponent({
12182
12383
  const fieldCards = computed(() => engineSchemasToCards(autoGroup.schemas.value, autoGroup.activeClassKey.value));
12183
12384
  const railGroups = computed(() => engineGroupsToRail(autoGroup.result.value.experimentalGroups ?? autoGroup.groups.value));
12184
12385
  const qcSampleCount = computed(() => autoGroup.classes.value.filter((c) => c.kind !== "biological" && c.kind !== "unknown").reduce((acc, c) => acc + c.members.length, 0));
12185
- const qcChip = computed(() => qcRouting.value !== "mix" && qcSampleCount.value > 0 ? {
12186
- name: "QC_Pool",
12187
- count: qcSampleCount.value
12188
- } : null);
12386
+ const qcChips = computed(() => qcRouting.value === "mix" ? [] : engineQcChips(autoGroup.classes.value));
12189
12387
  const fieldRecipeRoute = computed(() => engineToQcRoute(qcRouting.value));
12190
12388
  const headerCount = computed(() => autoGroup.samples.value.length);
12389
+ const headerLabel = computed(() => experimentLabel(autoGroup.experiment.value));
12390
+ const reservedNames = computed(() => engineColumnNames(autoGroup.schemas.value));
12391
+ const anatomy = computed(() => autoGroup.inputMode.value === "paste" ? engineAnatomy(autoGroup.classes.value, autoGroup.schemas.value, autoGroup.tokenized.value, fieldCards.value) : []);
12392
+ const fieldHints = computed(() => {
12393
+ const needsHelp = mostlySingletons(railGroups.value);
12394
+ return fieldCards.value.fields.map((field, index) => {
12395
+ if (!needsHelp || !fieldCards.value.enabled[index]) return null;
12396
+ const remaining = autoGroup.groupCountWithout(field.key);
12397
+ return `off → ${remaining} ${remaining === 1 ? "group" : "groups"}`;
12398
+ });
12399
+ });
12191
12400
  function onToggleField(cardIndex) {
12192
12401
  const field = fieldCards.value.fields[cardIndex];
12193
12402
  if (!field) return;
@@ -12198,6 +12407,11 @@ var AutoGroupModal_default = /* @__PURE__ */ defineComponent({
12198
12407
  if (!field) return;
12199
12408
  autoGroup.moveGroupByField(field.key, direction);
12200
12409
  }
12410
+ function onRenameField(cardIndex, name) {
12411
+ const field = fieldCards.value.fields[cardIndex];
12412
+ if (!field) return;
12413
+ autoGroup.renameGroupByField(field.key, name);
12414
+ }
12201
12415
  function onRouteChange(route) {
12202
12416
  setQcRouting(qcRouteToEngine(route));
12203
12417
  }
@@ -12306,15 +12520,20 @@ var AutoGroupModal_default = /* @__PURE__ */ defineComponent({
12306
12520
  enabled: fieldCards.value.enabled,
12307
12521
  layers: fieldCards.value.layers,
12308
12522
  groups: railGroups.value,
12309
- "qc-chip": qcChip.value,
12523
+ "qc-chips": qcChips.value,
12310
12524
  route: fieldRecipeRoute.value,
12311
12525
  "header-count": headerCount.value,
12312
12526
  "qc-count": qcSampleCount.value,
12313
12527
  "can-download-template": unref(autoGroup).canDownloadTemplate.value,
12314
12528
  loading: experimentLoading.value,
12315
- "experiment-label": "",
12529
+ "experiment-label": headerLabel.value,
12530
+ anatomy: anatomy.value,
12531
+ hints: fieldHints.value,
12532
+ "reserved-names": reservedNames.value,
12533
+ renamable: "",
12316
12534
  onToggle: onToggleField,
12317
12535
  onMove: onMoveField,
12536
+ onRename: onRenameField,
12318
12537
  "onUpdate:route": onRouteChange,
12319
12538
  onCsvFile,
12320
12539
  onDownloadTemplate,
@@ -12328,12 +12547,16 @@ var AutoGroupModal_default = /* @__PURE__ */ defineComponent({
12328
12547
  "enabled",
12329
12548
  "layers",
12330
12549
  "groups",
12331
- "qc-chip",
12550
+ "qc-chips",
12332
12551
  "route",
12333
12552
  "header-count",
12334
12553
  "qc-count",
12335
12554
  "can-download-template",
12336
- "loading"
12555
+ "loading",
12556
+ "experiment-label",
12557
+ "anatomy",
12558
+ "hints",
12559
+ "reserved-names"
12337
12560
  ])) : (openBlock(), createBlock(SmartGroupManual_default, {
12338
12561
  key: manualKey.value,
12339
12562
  "sample-names": manualSampleNames.value,
@@ -22909,4 +23132,4 @@ var components_exports = /* @__PURE__ */ __exportAll({
22909
23132
  //#endregion
22910
23133
  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 };
22911
23134
 
22912
- //# sourceMappingURL=components-GDDvof8D.js.map
23135
+ //# sourceMappingURL=components-bbanLd1u.js.map