@morscherlab/mint-sdk 1.4.0-beta.12 → 1.4.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AlertBox-BdoZqANv.js → AlertBox-SnsbVow-.js} +3 -3
- package/dist/{AlertBox-BdoZqANv.js.map → AlertBox-SnsbVow-.js.map} +1 -1
- package/dist/{AppSidebar-DGK5i-hJ.js → AppSidebar-Bi8g9mRt.js} +237 -70
- package/dist/{AppSidebar-DGK5i-hJ.js.map → AppSidebar-Bi8g9mRt.js.map} +1 -1
- package/dist/{ConfirmDialog-nbJj51is.js → ConfirmDialog-CSls2Nuv.js} +2 -2
- package/dist/{ConfirmDialog-nbJj51is.js.map → ConfirmDialog-CSls2Nuv.js.map} +1 -1
- package/dist/{ExperimentPopover-CbD7BR_s.js → ExperimentPopover-ChKCrklH.js} +5 -5
- package/dist/{ExperimentPopover-CbD7BR_s.js.map → ExperimentPopover-ChKCrklH.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-BoofZZxF.js → ExperimentSelectorModal-BqifjNaN.js} +113 -97
- package/dist/ExperimentSelectorModal-BqifjNaN.js.map +1 -0
- package/dist/{SegmentedControl-BkwYOKpG.js → SegmentedControl-hOgBVdju.js} +2 -2
- package/dist/{SegmentedControl-BkwYOKpG.js.map → SegmentedControl-hOgBVdju.js.map} +1 -1
- package/dist/{SettingsModal-BXsjIVnm.js → SettingsModal-jra_2u0o.js} +7 -7
- package/dist/{SettingsModal-BXsjIVnm.js.map → SettingsModal-jra_2u0o.js.map} +1 -1
- package/dist/__stories__/experiment-helpers.d.ts +10 -0
- package/dist/components/AppTopBar.vue.d.ts +13 -5
- package/dist/components/BaseButton.vue.d.ts +4 -2
- package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -1
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
- package/dist/{controlSchemaModel-BF5g4wRC.js → controlSchemaModel-CIH5Hqpw.js} +2 -2
- package/dist/{controlSchemaModel-BF5g4wRC.js.map → controlSchemaModel-CIH5Hqpw.js.map} +1 -1
- package/dist/{data-ihsBd9bs.js → data-OZ94DhVC.js} +11 -11
- package/dist/{data-ihsBd9bs.js.map → data-OZ94DhVC.js.map} +1 -1
- package/dist/data.js +8 -8
- package/dist/{experimentStatus-Cqp6qNrR.js → experimentStatus-BFrKpHAH.js} +2 -2
- package/dist/{experimentStatus-Cqp6qNrR.js.map → experimentStatus-BFrKpHAH.js.map} +1 -1
- package/dist/index.js +18 -18
- package/dist/{install-CFgfRO64.js → install-Bx_6KwJA.js} +15 -15
- package/dist/{install-CFgfRO64.js.map → install-Bx_6KwJA.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-B-pLwc5U.js → lab-ZDKlx1Oo.js} +10 -10
- package/dist/{lab-B-pLwc5U.js.map → lab-ZDKlx1Oo.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-DnK_1Q_S.js → records-B4yl2oIu.js} +2 -2
- package/dist/{records-DnK_1Q_S.js.map → records-B4yl2oIu.js.map} +1 -1
- package/dist/{runtime-pKfF_iql.js → runtime-BJvGms6h.js} +9 -9
- package/dist/{runtime-pKfF_iql.js.map → runtime-BJvGms6h.js.map} +1 -1
- package/dist/runtime.js +6 -6
- package/dist/{shell-BIH8Rc21.js → shell-Czyrzk7l.js} +5 -5
- package/dist/{shell-BIH8Rc21.js.map → shell-Czyrzk7l.js.map} +1 -1
- package/dist/shell.js +5 -5
- package/dist/styles.css +296 -45
- package/dist/types/index.d.ts +1 -1
- package/dist/types/jobs.d.ts +9 -0
- package/dist/{useDropdownState-8yWMu-7_.js → useDropdownState-Bb2N4m40.js} +15 -3
- package/dist/useDropdownState-Bb2N4m40.js.map +1 -0
- package/dist/{useExperimentSamples-BGN1dmcR.js → useExperimentSamples-BLx29CkM.js} +3 -3
- package/dist/{useExperimentSamples-BGN1dmcR.js.map → useExperimentSamples-BLx29CkM.js.map} +1 -1
- package/dist/{useListReorder-DPynGrCj.js → useListReorder-C_8RYeVP.js} +3 -3
- package/dist/{useListReorder-DPynGrCj.js.map → useListReorder-C_8RYeVP.js.map} +1 -1
- package/dist/{useRackEditor-B2FrQA0p.js → useRackEditor-BhYlXKFM.js} +8 -8
- package/dist/{useRackEditor-B2FrQA0p.js.map → useRackEditor-BhYlXKFM.js.map} +1 -1
- package/dist/{useRealtime-yWjTTnc9.js → useRealtime-CUMbW_ln.js} +2 -2
- package/dist/{useRealtime-yWjTTnc9.js.map → useRealtime-CUMbW_ln.js.map} +1 -1
- package/dist/{useRequest-Dqw12k2s.js → useRequest-C9uwM79q.js} +3 -3
- package/dist/{useRequest-Dqw12k2s.js.map → useRequest-C9uwM79q.js.map} +1 -1
- package/package.json +1 -1
- package/src/__stories__/experiment-helpers.ts +3 -3
- package/src/__tests__/components/AppTopBar.test.ts +175 -1
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
- package/src/__tests__/components/BaseButton.test.ts +19 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +34 -1
- package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
- package/src/components/AppTopBar.story.vue +59 -4
- package/src/components/AppTopBar.vue +107 -8
- package/src/components/BaseButton.vue +16 -2
- package/src/components/ExperimentSelectorModal.story.vue +38 -0
- package/src/components/ExperimentSelectorModal.vue +20 -1
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
- package/src/styles/components/app-page-selector.css +120 -45
- package/src/styles/components/app-top-bar.css +155 -0
- package/src/styles/components/button.css +8 -0
- package/src/styles/components/experiment-selector-modal.css +13 -0
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +10 -0
- package/dist/ExperimentSelectorModal-BoofZZxF.js.map +0 -1
- package/dist/useDropdownState-8yWMu-7_.js.map +0 -1
|
@@ -1,51 +1,76 @@
|
|
|
1
|
-
/* AppTopBar page selector —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
(own card, tinted active row) rather than .mint-popover + .mint-menu-item. */
|
|
1
|
+
/* AppTopBar page selector — design v2 option C: "<plugin> / <page>" as one switcher.
|
|
2
|
+
The crumb holds the plugin name, a slash and the tinted 32px trigger. The menu keeps its own
|
|
3
|
+
card (not .mint-popover): a "<plugin> pages" title, then one row per page with its hint and number key. */
|
|
5
4
|
|
|
6
|
-
/* Shrinks
|
|
5
|
+
/* Shrinks with the bar: the plugin name and the page label truncate instead of overlapping the nav. */
|
|
6
|
+
.mint-page-selector__crumb {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 0.375rem;
|
|
10
|
+
min-width: 0;
|
|
11
|
+
flex-shrink: 1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.mint-page-selector__plugin {
|
|
15
|
+
min-width: 0;
|
|
16
|
+
font-size: 0.875rem;
|
|
17
|
+
font-weight: 600;
|
|
18
|
+
color: var(--text-primary);
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
text-overflow: ellipsis;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.mint-page-selector__slash {
|
|
25
|
+
flex-shrink: 0;
|
|
26
|
+
font-size: 1.125rem;
|
|
27
|
+
font-weight: 300;
|
|
28
|
+
line-height: 1;
|
|
29
|
+
color: var(--text-muted);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Anchors the menu under the trigger, not under the plugin name. */
|
|
7
33
|
.mint-page-selector {
|
|
8
34
|
position: relative;
|
|
9
|
-
|
|
35
|
+
display: flex;
|
|
36
|
+
min-width: 0;
|
|
10
37
|
}
|
|
11
38
|
|
|
12
39
|
.mint-page-selector__trigger {
|
|
13
40
|
display: inline-flex;
|
|
14
41
|
align-items: center;
|
|
15
|
-
gap: 0.
|
|
16
|
-
width:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
gap: 0.375rem;
|
|
43
|
+
min-width: 0;
|
|
44
|
+
max-width: 100%;
|
|
45
|
+
height: 2rem;
|
|
46
|
+
padding: 0 0.5rem 0 0.375rem;
|
|
47
|
+
border: 0;
|
|
48
|
+
background: var(--color-primary-soft);
|
|
21
49
|
color: var(--text-primary);
|
|
22
50
|
border-radius: var(--radius-md);
|
|
23
51
|
font-size: 0.8125rem;
|
|
24
|
-
font-weight:
|
|
52
|
+
font-weight: 600;
|
|
25
53
|
font-family: inherit;
|
|
26
54
|
cursor: pointer;
|
|
27
55
|
transition:
|
|
28
56
|
background-color var(--mint-transition),
|
|
29
|
-
border-color var(--mint-transition),
|
|
30
57
|
box-shadow var(--mint-transition);
|
|
31
58
|
}
|
|
32
59
|
|
|
33
|
-
.mint-page-selector__trigger:hover
|
|
34
|
-
background: var(--
|
|
60
|
+
.mint-page-selector__trigger:hover {
|
|
61
|
+
background: color-mix(in srgb, var(--color-primary) 16%, transparent);
|
|
35
62
|
}
|
|
36
63
|
|
|
37
|
-
.mint-page-selector__trigger:focus-visible
|
|
38
|
-
.mint-page-selector__trigger--open {
|
|
64
|
+
.mint-page-selector__trigger:focus-visible {
|
|
39
65
|
outline: none;
|
|
40
|
-
|
|
41
|
-
box-shadow: var(--focus-ring-soft);
|
|
66
|
+
box-shadow: var(--focus-ring-offset);
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
/* Page icon tile is neutral; the plugin carries the color. */
|
|
45
70
|
.mint-page-selector__icon {
|
|
46
71
|
width: 1.25rem;
|
|
47
72
|
height: 1.25rem;
|
|
48
|
-
border-radius: var(--radius
|
|
73
|
+
border-radius: var(--radius);
|
|
49
74
|
background: var(--bg-tertiary);
|
|
50
75
|
color: var(--text-secondary-strong);
|
|
51
76
|
display: grid;
|
|
@@ -66,7 +91,7 @@
|
|
|
66
91
|
.mint-page-selector__trigger-metadata-icon.mint-plugin-icon--sm {
|
|
67
92
|
width: 1.25rem;
|
|
68
93
|
height: 1.25rem;
|
|
69
|
-
border-radius: var(--radius
|
|
94
|
+
border-radius: var(--radius);
|
|
70
95
|
}
|
|
71
96
|
|
|
72
97
|
.mint-page-selector__icon-fallback {
|
|
@@ -76,7 +101,6 @@
|
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
.mint-page-selector__label {
|
|
79
|
-
flex: 1;
|
|
80
104
|
min-width: 0;
|
|
81
105
|
text-align: left;
|
|
82
106
|
white-space: nowrap;
|
|
@@ -84,7 +108,6 @@
|
|
|
84
108
|
text-overflow: ellipsis;
|
|
85
109
|
}
|
|
86
110
|
|
|
87
|
-
/* The chevron sits inline; the wrap no longer draws a tile. */
|
|
88
111
|
.mint-page-selector__chevron-wrap {
|
|
89
112
|
display: inline-flex;
|
|
90
113
|
color: var(--text-secondary);
|
|
@@ -101,38 +124,46 @@
|
|
|
101
124
|
transform: rotate(180deg);
|
|
102
125
|
}
|
|
103
126
|
|
|
127
|
+
/* max-height: 8 two-line hint rows (8 x 68px) + title (~25px) + padding (8px) = ~577px; a 9th row scrolls. A short window caps it. */
|
|
104
128
|
.mint-page-selector__menu {
|
|
105
129
|
position: absolute;
|
|
130
|
+
box-sizing: border-box;
|
|
106
131
|
top: calc(100% + 0.375rem);
|
|
107
132
|
left: 0;
|
|
108
|
-
width:
|
|
133
|
+
width: 18.75rem;
|
|
134
|
+
max-height: min(36.25rem, calc(100vh - 6rem));
|
|
135
|
+
overflow-y: auto;
|
|
109
136
|
background: var(--bg-card);
|
|
110
137
|
border: 1px solid var(--border-color);
|
|
111
138
|
border-radius: var(--radius-lg);
|
|
112
139
|
box-shadow: var(--shadow-md);
|
|
113
|
-
padding: 0.
|
|
140
|
+
padding: 0.25rem;
|
|
114
141
|
z-index: 50;
|
|
115
142
|
}
|
|
116
143
|
|
|
144
|
+
/* The mockup's 10px uppercase eyebrow breaks rules 2a (no uppercase) and 8d (UI text >= 11px): 11px, sentence case. */
|
|
117
145
|
.mint-page-selector__menu-title {
|
|
118
|
-
padding: 0.375rem 0.
|
|
119
|
-
font-
|
|
146
|
+
padding: 0.375rem 0.5rem 0.25rem;
|
|
147
|
+
font-family: var(--font-mono);
|
|
148
|
+
font-size: 0.6875rem;
|
|
120
149
|
font-weight: 500;
|
|
121
|
-
|
|
150
|
+
letter-spacing: 0.06em;
|
|
151
|
+
color: var(--text-muted);
|
|
122
152
|
}
|
|
123
153
|
|
|
124
154
|
.mint-page-selector__item {
|
|
125
155
|
display: flex;
|
|
126
|
-
|
|
156
|
+
box-sizing: border-box;
|
|
157
|
+
align-items: flex-start;
|
|
127
158
|
gap: 0.625rem;
|
|
128
159
|
width: 100%;
|
|
129
|
-
padding: 0.5rem
|
|
160
|
+
padding: 0.5rem;
|
|
130
161
|
border: 0;
|
|
131
162
|
background: transparent;
|
|
132
|
-
border-radius:
|
|
163
|
+
border-radius: var(--radius);
|
|
133
164
|
color: var(--text-primary);
|
|
134
165
|
font-size: 0.8125rem;
|
|
135
|
-
|
|
166
|
+
line-height: 1.4;
|
|
136
167
|
font-family: inherit;
|
|
137
168
|
text-align: left;
|
|
138
169
|
text-decoration: none;
|
|
@@ -140,13 +171,22 @@
|
|
|
140
171
|
transition: background-color var(--mint-transition);
|
|
141
172
|
}
|
|
142
173
|
|
|
143
|
-
|
|
174
|
+
/* A row without a hint has one line: centre it on the icon tile. */
|
|
175
|
+
.mint-page-selector__item:not(:has(.mint-page-selector__item-hint)) {
|
|
176
|
+
align-items: center;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.mint-page-selector__item:hover:not(.mint-page-selector__item--disabled):not(.mint-page-selector__item--active) {
|
|
144
180
|
background: var(--bg-hover);
|
|
145
181
|
}
|
|
146
182
|
|
|
183
|
+
.mint-page-selector__item:focus-visible {
|
|
184
|
+
outline: 2px solid var(--color-primary);
|
|
185
|
+
outline-offset: -2px;
|
|
186
|
+
}
|
|
187
|
+
|
|
147
188
|
.mint-page-selector__item--active {
|
|
148
|
-
background: var(--
|
|
149
|
-
font-weight: 600;
|
|
189
|
+
background: var(--color-primary-soft);
|
|
150
190
|
}
|
|
151
191
|
|
|
152
192
|
.mint-page-selector__item--disabled {
|
|
@@ -155,9 +195,9 @@
|
|
|
155
195
|
}
|
|
156
196
|
|
|
157
197
|
.mint-page-selector__item-icon {
|
|
158
|
-
width: 1.
|
|
159
|
-
height: 1.
|
|
160
|
-
border-radius:
|
|
198
|
+
width: 1.75rem;
|
|
199
|
+
height: 1.75rem;
|
|
200
|
+
border-radius: var(--radius-md);
|
|
161
201
|
background: var(--bg-tertiary);
|
|
162
202
|
color: var(--text-secondary);
|
|
163
203
|
display: grid;
|
|
@@ -165,6 +205,10 @@
|
|
|
165
205
|
flex-shrink: 0;
|
|
166
206
|
}
|
|
167
207
|
|
|
208
|
+
.mint-page-selector__item-icon .mint-page-selector__icon-fallback {
|
|
209
|
+
font-size: 0.8125rem;
|
|
210
|
+
}
|
|
211
|
+
|
|
168
212
|
.mint-page-selector__item--active .mint-page-selector__item-icon {
|
|
169
213
|
background: var(--color-primary-soft);
|
|
170
214
|
color: var(--color-primary);
|
|
@@ -177,24 +221,55 @@
|
|
|
177
221
|
}
|
|
178
222
|
|
|
179
223
|
.mint-page-selector__item-icon svg {
|
|
180
|
-
width: 0.
|
|
181
|
-
height: 0.
|
|
224
|
+
width: 0.9375rem;
|
|
225
|
+
height: 0.9375rem;
|
|
182
226
|
}
|
|
183
227
|
|
|
184
228
|
.mint-page-selector__metadata-icon.mint-plugin-icon--sm {
|
|
185
|
-
width: 1.
|
|
186
|
-
height: 1.
|
|
187
|
-
border-radius:
|
|
229
|
+
width: 1.75rem;
|
|
230
|
+
height: 1.75rem;
|
|
231
|
+
border-radius: var(--radius-md);
|
|
188
232
|
}
|
|
189
233
|
|
|
190
|
-
.mint-page-selector__item-
|
|
234
|
+
.mint-page-selector__item-text {
|
|
191
235
|
flex: 1;
|
|
192
236
|
min-width: 0;
|
|
237
|
+
display: grid;
|
|
238
|
+
gap: 1px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.mint-page-selector__item-label {
|
|
242
|
+
font-weight: 600;
|
|
193
243
|
overflow: hidden;
|
|
194
244
|
text-overflow: ellipsis;
|
|
195
245
|
white-space: nowrap;
|
|
196
246
|
}
|
|
197
247
|
|
|
248
|
+
/* --_ink-primary is --color-primary-hover in light mode and stays legible in dark mode. */
|
|
249
|
+
.mint-page-selector__item--active .mint-page-selector__item-label {
|
|
250
|
+
color: var(--_ink-primary);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.mint-page-selector__item-hint {
|
|
254
|
+
font-size: 0.75rem;
|
|
255
|
+
line-height: 1.35;
|
|
256
|
+
color: var(--text-secondary);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* 11px, the rule 8d floor (the mockup has 10.5px). */
|
|
260
|
+
.mint-page-selector__item-key {
|
|
261
|
+
flex-shrink: 0;
|
|
262
|
+
margin-top: 0.125rem;
|
|
263
|
+
padding: 0 0.25rem;
|
|
264
|
+
border: 1px solid var(--border-color);
|
|
265
|
+
border-radius: var(--radius);
|
|
266
|
+
font-family: var(--font-mono);
|
|
267
|
+
font-size: 0.6875rem;
|
|
268
|
+
font-weight: 500;
|
|
269
|
+
line-height: 1rem;
|
|
270
|
+
color: var(--text-muted);
|
|
271
|
+
}
|
|
272
|
+
|
|
198
273
|
@media (prefers-reduced-motion: reduce) {
|
|
199
274
|
.mint-page-selector__trigger,
|
|
200
275
|
.mint-page-selector__chevron {
|
|
@@ -231,3 +231,158 @@
|
|
|
231
231
|
white-space: nowrap;
|
|
232
232
|
user-select: none;
|
|
233
233
|
}
|
|
234
|
+
|
|
235
|
+
/* ─── Run slot: [ experiment | job ] ───
|
|
236
|
+
While a job shows, one border wraps the experiment chip and the job
|
|
237
|
+
segment, as the experiment popover does for Save. The wrapper does not
|
|
238
|
+
clip: the popover panel and the focus rings reach outside it, so each
|
|
239
|
+
segment rounds its own outer corners. The idle Jobs button stands apart
|
|
240
|
+
with its own border. */
|
|
241
|
+
.mint-topbar__run-slot {
|
|
242
|
+
display: inline-flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
gap: 0.5rem;
|
|
245
|
+
min-width: 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.mint-topbar__run-slot--joined {
|
|
249
|
+
gap: 0;
|
|
250
|
+
border: 1px solid var(--border-color);
|
|
251
|
+
border-radius: var(--radius-md);
|
|
252
|
+
background: var(--bg-card);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.mint-topbar__run-slot--joined .mint-experiment-popover,
|
|
256
|
+
.mint-topbar__run-slot--joined .mint-experiment-popover__trigger {
|
|
257
|
+
border: 0;
|
|
258
|
+
border-radius: calc(var(--radius-md) - 1px) 0 0 calc(var(--radius-md) - 1px);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* The job takes the name's place: the name folds away while the job grows in,
|
|
262
|
+
and the code badge stays. The name stays in the accessible name. A trigger
|
|
263
|
+
with no code, the empty placeholder and the error text keep their text. */
|
|
264
|
+
.mint-topbar__run-slot .mint-experiment-popover__trigger-text {
|
|
265
|
+
transition:
|
|
266
|
+
max-width 200ms ease,
|
|
267
|
+
opacity 150ms ease,
|
|
268
|
+
margin 200ms ease;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.mint-topbar__run-slot--joined
|
|
272
|
+
.mint-experiment-popover__trigger:has(.mint-exp-code)
|
|
273
|
+
.mint-experiment-popover__trigger-text {
|
|
274
|
+
max-width: 0 !important; /* beats the inline maxNameWidth */
|
|
275
|
+
margin-inline-start: -0.5rem; /* takes back the trigger gap */
|
|
276
|
+
opacity: 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.mint-topbar__job {
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
max-width: 6rem; /* close to the content (icon + bar, or "Jobs"), so the fold is even */
|
|
282
|
+
overflow: hidden;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: 0.375rem;
|
|
285
|
+
flex-shrink: 0;
|
|
286
|
+
height: 30px;
|
|
287
|
+
padding: 0 0.625rem;
|
|
288
|
+
border: 0;
|
|
289
|
+
background: transparent;
|
|
290
|
+
color: var(--text-secondary);
|
|
291
|
+
font: inherit;
|
|
292
|
+
font-size: 0.75rem;
|
|
293
|
+
font-weight: 500;
|
|
294
|
+
white-space: nowrap;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
transition: background-color var(--mint-transition);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.mint-topbar__run-slot--joined .mint-topbar__job {
|
|
300
|
+
border-radius: calc(var(--radius-md) - 1px);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.mint-topbar__run-slot--joined .mint-experiment-popover + .mint-topbar__job {
|
|
304
|
+
border-left: 1px solid var(--border-color);
|
|
305
|
+
border-radius: 0 calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.mint-topbar__job--idle {
|
|
309
|
+
height: 32px;
|
|
310
|
+
border: 1px solid var(--border-color);
|
|
311
|
+
border-radius: var(--radius-md);
|
|
312
|
+
background: var(--bg-card);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.mint-topbar__job--idle:hover,
|
|
316
|
+
.mint-topbar__job--neutral:hover {
|
|
317
|
+
background: var(--bg-hover);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.mint-topbar__job:focus-visible {
|
|
321
|
+
outline: none;
|
|
322
|
+
box-shadow: inset var(--focus-ring);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.mint-topbar__job--primary { background: var(--color-primary-soft); color: var(--_ink-primary); }
|
|
326
|
+
.mint-topbar__job--warning { background: var(--mint-warning-bg); color: var(--mint-warning-text); }
|
|
327
|
+
.mint-topbar__job--success { background: var(--mint-success-bg); color: color-mix(in srgb, var(--mint-success) 50%, var(--text-primary)); }
|
|
328
|
+
.mint-topbar__job--error { background: var(--mint-error-bg); color: var(--_ink-error); }
|
|
329
|
+
|
|
330
|
+
.mint-topbar__job--primary:hover,
|
|
331
|
+
.mint-topbar__job--warning:hover,
|
|
332
|
+
.mint-topbar__job--success:hover,
|
|
333
|
+
.mint-topbar__job--error:hover {
|
|
334
|
+
filter: brightness(0.97);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.mint-topbar__job-icon {
|
|
338
|
+
flex-shrink: 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.mint-topbar__job-icon--spin {
|
|
342
|
+
animation: mint-topbar-job-spin 1s linear infinite;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
@keyframes mint-topbar-job-spin {
|
|
346
|
+
to { transform: rotate(360deg); }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.mint-topbar__job-bar {
|
|
350
|
+
width: 2rem;
|
|
351
|
+
height: 4px;
|
|
352
|
+
border-radius: 2px;
|
|
353
|
+
background: var(--border-color);
|
|
354
|
+
overflow: hidden;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.mint-topbar__job-bar > span {
|
|
358
|
+
display: block;
|
|
359
|
+
height: 100%;
|
|
360
|
+
background: var(--color-primary);
|
|
361
|
+
transition: width var(--mint-transition);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Enter and leave: the job grows in from zero width and folds back out. */
|
|
365
|
+
.mint-topbar-job-enter-active,
|
|
366
|
+
.mint-topbar-job-leave-active {
|
|
367
|
+
transition:
|
|
368
|
+
max-width 200ms ease,
|
|
369
|
+
padding 200ms ease,
|
|
370
|
+
opacity 150ms ease;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.mint-topbar-job-enter-from,
|
|
374
|
+
.mint-topbar-job-leave-to {
|
|
375
|
+
max-width: 0;
|
|
376
|
+
padding-inline: 0;
|
|
377
|
+
opacity: 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Screen-reader only: announces status changes of the shown job. */
|
|
381
|
+
.mint-topbar__job-status {
|
|
382
|
+
position: absolute;
|
|
383
|
+
width: 1px;
|
|
384
|
+
height: 1px;
|
|
385
|
+
overflow: hidden;
|
|
386
|
+
clip-path: inset(50%);
|
|
387
|
+
white-space: nowrap;
|
|
388
|
+
}
|
|
@@ -150,6 +150,14 @@
|
|
|
150
150
|
height: 1.25rem;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/* Flex centring centres the line box, and Geist's ascender puts the caps
|
|
154
|
+
about 1.6px above the middle. Trim the text box to cap height and baseline
|
|
155
|
+
so the caps sit on the centre line. Browsers without text-box keep the
|
|
156
|
+
untrimmed box. */
|
|
157
|
+
.mint-button__label {
|
|
158
|
+
text-box: trim-both cap alphabetic;
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
/* Spinner */
|
|
154
162
|
.mint-button__spinner {
|
|
155
163
|
animation: mint-button-spin 1s linear infinite;
|
|
@@ -112,13 +112,26 @@
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/* Footer with clear selection */
|
|
115
|
+
.mint-experiment-selector__description {
|
|
116
|
+
padding: 0 1.25rem 0.75rem;
|
|
117
|
+
font-size: 0.8125rem;
|
|
118
|
+
color: var(--text-secondary);
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
.mint-experiment-selector__footer {
|
|
116
122
|
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 0.75rem;
|
|
117
125
|
justify-content: flex-start;
|
|
118
126
|
padding: 0.5rem 1.25rem 0.75rem;
|
|
119
127
|
border-top: 1px solid var(--border-light);
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
.mint-experiment-selector__footer-slot {
|
|
131
|
+
flex: 1;
|
|
132
|
+
min-width: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
122
135
|
.mint-experiment-selector__clear-btn {
|
|
123
136
|
padding: 0.25rem 0;
|
|
124
137
|
border: none;
|
package/src/types/index.ts
CHANGED
package/src/types/jobs.ts
CHANGED
|
@@ -68,3 +68,13 @@ export interface JobSnapshotPayload {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export type JobStreamData = JobStateInput | JobSnapshotPayload
|
|
71
|
+
|
|
72
|
+
/** What the top-bar run slot shows for the viewer's own job. The plugin picks the job. */
|
|
73
|
+
export interface TopBarJob {
|
|
74
|
+
/** Lifecycle of the shown job; sets the icon and the tone. */
|
|
75
|
+
status: JobStatus
|
|
76
|
+
/** Tooltip and accessible name, e.g. "Extracting", "Saved to EXP-0142". Not shown as text. */
|
|
77
|
+
label: string
|
|
78
|
+
/** 0-100; drawn as a small bar after the icon. */
|
|
79
|
+
percent?: number | null
|
|
80
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ExperimentSelectorModal-BoofZZxF.js","names":[],"sources":["../src/composables/useExperimentSelector.ts","../src/components/ExperimentSelectorModal.vue","../src/components/ExperimentSelectorModal.vue"],"sourcesContent":["import { ref, reactive, computed, watch, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport { useDebouncedWatch } from './useDebouncedWatch'\nimport { useRequest } from './useRequest'\nimport { datePresetToISO } from './experiment-utils'\nimport type {\n ExperimentSummary,\n ExperimentListResponse,\n ExperimentFilters,\n ExperimentTypeOption,\n ExperimentSortField,\n PlatformContext,\n SelectOption,\n} from '../types'\n\nfunction getPlatformContext(): PlatformContext | undefined {\n if (typeof window === 'undefined') return undefined\n return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__\n}\n\nfunction getPlatformApiUrl(): string | undefined {\n return getPlatformContext()?.platformApiUrl\n}\n\nexport interface UseExperimentSelectorOptions {\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n apiBaseUrl?: string\n limit?: number\n immediate?: boolean\n /** Keep `statusCounts` in step with the non-status filters (one stats request per change). */\n withStatusCounts?: boolean\n}\n\nexport interface UseExperimentSelectorReturn {\n /** Current page of fetched experiment rows. */\n experiments: Ref<ExperimentSummary[]>\n /** Total matching experiment count reported by the platform. */\n total: Ref<number>\n /** Currently selected experiment, or null. */\n selectedExperiment: Ref<ExperimentSummary | null>\n /** Mutable filters used for the next fetch. */\n filters: ExperimentFilters\n /** Whether experiment rows are currently loading. */\n isLoading: Ref<boolean>\n /** Error message from the last failed fetch, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful experiment fetch, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Current zero-based page index. */\n page: Ref<number>\n /** Whether another page can be loaded. */\n hasMore: ComputedRef<boolean>\n // Sort\n /** Combined sort key in the form `<field>:<asc|desc>`. */\n sortKey: Ref<string>\n // Filter options\n /** Available experiment type options for filter UI. */\n experimentTypes: Ref<ExperimentTypeOption[]>\n /** Available project options for filter UI. */\n projects: Ref<SelectOption<string>[]>\n /** Experiment count per status under every filter except status itself. */\n statusCounts: Ref<Record<string, number>>\n // Methods\n /** Fetch the current page with the current filters. */\n fetch: () => Promise<void>\n /** Fetch and append the next page when available. */\n loadMore: () => Promise<void>\n /** Clear fetched rows and refetch page zero. */\n reset: () => void\n /** Select an experiment. */\n select: (experiment: ExperimentSummary) => void\n /** Clear selection and filters. */\n clear: () => void\n /** Fetch experiment type and project filter options. */\n fetchFilterOptions: () => Promise<void>\n /** Fetch `statusCounts` from `GET /experiments/stats`. */\n fetchStatusCounts: () => Promise<void>\n}\n\n/** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */\nexport function useExperimentSelector(\n options: UseExperimentSelectorOptions = {},\n): UseExperimentSelectorReturn {\n const {\n limit = 100,\n immediate = false,\n experimentType,\n allowedExperimentTypes: optionAllowedExperimentTypes,\n apiBaseUrl,\n withStatusCounts = false,\n } = options\n const platformBase = apiBaseUrl ?? getPlatformApiUrl()\n const api = useApi()\n\n const experiments = ref<ExperimentSummary[]>([])\n const total = ref(0)\n const selectedExperiment = ref<ExperimentSummary | null>(null)\n const request = useRequest('Failed to fetch experiments')\n const isLoading = request.isLoading\n const error = request.error\n const lastLoadedAt = ref<Date | null>(null)\n const page = ref(0)\n let fetchGeneration = 0\n\n // Sort: combined key like \"created_at:desc\"\n const sortKey = ref<string>('created_at:desc')\n\n // Filter option data (fetched once, cached)\n const experimentTypes = ref<ExperimentTypeOption[]>([])\n const projects = ref<SelectOption<string>[]>([])\n let filterOptionsFetched = false\n\n const statusCounts = ref<Record<string, number>>({})\n let statsGeneration = 0\n let countedScope: string | null = null\n\n const hasMore = computed(() => experiments.value.length < total.value)\n\n const filters: ExperimentFilters = reactive({\n search: undefined,\n status: undefined,\n project: undefined,\n experimentType: undefined,\n datePreset: undefined,\n mine: undefined,\n })\n\n function normalizeExperimentTypes(values: string[] | null | undefined): string[] | null {\n if (values == null) return null\n return [...new Set(values.filter((value): value is string => typeof value === 'string' && value.length > 0))]\n }\n\n function currentAllowedExperimentTypes(): string[] | null {\n if (optionAllowedExperimentTypes !== undefined) {\n return normalizeExperimentTypes(optionAllowedExperimentTypes)\n }\n return normalizeExperimentTypes(getPlatformContext()?.allowedExperimentTypes)\n }\n\n function parseSortKey(): { sortBy: ExperimentSortField; sortOrder: 'asc' | 'desc' } {\n const [field, order] = sortKey.value.split(':')\n return {\n sortBy: (field || 'created_at') as ExperimentSortField,\n sortOrder: (order || 'desc') as 'asc' | 'desc',\n }\n }\n\n type ExperimentTypeScope =\n | { kind: 'all' }\n | { kind: 'blocked' }\n | { kind: 'single'; experimentType: string }\n | { kind: 'multi'; experimentTypes: string[] }\n\n function resolveExperimentTypeScope(): ExperimentTypeScope {\n const allowedTypes = currentAllowedExperimentTypes()\n const selectedType = experimentType ?? filters.experimentType ?? undefined\n\n if (selectedType) {\n if (allowedTypes && !allowedTypes.includes(selectedType)) {\n return { kind: 'blocked' }\n }\n return { kind: 'single', experimentType: selectedType }\n }\n\n if (allowedTypes) {\n if (allowedTypes.length === 0) return { kind: 'blocked' }\n if (allowedTypes.length === 1) return { kind: 'single', experimentType: allowedTypes[0] }\n return { kind: 'multi', experimentTypes: allowedTypes }\n }\n\n return { kind: 'all' }\n }\n\n /** Query params shared by the list and the stats route; status, sort and paging are list-only. */\n function scopeParams(typeFilter: string | undefined): URLSearchParams {\n const params = new URLSearchParams()\n if (typeFilter) params.set('experiment_type', typeFilter)\n if (filters.search) params.set('search', filters.search)\n if (filters.project) params.set('project', filters.project)\n if (filters.mine) params.set('mine', 'true')\n if (filters.datePreset) {\n params.set('created_after', datePresetToISO(filters.datePreset))\n }\n return params\n }\n\n function buildExperimentListUrl(typeFilter: string | undefined, skip: number, pageLimit: number): string {\n const params = scopeParams(typeFilter)\n if (filters.status) params.set('status', filters.status)\n\n const { sortBy, sortOrder } = parseSortKey()\n params.set('sort_by', sortBy)\n params.set('sort_order', sortOrder)\n\n params.set('limit', String(pageLimit))\n params.set('skip', String(skip))\n\n const query = params.toString()\n const base = platformBase ?? ''\n return `${base}/experiments${query ? `?${query}` : ''}`\n }\n\n function sortExperiments(items: ExperimentSummary[]): ExperimentSummary[] {\n const { sortBy, sortOrder } = parseSortKey()\n const direction = sortOrder === 'desc' ? -1 : 1\n return [...items].sort((a, b) => {\n const aValue = a[sortBy] ?? ''\n const bValue = b[sortBy] ?? ''\n if (aValue < bValue) return -1 * direction\n if (aValue > bValue) return 1 * direction\n return 0\n })\n }\n\n async function fetchExperimentList(typeFilter: string | undefined, skip: number, pageLimit: number) {\n return await api.get<ExperimentListResponse>(buildExperimentListUrl(typeFilter, skip, pageLimit))\n }\n\n async function fetchScopedExperimentPage(\n requestedPage: number,\n ): Promise<ExperimentListResponse> {\n const scope = resolveExperimentTypeScope()\n const pageSkip = requestedPage * limit\n\n if (scope.kind === 'blocked') {\n return { experiments: [], total: 0 }\n }\n\n if (scope.kind === 'multi') {\n const perTypeLimit = pageSkip + limit\n const responses = await Promise.all(\n scope.experimentTypes.map(type => fetchExperimentList(type, 0, perTypeLimit)),\n )\n const merged = sortExperiments(responses.flatMap(response => response.experiments))\n return {\n experiments: merged.slice(pageSkip, pageSkip + limit),\n total: responses.reduce((sum, response) => sum + response.total, 0),\n }\n }\n\n return await fetchExperimentList(\n scope.kind === 'single' ? scope.experimentType : undefined,\n pageSkip,\n limit,\n )\n }\n\n async function fetchExperiments(): Promise<void> {\n const generation = ++fetchGeneration\n const requestedPage = page.value\n isLoading.value = true\n request.clearError()\n try {\n const data = await fetchScopedExperimentPage(requestedPage)\n if (generation !== fetchGeneration) return\n if (requestedPage === 0) {\n experiments.value = data.experiments\n } else {\n experiments.value = [...experiments.value, ...data.experiments]\n }\n total.value = data.total\n lastLoadedAt.value = new Date()\n } catch (err) {\n if (generation !== fetchGeneration) return\n request.setError(err, 'Failed to fetch experiments')\n if (requestedPage === 0) {\n experiments.value = []\n total.value = 0\n }\n } finally {\n if (generation === fetchGeneration) {\n isLoading.value = false\n }\n }\n }\n\n function invalidateFetch(): void {\n fetchGeneration += 1\n isLoading.value = false\n }\n\n async function fetchFilterOptions(): Promise<void> {\n if (filterOptionsFetched) return\n filterOptionsFetched = true\n\n const base = platformBase ?? ''\n const [typesRes, projectsRes] = await Promise.allSettled([\n api.get<Array<{ value: string; label: string; color?: string }>>(`${base}/experiments/experiment-types`),\n api.get<{ projects: Array<{ id: number; name: string }>; total: number }>(`${base}/projects`),\n ])\n\n if (typesRes.status === 'fulfilled' && Array.isArray(typesRes.value)) {\n const allowedTypes = currentAllowedExperimentTypes()\n const allowedSet = allowedTypes ? new Set(allowedTypes) : null\n experimentTypes.value = typesRes.value\n .filter(t => !allowedSet || allowedSet.has(t.value))\n .map(t => ({\n value: t.value,\n label: t.label,\n color: t.color,\n }))\n if (filters.experimentType && allowedSet && !allowedSet.has(filters.experimentType)) {\n filters.experimentType = undefined\n }\n }\n\n if (projectsRes.status === 'fulfilled' && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) {\n projects.value = projectsRes.value.projects.map(p => ({\n value: p.name,\n label: p.name,\n }))\n }\n }\n\n /** The filters the stats route reads; status, sort and paging do not change the counts. */\n function countScope(): string {\n return JSON.stringify([filters.search, filters.project, filters.experimentType, filters.datePreset, filters.mine])\n }\n\n function refreshStatusCountsOnScopeChange(): void {\n if (withStatusCounts && countScope() !== countedScope) fetchStatusCounts()\n }\n\n async function fetchStatusCounts(): Promise<void> {\n const generation = ++statsGeneration\n countedScope = countScope()\n const scope = resolveExperimentTypeScope()\n if (scope.kind === 'blocked') {\n statusCounts.value = {}\n return\n }\n const types = scope.kind === 'multi'\n ? scope.experimentTypes\n : [scope.kind === 'single' ? scope.experimentType : undefined]\n const base = platformBase ?? ''\n try {\n const responses = await Promise.all(\n types.map(type => api.get<{ total: number; by_status: Record<string, number> }>(\n `${base}/experiments/stats?${scopeParams(type)}`,\n )),\n )\n if (generation !== statsGeneration) return\n const counts: Record<string, number> = {}\n for (const response of responses) {\n for (const [status, count] of Object.entries(response.by_status)) {\n counts[status] = (counts[status] ?? 0) + count\n }\n }\n statusCounts.value = counts\n } catch {\n // Counts only decorate the status menu; without them it shows bare labels.\n // Forget the scope so the next status or sort change retries the request.\n if (generation === statsGeneration) {\n statusCounts.value = {}\n countedScope = null\n }\n }\n }\n\n async function loadMore(): Promise<void> {\n if (!hasMore.value || isLoading.value) return\n page.value++\n await fetchExperiments()\n }\n\n function reset(): void {\n page.value = 0\n experiments.value = []\n total.value = 0\n fetchExperiments()\n }\n\n function select(experiment: ExperimentSummary): void {\n selectedExperiment.value = experiment\n }\n\n function clear(): void {\n invalidateFetch()\n selectedExperiment.value = null\n filters.search = undefined\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.datePreset = undefined\n filters.mine = undefined\n sortKey.value = 'created_at:desc'\n page.value = 0\n request.clearError()\n }\n\n watch(\n () => [\n filters.search,\n filters.status,\n filters.project,\n filters.experimentType,\n filters.datePreset,\n filters.mine,\n sortKey.value,\n ],\n () => invalidateFetch(),\n { flush: 'sync' },\n )\n\n const searchWatch = useDebouncedWatch(\n () => filters.search,\n () => {\n page.value = 0\n fetchExperiments()\n refreshStatusCountsOnScopeChange()\n },\n { delay: 300 },\n )\n\n // Immediate watch on discrete filters and sort (cancel any pending search debounce)\n watch(\n () => [filters.status, filters.project, filters.experimentType, filters.datePreset, filters.mine, sortKey.value],\n () => {\n searchWatch.cancel()\n page.value = 0\n fetchExperiments()\n // Also checked on a status or sort change: the cancelled debounce may hold a new search.\n refreshStatusCountsOnScopeChange()\n },\n )\n\n if (immediate) {\n fetchExperiments()\n }\n\n return {\n experiments,\n total,\n selectedExperiment,\n filters,\n isLoading,\n error,\n lastLoadedAt,\n page,\n hasMore,\n sortKey,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n loadMore,\n reset,\n select,\n clear,\n fetchFilterOptions,\n fetchStatusCounts,\n }\n}\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n","<script setup lang=\"ts\">\n/**\n * Modal for searching and selecting an experiment from the platform list with filters and keyboard nav.\n *\n * @example Recommended\n * ```vue\n * <ExperimentSelectorModal v-model:open=\"open\" :allowed-experiment-types=\"['mass-spectrometry']\" />\n * ```\n */\nimport { ref, computed, watch, type Component } from 'vue'\nimport { ChevronDown, CircleDot, Folder, Shapes, User } from 'lucide-vue-next'\nimport type { ModalSize, ExperimentSummary, ExperimentFilters, ActionMenuItem, Tone } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport { useExperimentStore } from '../stores/experiment'\nimport { formatExperimentDate } from '../composables/experiment-utils'\nimport * as experimentStatus from '../utils/experimentStatus'\nimport BaseModal from './BaseModal.vue'\nimport ActionMenu from './ActionMenu.vue'\nimport ResourcePickerListInternal from './internal/ResourcePickerListInternal.vue'\n\ninterface Props {\n open: boolean\n experimentType?: string\n allowedExperimentTypes?: string[] | null\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'lg',\n})\n\n// Selection is committed to the shared experiment store rather than emitted.\n// A single owner of \"the current experiment\" is what stops callers holding —\n// or inventing — a record of their own.\nconst emit = defineEmits<{\n 'update:open': [value: boolean]\n}>()\n\nconst experimentStore = useExperimentStore()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n experimentTypes,\n projects,\n statusCounts,\n fetch: fetchExperiments,\n fetchFilterOptions,\n fetchStatusCounts,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n allowedExperimentTypes: props.allowedExperimentTypes,\n withStatusCounts: true,\n})\n\n// Re-mounting the list on open clears its keyboard highlight.\nconst openCount = ref(0)\n\nconst STATUS_ORDER = ['ongoing', 'planned', 'completed', 'cancelled'] as const\n\nconst canChooseExperimentType = computed(() =>\n !props.experimentType && experimentTypes.value.length > 1,\n)\n\ntype FilterKey = 'status' | 'project' | 'experimentType'\n\ninterface FilterMenu {\n key: FilterKey\n label: string\n icon: Component\n items: ActionMenuItem[]\n /** Trigger text while the filter is set. */\n activeLabel?: string\n dotTone?: Tone\n}\n\n/** Every item carries `checked`, so ActionMenu renders a single-choice menu. */\nfunction choiceItems(key: FilterKey, anyLabel: string, options: { value: string; label: string }[]): ActionMenuItem[] {\n const current = filters[key] ?? ''\n return [{ value: '', label: anyLabel }, ...options].map(option => ({\n id: option.value,\n label: option.label,\n checked: option.value === current,\n }))\n}\n\nconst filterMenus = computed<FilterMenu[]>(() => {\n const menus: FilterMenu[] = [{\n key: 'status',\n label: 'Status',\n icon: CircleDot,\n items: choiceItems('status', 'Any status', STATUS_ORDER.map(status => {\n const count = statusCounts.value[status]\n const label = experimentStatus.format(status)\n return { value: status, label: count === undefined ? label : `${label} · ${count}` }\n })),\n activeLabel: filters.status ? experimentStatus.format(filters.status) : undefined,\n dotTone: filters.status ? experimentStatus.tone(filters.status) : undefined,\n }]\n if (projects.value.length) {\n menus.push({\n key: 'project',\n label: 'Project',\n icon: Folder,\n items: choiceItems('project', 'Any project', projects.value),\n activeLabel: filters.project ?? undefined,\n })\n }\n if (canChooseExperimentType.value) {\n menus.push({\n key: 'experimentType',\n label: 'Type',\n icon: Shapes,\n items: choiceItems('experimentType', 'Any type', experimentTypes.value),\n activeLabel: experimentTypes.value.find(t => t.value === filters.experimentType)?.label\n ?? filters.experimentType ?? undefined,\n })\n }\n return menus\n})\n\nconst activeFilterCount = computed(() =>\n [filters.status, filters.project, filters.experimentType, filters.mine].filter(Boolean).length,\n)\n\nfunction clearFilters() {\n filters.status = undefined\n filters.project = undefined\n filters.experimentType = undefined\n filters.mine = undefined\n}\n\nfunction describe(experiment: ExperimentSummary) {\n const owner = experiment.created_by_user\n return {\n code: experiment.experiment_code,\n title: experiment.name,\n meta: [\n experiment.project_name || experiment.project || '',\n owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',\n ].filter(Boolean),\n status: experiment.status\n ? { label: experimentStatus.format(experiment.status), tone: experimentStatus.tone(experiment.status) }\n : undefined,\n date: formatExperimentDate(experiment.created_at),\n }\n}\n\nfunction setFilter<K extends keyof ExperimentFilters>(key: K, value: string | number) {\n ;(filters as Record<string, unknown>)[key] = String(value) || undefined\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n experimentStore.select(experiment)\n emit('update:open', false)\n}\n\nfunction handleDeselect() {\n experimentStore.clear()\n emit('update:open', false)\n}\n\n// Fetch on open\nwatch(\n () => props.open,\n (isOpen) => {\n if (isOpen) {\n openCount.value++\n fetchFilterOptions()\n fetchExperiments()\n fetchStatusCounts()\n }\n },\n { immediate: true },\n)\n</script>\n\n<template>\n <BaseModal\n :open=\"open\"\n :title=\"title\"\n :size=\"size\"\n @update:open=\"emit('update:open', $event)\"\n >\n <ResourcePickerListInternal\n :key=\"openCount\"\n :search=\"filters.search ?? ''\"\n class=\"mint-experiment-selector\"\n autofocus-search\n label=\"Experiments\"\n placeholder=\"Search experiments…\"\n :items=\"experiments\"\n :item-key=\"(experiment) => experiment.id\"\n :describe=\"describe\"\n :selected-key=\"currentExperimentId\"\n :loading=\"isLoading\"\n :error=\"error\"\n empty-title=\"No experiments found\"\n @update:search=\"filters.search = $event\"\n @select=\"handleSelect\"\n >\n <template #filters>\n <ActionMenu\n v-for=\"menu in filterMenus\"\n :key=\"menu.key\"\n :label=\"menu.label\"\n :items=\"menu.items\"\n @select=\"item => setFilter(menu.key, item.id)\"\n >\n <template #trigger=\"{ open: menuOpen, toggle, keydown }\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': menu.activeLabel }\"\n :aria-label=\"`${menu.label}: ${menu.activeLabel ?? menu.items[0].label}`\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"menuOpen\"\n @click=\"toggle\"\n @keydown=\"keydown\"\n >\n <span\n v-if=\"menu.dotTone\"\n class=\"mint-experiment-selector__dot\"\n :class=\"`mint-experiment-selector__dot--${menu.dotTone}`\"\n aria-hidden=\"true\"\n />\n <component :is=\"menu.icon\" v-else :size=\"13\" aria-hidden=\"true\" />\n <span class=\"mint-experiment-selector__toggle-label\">{{ menu.activeLabel ?? menu.label }}</span>\n <ChevronDown :size=\"12\" aria-hidden=\"true\" />\n </button>\n </template>\n </ActionMenu>\n <button\n type=\"button\"\n class=\"mint-experiment-selector__toggle\"\n :class=\"{ 'mint-experiment-selector__toggle--active': filters.mine }\"\n :aria-pressed=\"!!filters.mine\"\n @click=\"filters.mine = filters.mine ? undefined : true\"\n >\n <User :size=\"13\" aria-hidden=\"true\" />\n Mine\n </button>\n <button\n v-if=\"activeFilterCount\"\n type=\"button\"\n class=\"mint-experiment-selector__clear-filters\"\n @click=\"clearFilters\"\n >\n Clear {{ activeFilterCount }}\n </button>\n </template>\n </ResourcePickerListInternal>\n\n <div v-if=\"currentExperimentId != null\" class=\"mint-experiment-selector__footer\">\n <button\n type=\"button\"\n class=\"mint-experiment-selector__clear-btn\"\n @click=\"handleDeselect\"\n >\n Clear selection\n </button>\n </div>\n </BaseModal>\n</template>\n"],"mappings":"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"}
|