@morscherlab/mint-sdk 1.0.12 → 1.0.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/{ExperimentPopover-CCYB1oWp.js → ExperimentPopover-B29fIHQz.js} +6 -6
- package/dist/ExperimentPopover-B29fIHQz.js.map +1 -0
- package/dist/{ExperimentPopover-D0bg_fqM.js → ExperimentPopover-gdSA9ZCF.js} +1 -1
- package/dist/{ExperimentSelectorModal-DeBE0YKT.js → ExperimentSelectorModal-CHsU-LIh.js} +3 -3
- package/dist/{ExperimentSelectorModal-DeBE0YKT.js.map → ExperimentSelectorModal-CHsU-LIh.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-BXf-XnQw.js → ExperimentSelectorModal-DIFyL5ta.js} +1 -1
- package/dist/__tests__/composables/useCommandHistory.test.d.ts +1 -0
- package/dist/__tests__/composables/useFileImport.test.d.ts +1 -0
- package/dist/__tests__/composables/useOptimisticMutation.test.d.ts +1 -0
- package/dist/__tests__/composables/useResourceCrud.test.d.ts +1 -0
- package/dist/__tests__/composables/useWellPainting.test.d.ts +1 -0
- package/dist/__tests__/composables/useWellPlateAdapter.test.d.ts +1 -0
- package/dist/__tests__/composables/useWellPlateValidation.test.d.ts +1 -0
- package/dist/components/index.js +3 -3
- package/dist/{components-CqdBz8DI.js → components-Dq02EVZH.js} +10 -10
- package/dist/components-Dq02EVZH.js.map +1 -0
- package/dist/composables/index.d.ts +7 -0
- package/dist/composables/index.js +5 -5
- package/dist/composables/useCommandHistory.d.ts +29 -0
- package/dist/composables/useFileImport.d.ts +35 -0
- package/dist/composables/useOptimisticMutation.d.ts +28 -0
- package/dist/composables/useResourceCrud.d.ts +40 -0
- package/dist/composables/useWellPainting.d.ts +35 -0
- package/dist/composables/useWellPlateAdapter.d.ts +36 -0
- package/dist/composables/useWellPlateValidation.d.ts +27 -0
- package/dist/{composables-BWh0MpcK.js → composables-D9mexHSW.js} +668 -5
- package/dist/composables-D9mexHSW.js.map +1 -0
- package/dist/{experiment-utils-hGXMHlAc.js → experiment-utils-D11yT3AR.js} +1 -2
- package/dist/{experiment-utils-hGXMHlAc.js.map → experiment-utils-D11yT3AR.js.map} +1 -1
- package/dist/index.js +8 -8
- package/dist/install.js +3 -3
- package/dist/styles.css +4 -8
- package/dist/{useExperimentSelector-B3hAGvL4.js → useExperimentSelector-BBaz0w51.js} +2 -2
- package/dist/{useExperimentSelector-B3hAGvL4.js.map → useExperimentSelector-BBaz0w51.js.map} +1 -1
- package/dist/{useProtocolTemplates-BJxS5F0_.js → useProtocolTemplates-DODHlhxr.js} +3 -3
- package/dist/{useProtocolTemplates-BJxS5F0_.js.map → useProtocolTemplates-DODHlhxr.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/ExperimentPopover.test.ts +23 -0
- package/src/__tests__/composables/experiment-utils.test.ts +2 -2
- package/src/__tests__/composables/useAppExperiment.test.ts +2 -1
- package/src/__tests__/composables/useCommandHistory.test.ts +43 -0
- package/src/__tests__/composables/useFileImport.test.ts +44 -0
- package/src/__tests__/composables/useOptimisticMutation.test.ts +40 -0
- package/src/__tests__/composables/useResourceCrud.test.ts +56 -0
- package/src/__tests__/composables/useWellPainting.test.ts +52 -0
- package/src/__tests__/composables/useWellPlateAdapter.test.ts +50 -0
- package/src/__tests__/composables/useWellPlateValidation.test.ts +42 -0
- package/src/components/ExperimentPopover.story.vue +3 -4
- package/src/components/ExperimentPopover.vue +6 -6
- package/src/components/PluginWorkspaceView.vue +3 -3
- package/src/composables/experiment-utils.ts +1 -1
- package/src/composables/index.ts +67 -0
- package/src/composables/useCommandHistory.ts +113 -0
- package/src/composables/useFileImport.ts +231 -0
- package/src/composables/useOptimisticMutation.ts +107 -0
- package/src/composables/useResourceCrud.ts +245 -0
- package/src/composables/useWellPainting.ts +187 -0
- package/src/composables/useWellPlateAdapter.ts +147 -0
- package/src/composables/useWellPlateValidation.ts +85 -0
- package/src/styles/components/experiment-popover.css +2 -4
- package/dist/ExperimentPopover-CCYB1oWp.js.map +0 -1
- package/dist/components-CqdBz8DI.js.map +0 -1
- package/dist/composables-BWh0MpcK.js.map +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { computed, ref, toValue, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
import type { WellPlateFormat } from '../types'
|
|
3
|
+
|
|
4
|
+
export type WellPlateSource<T> = T | Ref<T> | ComputedRef<T> | (() => T)
|
|
5
|
+
|
|
6
|
+
export interface WellCoordinate {
|
|
7
|
+
row: number
|
|
8
|
+
col: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WellPaintResult<TMode extends string = string, TPayload = unknown> {
|
|
12
|
+
wellIds: string[]
|
|
13
|
+
mode: TMode
|
|
14
|
+
payload?: TPayload
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseWellPaintingOptions<TMode extends string = string, TPayload = unknown> {
|
|
18
|
+
format: WellPlateSource<WellPlateFormat>
|
|
19
|
+
initialMode?: TMode
|
|
20
|
+
initialPayload?: TPayload
|
|
21
|
+
onComplete?: (result: WellPaintResult<TMode, TPayload>) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseWellPaintingReturn<TMode extends string = string, TPayload = unknown> {
|
|
25
|
+
activeMode: Ref<TMode>
|
|
26
|
+
activePayload: Ref<TPayload | undefined>
|
|
27
|
+
isDragging: Ref<boolean>
|
|
28
|
+
previewWellIds: ComputedRef<string[]>
|
|
29
|
+
setMode: (mode: TMode, payload?: TPayload) => void
|
|
30
|
+
setPayload: (payload: TPayload | undefined) => void
|
|
31
|
+
onWellMouseDown: (wellId: string) => void
|
|
32
|
+
onWellMouseMove: (wellId: string) => void
|
|
33
|
+
onWellMouseUp: () => void
|
|
34
|
+
cancel: () => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ROW_LABELS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
38
|
+
|
|
39
|
+
const PLATE_DIMENSIONS: Record<WellPlateFormat, { rows: number; cols: number }> = {
|
|
40
|
+
6: { rows: 2, cols: 3 },
|
|
41
|
+
12: { rows: 3, cols: 4 },
|
|
42
|
+
24: { rows: 4, cols: 6 },
|
|
43
|
+
48: { rows: 6, cols: 8 },
|
|
44
|
+
54: { rows: 6, cols: 9 },
|
|
45
|
+
96: { rows: 8, cols: 12 },
|
|
46
|
+
384: { rows: 16, cols: 24 },
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Pointer painting/rectangle selection state for WellPlate-based editors. */
|
|
50
|
+
export function useWellPainting<TMode extends string = string, TPayload = unknown>(
|
|
51
|
+
options: UseWellPaintingOptions<TMode, TPayload>,
|
|
52
|
+
): UseWellPaintingReturn<TMode, TPayload> {
|
|
53
|
+
const activeMode = ref<TMode>(options.initialMode ?? 'select' as TMode) as Ref<TMode>
|
|
54
|
+
const activePayload = ref<TPayload | undefined>(options.initialPayload) as Ref<TPayload | undefined>
|
|
55
|
+
const isDragging = ref(false)
|
|
56
|
+
const dragStart = ref<WellCoordinate | null>(null)
|
|
57
|
+
const dragEnd = ref<WellCoordinate | null>(null)
|
|
58
|
+
const paintTrail = ref<Set<string>>(new Set())
|
|
59
|
+
|
|
60
|
+
const previewWellIds = computed(() => {
|
|
61
|
+
if (!isDragging.value) return []
|
|
62
|
+
if (activeMode.value === 'select') {
|
|
63
|
+
if (!dragStart.value || !dragEnd.value) return []
|
|
64
|
+
return wellIdsInRectangle(dragStart.value, dragEnd.value, toValue(options.format))
|
|
65
|
+
}
|
|
66
|
+
return [...paintTrail.value]
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
function setMode(mode: TMode, payload?: TPayload): void {
|
|
70
|
+
activeMode.value = mode
|
|
71
|
+
activePayload.value = payload
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setPayload(payload: TPayload | undefined): void {
|
|
75
|
+
activePayload.value = payload
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onWellMouseDown(wellId: string): void {
|
|
79
|
+
const coord = wellIdToCoordinate(wellId)
|
|
80
|
+
if (!coord) return
|
|
81
|
+
isDragging.value = true
|
|
82
|
+
dragStart.value = coord
|
|
83
|
+
dragEnd.value = coord
|
|
84
|
+
paintTrail.value = new Set([wellId])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function onWellMouseMove(wellId: string): void {
|
|
88
|
+
if (!isDragging.value) return
|
|
89
|
+
const coord = wellIdToCoordinate(wellId)
|
|
90
|
+
if (!coord) return
|
|
91
|
+
dragEnd.value = coord
|
|
92
|
+
if (activeMode.value !== 'select') {
|
|
93
|
+
paintTrail.value = new Set([...paintTrail.value, wellId])
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function onWellMouseUp(): void {
|
|
98
|
+
if (!isDragging.value) return
|
|
99
|
+
const wellIds = previewWellIds.value
|
|
100
|
+
isDragging.value = false
|
|
101
|
+
if (wellIds.length > 0) {
|
|
102
|
+
options.onComplete?.({
|
|
103
|
+
wellIds,
|
|
104
|
+
mode: activeMode.value,
|
|
105
|
+
payload: activePayload.value,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
clearDragState()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cancel(): void {
|
|
112
|
+
isDragging.value = false
|
|
113
|
+
clearDragState()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function clearDragState(): void {
|
|
117
|
+
dragStart.value = null
|
|
118
|
+
dragEnd.value = null
|
|
119
|
+
paintTrail.value = new Set()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
activeMode,
|
|
124
|
+
activePayload,
|
|
125
|
+
isDragging,
|
|
126
|
+
previewWellIds,
|
|
127
|
+
setMode,
|
|
128
|
+
setPayload,
|
|
129
|
+
onWellMouseDown,
|
|
130
|
+
onWellMouseMove,
|
|
131
|
+
onWellMouseUp,
|
|
132
|
+
cancel,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function wellIdToCoordinate(wellId: string): WellCoordinate | null {
|
|
137
|
+
const match = /^([A-Z]+)(\d+)$/i.exec(wellId.trim())
|
|
138
|
+
if (!match) return null
|
|
139
|
+
return {
|
|
140
|
+
row: rowLabelToIndex(match[1].toUpperCase()),
|
|
141
|
+
col: Number(match[2]) - 1,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function coordinateToWellId(coord: WellCoordinate): string {
|
|
146
|
+
return `${indexToRowLabel(coord.row)}${coord.col + 1}`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function wellIdsInRectangle(
|
|
150
|
+
start: WellCoordinate,
|
|
151
|
+
end: WellCoordinate,
|
|
152
|
+
format: WellPlateFormat,
|
|
153
|
+
): string[] {
|
|
154
|
+
const dimensions = PLATE_DIMENSIONS[format]
|
|
155
|
+
const minRow = Math.max(0, Math.min(start.row, end.row))
|
|
156
|
+
const maxRow = Math.min(dimensions.rows - 1, Math.max(start.row, end.row))
|
|
157
|
+
const minCol = Math.max(0, Math.min(start.col, end.col))
|
|
158
|
+
const maxCol = Math.min(dimensions.cols - 1, Math.max(start.col, end.col))
|
|
159
|
+
const ids: string[] = []
|
|
160
|
+
|
|
161
|
+
for (let row = minRow; row <= maxRow; row++) {
|
|
162
|
+
for (let col = minCol; col <= maxCol; col++) {
|
|
163
|
+
ids.push(coordinateToWellId({ row, col }))
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return ids
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function rowLabelToIndex(label: string): number {
|
|
170
|
+
let result = 0
|
|
171
|
+
for (const char of label) {
|
|
172
|
+
result = result * 26 + (char.charCodeAt(0) - 64)
|
|
173
|
+
}
|
|
174
|
+
return result - 1
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function indexToRowLabel(index: number): string {
|
|
178
|
+
if (index < ROW_LABELS.length) return ROW_LABELS[index]
|
|
179
|
+
let value = index + 1
|
|
180
|
+
let label = ''
|
|
181
|
+
while (value > 0) {
|
|
182
|
+
value--
|
|
183
|
+
label = ROW_LABELS[value % 26] + label
|
|
184
|
+
value = Math.floor(value / 26)
|
|
185
|
+
}
|
|
186
|
+
return label
|
|
187
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { computed, toValue, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
import type { ColumnCondition, RowCondition, Well } from '../types'
|
|
3
|
+
|
|
4
|
+
export type WellPlateAdapterSource<T> =
|
|
5
|
+
| T
|
|
6
|
+
| Ref<T>
|
|
7
|
+
| ComputedRef<T>
|
|
8
|
+
| (() => T)
|
|
9
|
+
|
|
10
|
+
export interface WellPlateEntry {
|
|
11
|
+
wellId: string
|
|
12
|
+
state?: Well['state']
|
|
13
|
+
sampleType?: string
|
|
14
|
+
value?: number
|
|
15
|
+
metadata?: Record<string, unknown>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WellPlateAdapterOptions<TEntry> {
|
|
19
|
+
entries: WellPlateAdapterSource<readonly TEntry[]>
|
|
20
|
+
getWellId: (entry: TEntry) => string
|
|
21
|
+
getState?: (entry: TEntry) => Well['state'] | undefined
|
|
22
|
+
getSampleType?: (entry: TEntry) => string | undefined
|
|
23
|
+
getValue?: (entry: TEntry) => number | undefined
|
|
24
|
+
getMetadata?: (entry: TEntry) => Record<string, unknown> | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseWellPlateAdapterReturn {
|
|
28
|
+
wells: ComputedRef<Record<string, Partial<Well>>>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Convert domain rows into WellPlate's wells prop without hand-written adapters. */
|
|
32
|
+
export function useWellPlateAdapter<TEntry>(
|
|
33
|
+
options: WellPlateAdapterOptions<TEntry>,
|
|
34
|
+
): UseWellPlateAdapterReturn {
|
|
35
|
+
const wells = computed(() => createWellPlateWells(toValue(options.entries), options))
|
|
36
|
+
return { wells }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createWellPlateWells<TEntry>(
|
|
40
|
+
entries: readonly TEntry[],
|
|
41
|
+
options: Omit<WellPlateAdapterOptions<TEntry>, 'entries'>,
|
|
42
|
+
): Record<string, Partial<Well>> {
|
|
43
|
+
return Object.fromEntries(entries.map((entry) => {
|
|
44
|
+
const wellId = options.getWellId(entry)
|
|
45
|
+
const coord = parseWellId(wellId)
|
|
46
|
+
const well: Partial<Well> = {
|
|
47
|
+
id: wellId,
|
|
48
|
+
row: coord.row,
|
|
49
|
+
col: coord.col,
|
|
50
|
+
state: options.getState?.(entry) ?? 'filled',
|
|
51
|
+
sampleType: options.getSampleType?.(entry),
|
|
52
|
+
value: options.getValue?.(entry),
|
|
53
|
+
metadata: options.getMetadata?.(entry),
|
|
54
|
+
}
|
|
55
|
+
return [wellId, well]
|
|
56
|
+
}))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface AxisConditionEntry {
|
|
60
|
+
label: string
|
|
61
|
+
color: string
|
|
62
|
+
start: string | number
|
|
63
|
+
concentrations: readonly { value: number; replicates?: number }[]
|
|
64
|
+
unit?: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createColumnConditions(entries: readonly AxisConditionEntry[]): ColumnCondition[] {
|
|
68
|
+
return entries.map((entry) => {
|
|
69
|
+
const startCol = typeof entry.start === 'number'
|
|
70
|
+
? entry.start
|
|
71
|
+
: Number.parseInt(entry.start, 10)
|
|
72
|
+
const cols: number[] = []
|
|
73
|
+
const concentrations: number[] = []
|
|
74
|
+
|
|
75
|
+
for (const concentration of entry.concentrations) {
|
|
76
|
+
const replicates = concentration.replicates ?? 1
|
|
77
|
+
for (let index = 0; index < replicates; index++) {
|
|
78
|
+
cols.push(startCol + cols.length)
|
|
79
|
+
concentrations.push(concentration.value)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
label: entry.label,
|
|
85
|
+
color: entry.color,
|
|
86
|
+
unit: entry.unit,
|
|
87
|
+
cols,
|
|
88
|
+
concentrations,
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createRowConditions(entries: readonly AxisConditionEntry[]): RowCondition[] {
|
|
94
|
+
return entries.map((entry) => {
|
|
95
|
+
const startRow = typeof entry.start === 'number'
|
|
96
|
+
? entry.start
|
|
97
|
+
: rowLabelToIndex(entry.start)
|
|
98
|
+
const rows: string[] = []
|
|
99
|
+
const concentrations: number[] = []
|
|
100
|
+
|
|
101
|
+
for (const concentration of entry.concentrations) {
|
|
102
|
+
const replicates = concentration.replicates ?? 1
|
|
103
|
+
for (let index = 0; index < replicates; index++) {
|
|
104
|
+
rows.push(indexToRowLabel(startRow + rows.length))
|
|
105
|
+
concentrations.push(concentration.value)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
label: entry.label,
|
|
111
|
+
color: entry.color,
|
|
112
|
+
unit: entry.unit,
|
|
113
|
+
rows,
|
|
114
|
+
concentrations,
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseWellId(wellId: string): { row: number; col: number } {
|
|
120
|
+
const match = /^([A-Z]+)(\d+)$/i.exec(wellId.trim())
|
|
121
|
+
if (!match) {
|
|
122
|
+
throw new Error(`[MINT SDK] Invalid well id "${wellId}".`)
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
row: rowLabelToIndex(match[1].toUpperCase()),
|
|
126
|
+
col: Number(match[2]) - 1,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function rowLabelToIndex(label: string): number {
|
|
131
|
+
let result = 0
|
|
132
|
+
for (const char of label.toUpperCase()) {
|
|
133
|
+
result = result * 26 + (char.charCodeAt(0) - 64)
|
|
134
|
+
}
|
|
135
|
+
return result - 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function indexToRowLabel(index: number): string {
|
|
139
|
+
let value = index + 1
|
|
140
|
+
let label = ''
|
|
141
|
+
while (value > 0) {
|
|
142
|
+
value--
|
|
143
|
+
label = String.fromCharCode(65 + (value % 26)) + label
|
|
144
|
+
value = Math.floor(value / 26)
|
|
145
|
+
}
|
|
146
|
+
return label
|
|
147
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { computed, toValue, type ComputedRef, type Ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export type WellPlateValidationSeverity = 'error' | 'warning' | 'info'
|
|
4
|
+
|
|
5
|
+
export interface WellPlateValidationWarning<TType extends string = string> {
|
|
6
|
+
type: TType
|
|
7
|
+
severity: WellPlateValidationSeverity
|
|
8
|
+
message: string
|
|
9
|
+
affectedWells?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WellPlateValidationRule<TContext, TType extends string = string> {
|
|
13
|
+
type: TType
|
|
14
|
+
severity: WellPlateValidationSeverity
|
|
15
|
+
validate: (context: TContext) => false | string | Omit<WellPlateValidationWarning<TType>, 'type' | 'severity'>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type WellPlateValidationSource<T> =
|
|
19
|
+
| T
|
|
20
|
+
| Ref<T>
|
|
21
|
+
| ComputedRef<T>
|
|
22
|
+
| (() => T)
|
|
23
|
+
|
|
24
|
+
export interface UseWellPlateValidationOptions<TContext, TType extends string = string> {
|
|
25
|
+
context: WellPlateValidationSource<TContext>
|
|
26
|
+
rules: WellPlateValidationSource<readonly WellPlateValidationRule<TContext, TType>[]>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UseWellPlateValidationReturn<TType extends string = string> {
|
|
30
|
+
warnings: ComputedRef<WellPlateValidationWarning<TType>[]>
|
|
31
|
+
hasBlockingIssues: ComputedRef<boolean>
|
|
32
|
+
warningCount: ComputedRef<number>
|
|
33
|
+
errorCount: ComputedRef<number>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Rule-driven validation state for WellPlate layout/design screens. */
|
|
37
|
+
export function useWellPlateValidation<TContext, TType extends string = string>(
|
|
38
|
+
options: UseWellPlateValidationOptions<TContext, TType>,
|
|
39
|
+
): UseWellPlateValidationReturn<TType> {
|
|
40
|
+
const warnings = computed(() => runWellPlateValidation(
|
|
41
|
+
toValue(options.context),
|
|
42
|
+
toValue(options.rules),
|
|
43
|
+
))
|
|
44
|
+
const hasBlockingIssues = computed(() => warnings.value.some(warning => warning.severity === 'error'))
|
|
45
|
+
const warningCount = computed(() => warnings.value.length)
|
|
46
|
+
const errorCount = computed(() => warnings.value.filter(warning => warning.severity === 'error').length)
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
warnings,
|
|
50
|
+
hasBlockingIssues,
|
|
51
|
+
warningCount,
|
|
52
|
+
errorCount,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function runWellPlateValidation<TContext, TType extends string = string>(
|
|
57
|
+
context: TContext,
|
|
58
|
+
rules: readonly WellPlateValidationRule<TContext, TType>[],
|
|
59
|
+
): WellPlateValidationWarning<TType>[] {
|
|
60
|
+
const warnings: WellPlateValidationWarning<TType>[] = []
|
|
61
|
+
for (const rule of rules) {
|
|
62
|
+
const result = rule.validate(context)
|
|
63
|
+
if (!result) continue
|
|
64
|
+
warnings.push(normalizeWarning(rule, result))
|
|
65
|
+
}
|
|
66
|
+
return warnings
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeWarning<TContext, TType extends string>(
|
|
70
|
+
rule: WellPlateValidationRule<TContext, TType>,
|
|
71
|
+
result: string | Omit<WellPlateValidationWarning<TType>, 'type' | 'severity'>,
|
|
72
|
+
): WellPlateValidationWarning<TType> {
|
|
73
|
+
if (typeof result === 'string') {
|
|
74
|
+
return {
|
|
75
|
+
type: rule.type,
|
|
76
|
+
severity: rule.severity,
|
|
77
|
+
message: result,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
type: rule.type,
|
|
82
|
+
severity: rule.severity,
|
|
83
|
+
...result,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -20,9 +20,7 @@
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/* ─── Trigger (left / main chip) ───
|
|
23
|
-
Shows
|
|
24
|
-
compactness. The trigger is now sized to its content — no fixed max-width
|
|
25
|
-
is needed once the long name is gone. */
|
|
23
|
+
Shows the experiment name, truncated to preserve topbar density. */
|
|
26
24
|
.mint-experiment-popover__trigger {
|
|
27
25
|
display: inline-flex;
|
|
28
26
|
align-items: center;
|
|
@@ -99,7 +97,7 @@
|
|
|
99
97
|
text-align: left;
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
/* Experiment code in the trigger — monospace, tabular
|
|
100
|
+
/* Experiment code fallback in the trigger — monospace, tabular */
|
|
103
101
|
.mint-experiment-popover__trigger-code {
|
|
104
102
|
font-family: var(--font-mono, 'Fira Code', monospace);
|
|
105
103
|
font-size: 0.75rem;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ExperimentPopover-CCYB1oWp.js","names":["$slots"],"sources":["../src/components/ConfirmDialog.vue","../src/components/ConfirmDialog.vue","../src/components/ExperimentPopover.vue","../src/components/ExperimentPopover.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/** Confirm/cancel dialog with danger, warning, and info variants; blocks close while loading. */\nimport BaseModal from './BaseModal.vue'\n\ninterface Props {\n modelValue: boolean\n title?: string\n subtitle?: string\n message?: string\n variant?: 'danger' | 'warning' | 'info'\n confirmLabel?: string\n cancelLabel?: string\n loading?: boolean\n}\n\nwithDefaults(defineProps<Props>(), {\n title: 'Confirm',\n variant: 'danger',\n confirmLabel: 'Confirm',\n cancelLabel: 'Cancel',\n loading: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: boolean]\n confirm: []\n cancel: []\n}>()\n\nfunction handleCancel() {\n emit('update:modelValue', false)\n emit('cancel')\n}\n\nfunction handleConfirm() {\n emit('confirm')\n}\n</script>\n\n<template>\n <BaseModal\n :model-value=\"modelValue\"\n :title=\"title\"\n :subtitle=\"subtitle\"\n size=\"sm\"\n :closable=\"!loading\"\n :close-on-overlay=\"!loading\"\n :close-on-escape=\"!loading\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n >\n <div class=\"mint-confirm\">\n <!-- Icon is now opt-in via slot. Refresh design keeps the body focused on\n the message; variant intent is carried by the confirm button's color. -->\n <div\n v-if=\"$slots.icon\"\n :class=\"['mint-confirm__icon', `mint-confirm__icon--${variant}`]\"\n >\n <slot name=\"icon\" />\n </div>\n <p v-if=\"message\" class=\"mint-confirm__message\">{{ message }}</p>\n <slot />\n </div>\n\n <template #footer>\n <div class=\"mint-confirm__footer\">\n <button\n type=\"button\"\n class=\"mint-confirm__btn-cancel\"\n :disabled=\"loading\"\n @click=\"handleCancel\"\n >\n {{ cancelLabel }}\n </button>\n <button\n type=\"button\"\n :class=\"['mint-confirm__btn-confirm', `mint-confirm__btn-confirm--${variant}`]\"\n :disabled=\"loading\"\n @click=\"handleConfirm\"\n >\n <svg v-if=\"loading\" class=\"mint-confirm__btn-spinner\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle style=\"opacity: 0.25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path style=\"opacity: 0.75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\" />\n </svg>\n {{ confirmLabel }}\n </button>\n </div>\n </template>\n </BaseModal>\n</template>\n\n<style>\n@import '../styles/components/confirm-dialog.css';\n</style>\n","<script setup lang=\"ts\">\n/** Confirm/cancel dialog with danger, warning, and info variants; blocks close while loading. */\nimport BaseModal from './BaseModal.vue'\n\ninterface Props {\n modelValue: boolean\n title?: string\n subtitle?: string\n message?: string\n variant?: 'danger' | 'warning' | 'info'\n confirmLabel?: string\n cancelLabel?: string\n loading?: boolean\n}\n\nwithDefaults(defineProps<Props>(), {\n title: 'Confirm',\n variant: 'danger',\n confirmLabel: 'Confirm',\n cancelLabel: 'Cancel',\n loading: false,\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: boolean]\n confirm: []\n cancel: []\n}>()\n\nfunction handleCancel() {\n emit('update:modelValue', false)\n emit('cancel')\n}\n\nfunction handleConfirm() {\n emit('confirm')\n}\n</script>\n\n<template>\n <BaseModal\n :model-value=\"modelValue\"\n :title=\"title\"\n :subtitle=\"subtitle\"\n size=\"sm\"\n :closable=\"!loading\"\n :close-on-overlay=\"!loading\"\n :close-on-escape=\"!loading\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n >\n <div class=\"mint-confirm\">\n <!-- Icon is now opt-in via slot. Refresh design keeps the body focused on\n the message; variant intent is carried by the confirm button's color. -->\n <div\n v-if=\"$slots.icon\"\n :class=\"['mint-confirm__icon', `mint-confirm__icon--${variant}`]\"\n >\n <slot name=\"icon\" />\n </div>\n <p v-if=\"message\" class=\"mint-confirm__message\">{{ message }}</p>\n <slot />\n </div>\n\n <template #footer>\n <div class=\"mint-confirm__footer\">\n <button\n type=\"button\"\n class=\"mint-confirm__btn-cancel\"\n :disabled=\"loading\"\n @click=\"handleCancel\"\n >\n {{ cancelLabel }}\n </button>\n <button\n type=\"button\"\n :class=\"['mint-confirm__btn-confirm', `mint-confirm__btn-confirm--${variant}`]\"\n :disabled=\"loading\"\n @click=\"handleConfirm\"\n >\n <svg v-if=\"loading\" class=\"mint-confirm__btn-spinner\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle style=\"opacity: 0.25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\" />\n <path style=\"opacity: 0.75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\" />\n </svg>\n {{ confirmLabel }}\n </button>\n </div>\n </template>\n </BaseModal>\n</template>\n\n<style>\n@import '../styles/components/confirm-dialog.css';\n</style>\n","<script setup lang=\"ts\">\n/** Floating popover showing the active experiment with save, detach, and select actions. */\nimport { ref, watch, onUnmounted } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { formatExperimentStatus } from '../composables/experiment-utils'\nimport ConfirmDialog from './ConfirmDialog.vue'\n\ninterface Props {\n experimentName?: string\n experimentCode?: string\n experimentStatus?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n confirmSave?: boolean\n confirmTitle?: string\n confirmMessage?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n confirmSave: true,\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst { isOpen, rootRef: popoverRef, close, toggle } = useDropdownState({\n closeOnEscape: false,\n})\nconst showSuccess = ref(false)\nconst showConfirm = ref(false)\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n if (props.confirmSave) {\n showConfirm.value = true\n } else {\n emit('save')\n }\n}\n\nfunction handleConfirmSave() {\n showConfirm.value = false\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nlet successTimer: ReturnType<typeof setTimeout> | null = null\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (successTimer) clearTimeout(successTimer)\n if (msg) {\n showSuccess.value = true\n successTimer = setTimeout(() => {\n showSuccess.value = false\n successTimer = null\n }, 3000)\n }\n})\n\nonUnmounted(() => {\n if (successTimer) clearTimeout(successTimer)\n})\n</script>\n\n<template>\n <div ref=\"popoverRef\" class=\"mint-experiment-popover\">\n <!-- Split trigger: experiment pill + inline save -->\n <div\n :class=\"[\n 'mint-experiment-popover__split',\n { 'mint-experiment-popover__split--with-save': showSave && experimentName },\n ]\"\n >\n <!-- Left: experiment trigger (opens popover) — shows only the code for\n maximum topbar compactness; full name + status live in the panel. -->\n <button\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__trigger',\n { 'mint-experiment-popover__trigger--active': isOpen },\n { 'mint-experiment-popover__trigger--empty': !experimentCode && !experimentName },\n ]\"\n :title=\"experimentName || undefined\"\n @click.stop=\"toggle\"\n >\n <!-- Flask icon -->\n <svg class=\"mint-experiment-popover__trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <!-- Code preferred, name as fallback, \"No experiment\" as empty state -->\n <span v-if=\"experimentCode\" class=\"mint-experiment-popover__trigger-code\">{{ experimentCode }}</span>\n <span v-else-if=\"experimentName\" class=\"mint-experiment-popover__trigger-text\">{{ experimentName }}</span>\n <span v-else class=\"mint-experiment-popover__trigger-text\">No experiment</span>\n <!-- Chevron -->\n <svg class=\"mint-experiment-popover__trigger-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n\n <!-- Right: inline save button (direct action) -->\n <button\n v-if=\"showSave && experimentName\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__save-trigger',\n { 'mint-experiment-popover__save-trigger--loading': saveLoading },\n { 'mint-experiment-popover__save-trigger--success': showSuccess },\n { 'mint-experiment-popover__save-trigger--disabled': saveDisabled && !showSuccess },\n ]\"\n :disabled=\"saveDisabled && !showSuccess\"\n :title=\"saveDisabled && saveDisabledMessage ? saveDisabledMessage : showSuccess && saveSuccessMessage ? saveSuccessMessage : 'Save to Experiment'\"\n @click.stop=\"handleSave\"\n >\n <!-- Loading spinner -->\n <span v-if=\"saveLoading\" class=\"mint-experiment-popover__spinner--inline\" />\n <!-- Success check -->\n <svg v-else-if=\"showSuccess\" class=\"mint-experiment-popover__save-trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2.5\" d=\"M5 13l4 4L19 7\" />\n </svg>\n <!-- Save icon -->\n <svg v-else class=\"mint-experiment-popover__save-trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4\" />\n </svg>\n </button>\n </div>\n\n <!-- Popover panel -->\n <div v-if=\"isOpen\" class=\"mint-experiment-popover__panel\">\n <!-- Header -->\n <div class=\"mint-experiment-popover__header\">\n <div class=\"mint-experiment-popover__title\">Experiment</div>\n <div class=\"mint-experiment-popover__subtitle\">\n {{ experimentName ? 'Linked experiment context' : 'Link to an MINT experiment' }}\n </div>\n </div>\n\n <!-- No experiment selected -->\n <div v-if=\"!experimentName\" class=\"mint-experiment-popover__body\">\n <button type=\"button\" class=\"mint-experiment-popover__select-btn\" @click=\"handleSelect\">\n <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\" />\n </svg>\n Select Experiment\n </button>\n </div>\n\n <!-- Experiment selected -->\n <div v-else class=\"mint-experiment-popover__body\">\n <div class=\"mint-experiment-popover__card\">\n <div class=\"mint-experiment-popover__card-icon\">\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n </div>\n <div class=\"mint-experiment-popover__card-info\">\n <span v-if=\"experimentCode\" class=\"mint-experiment-popover__card-code\">{{ experimentCode }}</span>\n <div class=\"mint-experiment-popover__card-name\">{{ experimentName }}</div>\n <div v-if=\"experimentStatus\" class=\"mint-experiment-popover__card-status\">\n {{ formatExperimentStatus(experimentStatus) }}\n </div>\n </div>\n </div>\n <div class=\"mint-experiment-popover__card-actions\">\n <button type=\"button\" class=\"mint-experiment-popover__change-btn\" @click=\"handleSelect\">\n Change\n </button>\n <button v-if=\"showDetach\" type=\"button\" class=\"mint-experiment-popover__detach-btn\" @click=\"handleDetach\">\n Detach\n </button>\n </div>\n </div>\n </div>\n\n <!-- Save confirmation dialog -->\n <ConfirmDialog\n v-model=\"showConfirm\"\n :title=\"confirmTitle ?? 'Save to Experiment'\"\n :message=\"confirmMessage ?? `Save current data to ${experimentName}?`\"\n variant=\"info\"\n confirm-label=\"Save\"\n :loading=\"saveLoading\"\n @confirm=\"handleConfirmSave\"\n />\n </div>\n</template>\n\n<style>\n@import '../styles/components/experiment-popover.css';\n</style>\n","<script setup lang=\"ts\">\n/** Floating popover showing the active experiment with save, detach, and select actions. */\nimport { ref, watch, onUnmounted } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { formatExperimentStatus } from '../composables/experiment-utils'\nimport ConfirmDialog from './ConfirmDialog.vue'\n\ninterface Props {\n experimentName?: string\n experimentCode?: string\n experimentStatus?: string\n showSave?: boolean\n showDetach?: boolean\n saveDisabled?: boolean\n saveLoading?: boolean\n saveSuccessMessage?: string\n saveDisabledMessage?: string\n confirmSave?: boolean\n confirmTitle?: string\n confirmMessage?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n showSave: false,\n showDetach: false,\n saveDisabled: false,\n saveLoading: false,\n confirmSave: true,\n})\n\nconst emit = defineEmits<{\n select: []\n save: []\n detach: []\n}>()\n\nconst { isOpen, rootRef: popoverRef, close, toggle } = useDropdownState({\n closeOnEscape: false,\n})\nconst showSuccess = ref(false)\nconst showConfirm = ref(false)\n\nfunction handleSelect() {\n emit('select')\n close()\n}\n\nfunction handleSave() {\n if (props.saveDisabled || props.saveLoading) return\n if (props.confirmSave) {\n showConfirm.value = true\n } else {\n emit('save')\n }\n}\n\nfunction handleConfirmSave() {\n showConfirm.value = false\n emit('save')\n}\n\nfunction handleDetach() {\n emit('detach')\n close()\n}\n\nlet successTimer: ReturnType<typeof setTimeout> | null = null\n\n// Show success state when saveSuccessMessage changes from empty to a value\nwatch(() => props.saveSuccessMessage, (msg) => {\n if (successTimer) clearTimeout(successTimer)\n if (msg) {\n showSuccess.value = true\n successTimer = setTimeout(() => {\n showSuccess.value = false\n successTimer = null\n }, 3000)\n }\n})\n\nonUnmounted(() => {\n if (successTimer) clearTimeout(successTimer)\n})\n</script>\n\n<template>\n <div ref=\"popoverRef\" class=\"mint-experiment-popover\">\n <!-- Split trigger: experiment pill + inline save -->\n <div\n :class=\"[\n 'mint-experiment-popover__split',\n { 'mint-experiment-popover__split--with-save': showSave && experimentName },\n ]\"\n >\n <!-- Left: experiment trigger (opens popover) — shows only the code for\n maximum topbar compactness; full name + status live in the panel. -->\n <button\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__trigger',\n { 'mint-experiment-popover__trigger--active': isOpen },\n { 'mint-experiment-popover__trigger--empty': !experimentCode && !experimentName },\n ]\"\n :title=\"experimentName || undefined\"\n @click.stop=\"toggle\"\n >\n <!-- Flask icon -->\n <svg class=\"mint-experiment-popover__trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n <!-- Code preferred, name as fallback, \"No experiment\" as empty state -->\n <span v-if=\"experimentCode\" class=\"mint-experiment-popover__trigger-code\">{{ experimentCode }}</span>\n <span v-else-if=\"experimentName\" class=\"mint-experiment-popover__trigger-text\">{{ experimentName }}</span>\n <span v-else class=\"mint-experiment-popover__trigger-text\">No experiment</span>\n <!-- Chevron -->\n <svg class=\"mint-experiment-popover__trigger-chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </button>\n\n <!-- Right: inline save button (direct action) -->\n <button\n v-if=\"showSave && experimentName\"\n type=\"button\"\n :class=\"[\n 'mint-experiment-popover__save-trigger',\n { 'mint-experiment-popover__save-trigger--loading': saveLoading },\n { 'mint-experiment-popover__save-trigger--success': showSuccess },\n { 'mint-experiment-popover__save-trigger--disabled': saveDisabled && !showSuccess },\n ]\"\n :disabled=\"saveDisabled && !showSuccess\"\n :title=\"saveDisabled && saveDisabledMessage ? saveDisabledMessage : showSuccess && saveSuccessMessage ? saveSuccessMessage : 'Save to Experiment'\"\n @click.stop=\"handleSave\"\n >\n <!-- Loading spinner -->\n <span v-if=\"saveLoading\" class=\"mint-experiment-popover__spinner--inline\" />\n <!-- Success check -->\n <svg v-else-if=\"showSuccess\" class=\"mint-experiment-popover__save-trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2.5\" d=\"M5 13l4 4L19 7\" />\n </svg>\n <!-- Save icon -->\n <svg v-else class=\"mint-experiment-popover__save-trigger-icon\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4\" />\n </svg>\n </button>\n </div>\n\n <!-- Popover panel -->\n <div v-if=\"isOpen\" class=\"mint-experiment-popover__panel\">\n <!-- Header -->\n <div class=\"mint-experiment-popover__header\">\n <div class=\"mint-experiment-popover__title\">Experiment</div>\n <div class=\"mint-experiment-popover__subtitle\">\n {{ experimentName ? 'Linked experiment context' : 'Link to an MINT experiment' }}\n </div>\n </div>\n\n <!-- No experiment selected -->\n <div v-if=\"!experimentName\" class=\"mint-experiment-popover__body\">\n <button type=\"button\" class=\"mint-experiment-popover__select-btn\" @click=\"handleSelect\">\n <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\" />\n </svg>\n Select Experiment\n </button>\n </div>\n\n <!-- Experiment selected -->\n <div v-else class=\"mint-experiment-popover__body\">\n <div class=\"mint-experiment-popover__card\">\n <div class=\"mint-experiment-popover__card-icon\">\n <svg fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n stroke-width=\"1.75\"\n d=\"M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z\"\n />\n </svg>\n </div>\n <div class=\"mint-experiment-popover__card-info\">\n <span v-if=\"experimentCode\" class=\"mint-experiment-popover__card-code\">{{ experimentCode }}</span>\n <div class=\"mint-experiment-popover__card-name\">{{ experimentName }}</div>\n <div v-if=\"experimentStatus\" class=\"mint-experiment-popover__card-status\">\n {{ formatExperimentStatus(experimentStatus) }}\n </div>\n </div>\n </div>\n <div class=\"mint-experiment-popover__card-actions\">\n <button type=\"button\" class=\"mint-experiment-popover__change-btn\" @click=\"handleSelect\">\n Change\n </button>\n <button v-if=\"showDetach\" type=\"button\" class=\"mint-experiment-popover__detach-btn\" @click=\"handleDetach\">\n Detach\n </button>\n </div>\n </div>\n </div>\n\n <!-- Save confirmation dialog -->\n <ConfirmDialog\n v-model=\"showConfirm\"\n :title=\"confirmTitle ?? 'Save to Experiment'\"\n :message=\"confirmMessage ?? `Save current data to ${experimentName}?`\"\n variant=\"info\"\n confirm-label=\"Save\"\n :loading=\"saveLoading\"\n @confirm=\"handleConfirmSave\"\n />\n </div>\n</template>\n\n<style>\n@import '../styles/components/experiment-popover.css';\n</style>\n"],"mappings":"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"}
|