@morscherlab/mint-sdk 1.1.8 → 1.1.9

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 (90) hide show
  1. package/dist/__tests__/components/RetiredStyles.test.d.ts +1 -0
  2. package/dist/__tests__/composables/autoGroup/groupByOrder.test.d.ts +1 -0
  3. package/dist/__tests__/composables/useSampleGroups.tree.test.d.ts +1 -0
  4. package/dist/components/AppTopBar.vue.d.ts +6 -11
  5. package/dist/components/AutoGroupModal.adapter.d.ts +10 -6
  6. package/dist/components/PluginIcon.vue.d.ts +2 -2
  7. package/dist/components/PluginWorkspaceView.props.d.ts +5 -7
  8. package/dist/components/PluginWorkspaceView.vue.d.ts +0 -3
  9. package/dist/components/SampleSelector.groups.d.ts +7 -7
  10. package/dist/components/SampleSelector.selection.d.ts +6 -12
  11. package/dist/components/SampleSelector.tree.d.ts +39 -0
  12. package/dist/components/SampleSelectorGroupNode.vue.d.ts +9 -0
  13. package/dist/components/SmartGroup.types.d.ts +5 -0
  14. package/dist/components/SmartGroupFieldRecipe.groups.d.ts +3 -2
  15. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -0
  16. package/dist/components/index.js +1 -1
  17. package/dist/{components-CDomhGSj.js → components-BbtUpCTv.js} +913 -943
  18. package/dist/components-BbtUpCTv.js.map +1 -0
  19. package/dist/composables/index.d.ts +1 -1
  20. package/dist/composables/index.js +1 -1
  21. package/dist/composables/useAutoGroup.d.ts +3 -0
  22. package/dist/composables/usePlatformContext.d.ts +3 -0
  23. package/dist/composables/useSampleGroups.d.ts +25 -0
  24. package/dist/index.js +2 -2
  25. package/dist/install.js +1 -1
  26. package/dist/styles.css +130 -557
  27. package/dist/types/platform.d.ts +1 -0
  28. package/dist/{useProtocolTemplates-BMt0LlXf.js → useProtocolTemplates-BWfNaynA.js} +199 -24
  29. package/dist/useProtocolTemplates-BWfNaynA.js.map +1 -0
  30. package/dist/utils/pluginIcon.d.ts +1 -1
  31. package/dist/utils/sampleGroupPath.d.ts +6 -0
  32. package/package.json +1 -1
  33. package/src/__tests__/components/AppAvatarMenu.test.ts +11 -0
  34. package/src/__tests__/components/AppTopBar.test.ts +84 -10
  35. package/src/__tests__/components/AppTopBarPageSelector.test.ts +1 -1
  36. package/src/__tests__/components/AutoGroupModal.adapter.test.ts +16 -6
  37. package/src/__tests__/components/PluginIcon.test.ts +19 -0
  38. package/src/__tests__/components/PluginWorkspaceView.test.ts +8 -0
  39. package/src/__tests__/components/RetiredStyles.test.ts +82 -0
  40. package/src/__tests__/components/SampleSelector.drag.test.ts +26 -0
  41. package/src/__tests__/components/SampleSelector.groups.test.ts +19 -6
  42. package/src/__tests__/components/SampleSelector.selection.test.ts +9 -23
  43. package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +5 -0
  44. package/src/__tests__/composables/autoGroup/compose.test.ts +9 -10
  45. package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +249 -0
  46. package/src/__tests__/composables/autoGroup/integration.test.ts +3 -7
  47. package/src/__tests__/composables/useSampleGroups.tree.test.ts +145 -0
  48. package/src/components/AppTopBar.story.vue +9 -7
  49. package/src/components/AppTopBar.vue +79 -72
  50. package/src/components/AutoGroupModal.adapter.ts +17 -12
  51. package/src/components/AutoGroupModal.story.vue +89 -0
  52. package/src/components/AutoGroupModal.vue +16 -8
  53. package/src/components/PluginIcon.story.vue +11 -9
  54. package/src/components/PluginIcon.vue +3 -3
  55. package/src/components/PluginWorkspaceView.props.ts +5 -7
  56. package/src/components/PluginWorkspaceView.vue +0 -4
  57. package/src/components/SampleSelector.drag.ts +5 -5
  58. package/src/components/SampleSelector.groups.ts +15 -15
  59. package/src/components/SampleSelector.selection.ts +7 -46
  60. package/src/components/SampleSelector.story.vue +37 -0
  61. package/src/components/SampleSelector.tree.ts +48 -0
  62. package/src/components/SampleSelector.vue +52 -320
  63. package/src/components/SampleSelectorGroupNode.vue +197 -0
  64. package/src/components/SmartGroup.types.ts +5 -0
  65. package/src/components/SmartGroupFieldRecipe.groups.ts +6 -2
  66. package/src/components/SmartGroupFieldRecipe.vue +76 -4
  67. package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -2
  68. package/src/composables/autoGroup/compose.ts +31 -10
  69. package/src/composables/index.ts +1 -0
  70. package/src/composables/useAutoGroup.ts +108 -5
  71. package/src/composables/useSampleGroups.ts +118 -33
  72. package/src/styles/components/app-avatar-menu.css +2 -1
  73. package/src/styles/components/app-plugin-switcher.css +0 -13
  74. package/src/styles/components/app-top-bar.css +13 -54
  75. package/src/styles/components/dose-calculator.css +0 -4
  76. package/src/styles/components/experiment-popover.css +2 -104
  77. package/src/styles/components/experiment-selector-modal.css +0 -7
  78. package/src/styles/components/plugin-icon.css +3 -0
  79. package/src/styles/components/protocol-step-editor.css +0 -6
  80. package/src/styles/components/reagent-editor.css +0 -5
  81. package/src/styles/components/sample-hierarchy-tree.css +0 -4
  82. package/src/styles/components/sample-selector.css +0 -96
  83. package/src/styles/components/schedule-calendar.css +0 -6
  84. package/src/styles/components/smart-group.css +22 -0
  85. package/src/styles/components/well-plate.css +1 -27
  86. package/src/types/platform.ts +1 -0
  87. package/src/utils/pluginIcon.ts +5 -2
  88. package/src/utils/sampleGroupPath.ts +19 -0
  89. package/dist/components-CDomhGSj.js.map +0 -1
  90. package/dist/useProtocolTemplates-BMt0LlXf.js.map +0 -1
@@ -23,13 +23,22 @@
23
23
  position: relative;
24
24
  }
25
25
 
26
- /* New: brand container (flex-shrink: 0) */
26
+ /* Brand container */
27
27
  .mint-topbar__brand {
28
28
  display: flex;
29
29
  align-items: center;
30
30
  flex-shrink: 0;
31
31
  }
32
32
 
33
+ .mint-topbar__identity {
34
+ width: 2.25rem;
35
+ height: 2.25rem;
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ flex-shrink: 0;
40
+ }
41
+
33
42
  /* Centered section — absolute so asymmetric left/right weight never shifts the
34
43
  pill-nav off the topbar midline. Pointer-events: none lets the pill-nav
35
44
  overlay the background while remaining interactive via its own children. */
@@ -66,8 +75,8 @@
66
75
  }
67
76
 
68
77
  .mint-topbar__logo-icon {
69
- width: 2rem;
70
- height: 2rem;
78
+ width: 2.25rem;
79
+ height: 2.25rem;
71
80
  background-color: var(--color-primary);
72
81
  border-radius: var(--radius-md);
73
82
  display: flex;
@@ -81,12 +90,6 @@
81
90
  font-size: 0.875rem;
82
91
  }
83
92
 
84
- .mint-topbar__logo-svg {
85
- width: 1.125rem;
86
- height: 1.125rem;
87
- color: white;
88
- }
89
-
90
93
  .mint-topbar__title-only {
91
94
  font-weight: 600;
92
95
  color: var(--text-primary);
@@ -137,7 +140,7 @@
137
140
  line-height: 1.25;
138
141
  }
139
142
 
140
- /* New: generic icon button (notifications, etc.) */
143
+ /* Generic icon button (notifications, etc.) */
141
144
  .mint-topbar__icon-btn {
142
145
  position: relative;
143
146
  display: inline-flex;
@@ -229,50 +232,6 @@
229
232
  height: 1.25rem;
230
233
  }
231
234
 
232
- /* Profile button */
233
- .mint-topbar__profile-btn {
234
- display: inline-flex;
235
- align-items: center;
236
- gap: 0.5rem;
237
- padding: 0.25rem 0.75rem 0.25rem 0.25rem;
238
- border: none;
239
- background: transparent;
240
- border-radius: var(--radius);
241
- cursor: pointer;
242
- transition: background-color 0.15s ease;
243
- }
244
-
245
- .mint-topbar__profile-btn:hover {
246
- background: var(--bg-hover);
247
- }
248
-
249
- .mint-topbar__profile-avatar {
250
- width: 1.75rem;
251
- height: 1.75rem;
252
- border-radius: 9999px;
253
- display: flex;
254
- align-items: center;
255
- justify-content: center;
256
- font-size: 0.75rem;
257
- font-weight: 500;
258
- color: white;
259
- background: linear-gradient(135deg, var(--color-primary-hover) 0%, var(--color-primary) 100%);
260
- flex-shrink: 0;
261
- }
262
-
263
- .mint-topbar__profile-name {
264
- font-size: 0.875rem;
265
- font-weight: 500;
266
- color: var(--text-primary);
267
- white-space: nowrap;
268
- }
269
-
270
- @media (max-width: 640px) {
271
- .mint-topbar__profile-name {
272
- display: none;
273
- }
274
- }
275
-
276
235
  /* Standalone mode badge */
277
236
  .mint-topbar__standalone-badge {
278
237
  display: inline-flex;
@@ -295,7 +295,3 @@
295
295
  color: var(--text-muted);
296
296
  margin-bottom: 0.375rem;
297
297
  }
298
-
299
- .mint-dose-calculator__mw-input {
300
- width: 100%;
301
- }
@@ -523,105 +523,6 @@
523
523
  background: rgba(239, 68, 68, 0.08);
524
524
  }
525
525
 
526
- /* Divider */
527
- .mint-experiment-popover__divider {
528
- height: 1px;
529
- background: var(--border-light);
530
- margin: 0;
531
- }
532
-
533
- /* Save footer */
534
- .mint-experiment-popover__footer {
535
- padding: 0.75rem 1rem;
536
- }
537
-
538
- .mint-experiment-popover__save-btn {
539
- display: flex;
540
- align-items: center;
541
- justify-content: center;
542
- gap: 0.375rem;
543
- width: 100%;
544
- height: 2.125rem;
545
- padding: 0 0.75rem;
546
- border: none;
547
- background-color: var(--color-primary);
548
- background-image: linear-gradient(
549
- to bottom,
550
- rgba(255, 255, 255, 0.12),
551
- rgba(255, 255, 255, 0)
552
- );
553
- border-radius: var(--radius-md);
554
- color: white;
555
- font-size: 0.8125rem;
556
- font-weight: 500;
557
- font-family: inherit;
558
- cursor: pointer;
559
- transform: translateY(0);
560
- transition:
561
- background-color 0.15s var(--mint-ease-out-quart, cubic-bezier(0.25, 1, 0.5, 1)),
562
- box-shadow 0.15s var(--mint-ease-out-quart, cubic-bezier(0.25, 1, 0.5, 1)),
563
- opacity 0.15s var(--mint-ease-out-quart, cubic-bezier(0.25, 1, 0.5, 1)),
564
- transform 0.15s var(--mint-ease-out-quart, cubic-bezier(0.25, 1, 0.5, 1)),
565
- background-image 0.15s var(--mint-ease-out-quart, cubic-bezier(0.25, 1, 0.5, 1));
566
- }
567
-
568
- .mint-experiment-popover__save-btn:hover:not(:disabled) {
569
- background-color: var(--color-primary-hover);
570
- box-shadow: var(--shadow-sm);
571
- transform: translateY(-1px);
572
- }
573
-
574
- .mint-experiment-popover__save-btn:active:not(:disabled) {
575
- transform: translateY(0);
576
- box-shadow: none;
577
- background-image: none;
578
- transition-duration: 0.05s;
579
- }
580
-
581
- .mint-experiment-popover__save-btn:disabled {
582
- opacity: 0.45;
583
- cursor: not-allowed;
584
- background-image: none;
585
- }
586
-
587
- .mint-experiment-popover__save-btn--loading {
588
- opacity: 0.8;
589
- pointer-events: none;
590
- }
591
-
592
- .mint-experiment-popover__save-btn--success {
593
- background-color: var(--color-primary-soft);
594
- background-image: none;
595
- border: 1px solid rgba(16, 185, 129, 0.3);
596
- color: var(--mint-success, #10b981);
597
- }
598
-
599
- .mint-experiment-popover__save-btn--success:hover {
600
- background-color: rgba(16, 185, 129, 0.18);
601
- }
602
-
603
- .mint-experiment-popover__save-icon,
604
- .mint-experiment-popover__check-icon {
605
- width: 1rem;
606
- height: 1rem;
607
- }
608
-
609
- .mint-experiment-popover__save-hint {
610
- font-size: 0.6875rem;
611
- color: var(--text-muted);
612
- text-align: center;
613
- margin-top: 0.5rem;
614
- }
615
-
616
- .mint-experiment-popover__spinner {
617
- width: 0.875rem;
618
- height: 0.875rem;
619
- border: 2px solid rgba(255, 255, 255, 0.3);
620
- border-top-color: white;
621
- border-radius: 50%;
622
- animation: mint-experiment-popover-spin 0.6s linear infinite;
623
- }
624
-
625
526
  @keyframes mint-experiment-popover-spin {
626
527
  to { transform: rotate(360deg); }
627
528
  }
@@ -635,8 +536,7 @@
635
536
  @media (prefers-reduced-motion: reduce) {
636
537
  .mint-experiment-popover__trigger,
637
538
  .mint-experiment-popover__save-trigger,
638
- .mint-experiment-popover__select-btn,
639
- .mint-experiment-popover__save-btn {
539
+ .mint-experiment-popover__select-btn {
640
540
  transition-property: background-color, border-color, box-shadow, color, opacity;
641
541
  transition-duration: 0.15s;
642
542
  transition-timing-function: ease;
@@ -648,9 +548,7 @@
648
548
  .mint-experiment-popover__save-trigger:hover,
649
549
  .mint-experiment-popover__save-trigger:active,
650
550
  .mint-experiment-popover__select-btn:hover,
651
- .mint-experiment-popover__select-btn:active,
652
- .mint-experiment-popover__save-btn:hover,
653
- .mint-experiment-popover__save-btn:active {
551
+ .mint-experiment-popover__select-btn:active {
654
552
  transform: none;
655
553
  }
656
554
  .mint-experiment-popover__trigger-chevron {
@@ -16,18 +16,11 @@
16
16
  flex-wrap: wrap;
17
17
  }
18
18
 
19
- /* Base filters row */
20
- .mint-experiment-selector__filters {
21
- display: flex;
22
- gap: 0.5rem;
23
- }
24
-
25
19
  .mint-experiment-selector__search {
26
20
  flex: 1;
27
21
  min-width: 10rem;
28
22
  }
29
23
 
30
- .mint-experiment-selector__status-filter,
31
24
  .mint-experiment-selector__filter-select {
32
25
  flex-shrink: 0;
33
26
  width: 10rem;
@@ -13,6 +13,7 @@
13
13
  /* Sizes */
14
14
  .mint-plugin-icon--sm { width: 1.5rem; height: 1.5rem; }
15
15
  .mint-plugin-icon--md { width: 2rem; height: 2rem; }
16
+ .mint-plugin-icon--topbar { width: 2.25rem; height: 2.25rem; }
16
17
  .mint-plugin-icon--lg { width: 3rem; height: 3rem; border-radius: 0.75rem; }
17
18
 
18
19
  /* Variants — `--mint-plugin-icon-tone` falls back to --color-primary */
@@ -30,6 +31,8 @@
30
31
  .mint-plugin-icon--sm .mint-plugin-icon__img { width: 0.9375rem; height: 0.9375rem; }
31
32
  .mint-plugin-icon--md .mint-plugin-icon__svg,
32
33
  .mint-plugin-icon--md .mint-plugin-icon__img { width: 1.125rem; height: 1.125rem; }
34
+ .mint-plugin-icon--topbar .mint-plugin-icon__svg,
35
+ .mint-plugin-icon--topbar .mint-plugin-icon__img { width: 1.25rem; height: 1.25rem; }
33
36
  .mint-plugin-icon--lg .mint-plugin-icon__svg,
34
37
  .mint-plugin-icon--lg .mint-plugin-icon__img { width: 1.5rem; height: 1.5rem; }
35
38
 
@@ -203,12 +203,6 @@
203
203
  color: var(--mint-error);
204
204
  }
205
205
 
206
- .mint-protocol-editor__field-row {
207
- display: grid;
208
- grid-template-columns: repeat(2, 1fr);
209
- gap: 0.75rem;
210
- }
211
-
212
206
  /* Input styles */
213
207
  .mint-protocol-editor__input {
214
208
  width: 100%;
@@ -116,11 +116,6 @@
116
116
  min-width: 0;
117
117
  }
118
118
 
119
- .mint-reagent-editor__row {
120
- display: flex;
121
- gap: 0.75rem;
122
- }
123
-
124
119
  .mint-reagent-editor__position-row {
125
120
  display: flex;
126
121
  gap: 0.625rem;
@@ -32,10 +32,6 @@
32
32
  background-color: var(--bg-hover);
33
33
  }
34
34
 
35
- .mint-sample-tree__node--selected > .mint-sample-tree__node-header {
36
- background-color: rgba(59, 130, 246, 0.1);
37
- }
38
-
39
35
  .mint-sample-tree--sm .mint-sample-tree__node-header {
40
36
  padding: 0.25rem 0.375rem;
41
37
  gap: 0.25rem;
@@ -110,87 +110,6 @@
110
110
  white-space: nowrap;
111
111
  }
112
112
 
113
- /* Level Popover */
114
- .mint-sample-selector__popover {
115
- position: absolute;
116
- left: 0;
117
- right: 0;
118
- top: 100%;
119
- margin-top: 0.5rem;
120
- background-color: var(--bg-primary);
121
- border: 1px solid var(--border-color);
122
- border-radius: var(--radius-lg);
123
- box-shadow: var(--shadow-lg);
124
- overflow: hidden;
125
- z-index: 50;
126
- }
127
-
128
- .mint-sample-selector__popover-header {
129
- padding: 0.5rem 0.75rem;
130
- font-size: 0.6875rem;
131
- font-weight: 600;
132
- text-transform: uppercase;
133
- letter-spacing: 0.05em;
134
- color: var(--text-secondary);
135
- background-color: var(--bg-secondary);
136
- }
137
-
138
- .mint-sample-selector__popover-body {
139
- padding: 0.25rem 0;
140
- }
141
-
142
- .mint-sample-selector__level-btn {
143
- width: 100%;
144
- padding: 0.625rem 0.75rem;
145
- text-align: left;
146
- background: none;
147
- border: none;
148
- cursor: pointer;
149
- transition: background-color 0.15s ease;
150
- }
151
-
152
- .mint-sample-selector__level-btn:hover {
153
- background-color: var(--bg-hover);
154
- }
155
-
156
- .mint-sample-selector__level-info {
157
- display: flex;
158
- flex-direction: column;
159
- gap: 0.125rem;
160
- }
161
-
162
- .mint-sample-selector__level-title {
163
- display: flex;
164
- align-items: center;
165
- gap: 0.5rem;
166
- font-size: 0.875rem;
167
- font-weight: 500;
168
- color: var(--text-primary);
169
- }
170
-
171
- .mint-sample-selector__level-badge {
172
- font-size: 0.6875rem;
173
- padding: 0.125rem 0.375rem;
174
- background-color: var(--color-primary);
175
- color: white;
176
- border-radius: 9999px;
177
- font-variant-numeric: tabular-nums;
178
- }
179
-
180
- .mint-sample-selector__level-desc {
181
- font-size: 0.75rem;
182
- color: var(--text-muted);
183
- }
184
-
185
- .mint-sample-selector__level-example {
186
- margin-left: 0.25rem;
187
- }
188
-
189
- .mint-sample-selector__level-example code {
190
- font-family: 'Fira Code', monospace;
191
- font-size: 0.6875rem;
192
- }
193
-
194
113
  /* Grouped View */
195
114
  .mint-sample-selector__grouped {
196
115
  display: flex;
@@ -706,21 +625,6 @@
706
625
  text-overflow: ellipsis;
707
626
  }
708
627
 
709
- /* Transitions */
710
- .mint-popover-enter-active {
711
- transition: all 0.15s ease-out;
712
- }
713
-
714
- .mint-popover-leave-active {
715
- transition: all 0.1s ease-in;
716
- }
717
-
718
- .mint-popover-enter-from,
719
- .mint-popover-leave-to {
720
- opacity: 0;
721
- transform: translateY(-0.25rem);
722
- }
723
-
724
628
  .mint-collapse-enter-active {
725
629
  transition: all 0.2s ease-out;
726
630
  overflow: hidden;
@@ -207,12 +207,6 @@
207
207
  color: white;
208
208
  }
209
209
 
210
- /* Grid lines / slots */
211
- .mint-schedule__grid-line {
212
- height: var(--slot-height, 2.5rem);
213
- border-bottom: 1px solid var(--border-light, var(--border-color));
214
- }
215
-
216
210
  .mint-schedule__slot {
217
211
  height: var(--slot-height, 2.5rem);
218
212
  border-bottom: 1px solid var(--border-light, var(--border-color));
@@ -305,6 +305,28 @@
305
305
  .fr .sg-field__vals { grid-column: 1 / 2; grid-row: 2; }
306
306
  .fr-switch { grid-column: 2; grid-row: 1 / 3; align-self: center; }
307
307
 
308
+ /* Layer-order controls: which hierarchy layer this field forms, plus reorder. */
309
+ .sg-field__order {
310
+ grid-column: 1 / 3;
311
+ display: flex; align-items: center; gap: 0.25rem;
312
+ }
313
+ .sg-field__layer {
314
+ font-size: 0.625rem; font-weight: 600; text-transform: uppercase;
315
+ letter-spacing: 0.04em; color: var(--color-primary);
316
+ padding: 0.0625rem 0.375rem; border-radius: 9999px;
317
+ background: var(--color-primary-soft);
318
+ margin-right: auto;
319
+ }
320
+ .sg-field__order-btn {
321
+ display: inline-flex; align-items: center; justify-content: center;
322
+ width: 1.25rem; height: 1.25rem; padding: 0;
323
+ border: 1px solid var(--border-color); border-radius: 0.375rem;
324
+ background: var(--bg-primary); color: var(--text-secondary);
325
+ cursor: pointer; transition: background 0.15s ease, color 0.15s ease;
326
+ }
327
+ .sg-field__order-btn:hover:not(:disabled) { background: var(--bg-tertiary); color: var(--text-primary); }
328
+ .sg-field__order-btn:disabled { opacity: 0.4; cursor: default; }
329
+
308
330
  /* Replicate-on: the field card itself warns instead of silently shattering. */
309
331
  .fr-field--warn.sg-field--on {
310
332
  border-color: var(--mint-warning-border);
@@ -113,7 +113,7 @@
113
113
  background-color: rgba(99, 102, 241, 0.2);
114
114
  }
115
115
 
116
- .mint-well-plate__well--hovered:not(.mint-well-plate__well--readonly) {
116
+ .mint-well-plate__well--hovered {
117
117
  box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.5);
118
118
  }
119
119
 
@@ -162,32 +162,6 @@
162
162
  color: var(--text-primary);
163
163
  }
164
164
 
165
- /* Sample type colors */
166
- .mint-well-plate__well--sample {
167
- background-color: rgba(16, 185, 129, 0.15);
168
- border: 1px solid rgba(16, 185, 129, 0.4);
169
- }
170
-
171
- .mint-well-plate__well--control {
172
- background-color: rgba(59, 130, 246, 0.15);
173
- border: 1px solid rgba(59, 130, 246, 0.4);
174
- }
175
-
176
- .mint-well-plate__well--blank {
177
- background-color: rgba(249, 115, 22, 0.15);
178
- border: 1px solid rgba(249, 115, 22, 0.4);
179
- }
180
-
181
- .mint-well-plate__well--qc {
182
- background-color: rgba(139, 92, 246, 0.15);
183
- border: 1px solid rgba(139, 92, 246, 0.4);
184
- }
185
-
186
- .mint-well-plate__well--iqc {
187
- background-color: rgba(236, 72, 153, 0.15);
188
- border: 1px solid rgba(236, 72, 153, 0.4);
189
- }
190
-
191
165
  /* Condition header - drug label row */
192
166
  .mint-well-plate__condition-label {
193
167
  font-weight: 600;
@@ -5,6 +5,7 @@ export interface PluginInfo {
5
5
  id: string
6
6
  name: string
7
7
  version: string
8
+ plugin_type?: string
8
9
  description?: string
9
10
  icon?: string
10
11
  color?: string
@@ -1,4 +1,4 @@
1
- export type PluginIconFormat = 'path' | 'data-url' | 'https-url' | 'structured' | 'fallback'
1
+ export type PluginIconFormat = 'path' | 'data-url' | 'asset-url' | 'structured' | 'fallback'
2
2
 
3
3
  export interface StructuredPluginIconStop {
4
4
  offset: string
@@ -41,6 +41,7 @@ export const PLUGIN_ICON_FALLBACK_PATH = 'M14 7v4a1 1 0 0 0 1 1h4M5 3h9l5 5v11a2
41
41
 
42
42
  const PATH_REGEX = /^[Mm][\s,\d\-.]/
43
43
  const RASTER_DATA_URL_REGEX = /^data:image\/(png|jpeg|jpg|gif|webp);/
44
+ const SAME_ORIGIN_ASSET_URL_REGEX = /^\/(?!\/)[^\s?#]+\.(?:svg|png|jpe?g|gif|webp)(?:[?#].*)?$/i
44
45
  const VIEW_BOX_REGEX = /^-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?\s+\d+(?:\.\d+)?\s+\d+(?:\.\d+)?$/
45
46
  const COLOR_REGEX = /^(#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})|currentColor|white|black|none)$/i
46
47
  const MAX_STRUCTURED_ICON_LENGTH = 8000
@@ -58,7 +59,9 @@ export function detectPluginIcon(icon?: string): DetectedPluginIcon {
58
59
  if (structured) return { format: 'structured', value: structured }
59
60
  if (PATH_REGEX.test(raw)) return { format: 'path', value: raw }
60
61
  if (RASTER_DATA_URL_REGEX.test(raw)) return { format: 'data-url', value: raw }
61
- if (raw.startsWith('https://')) return { format: 'https-url', value: raw }
62
+ if (raw.startsWith('https://') || SAME_ORIGIN_ASSET_URL_REGEX.test(raw)) {
63
+ return { format: 'asset-url', value: raw }
64
+ }
62
65
  return { format: 'fallback', value: PLUGIN_ICON_FALLBACK_PATH }
63
66
  }
64
67
 
@@ -0,0 +1,19 @@
1
+ /** Split a group name into normalized hierarchy segments. */
2
+ export function splitSampleGroupPath(groupName: string, separator: string): string[] {
3
+ const parts = groupName
4
+ .split(separator)
5
+ .map(part => part.trim())
6
+ .filter(part => part.length > 0)
7
+ return parts.length > 0 ? parts : [groupName]
8
+ }
9
+
10
+ /** First normalized hierarchy segment, used as the reorderable family key. */
11
+ export function getSampleGroupMajorPrefix(groupName: string, separator: string): string {
12
+ return splitSampleGroupPath(groupName, separator)[0]
13
+ }
14
+
15
+ /** Normalized path above a concrete leaf group. */
16
+ export function getSampleGroupParentPath(groupName: string, separator: string): string {
17
+ const parts = splitSampleGroupPath(groupName, separator)
18
+ return parts.length > 1 ? parts.slice(0, -1).join(separator) : ''
19
+ }