@morscherlab/mint-sdk 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/__tests__/composables/useCommandHistory.test.d.ts +1 -0
  2. package/dist/__tests__/composables/useFileImport.test.d.ts +1 -0
  3. package/dist/__tests__/composables/useOptimisticMutation.test.d.ts +1 -0
  4. package/dist/__tests__/composables/useResourceCrud.test.d.ts +1 -0
  5. package/dist/__tests__/composables/useWellPainting.test.d.ts +1 -0
  6. package/dist/__tests__/composables/useWellPlateAdapter.test.d.ts +1 -0
  7. package/dist/__tests__/composables/useWellPlateValidation.test.d.ts +1 -0
  8. package/dist/components/index.js +1 -1
  9. package/dist/{components-CzFtQExv.js → components-Cyk8QEyL.js} +81 -75
  10. package/dist/components-Cyk8QEyL.js.map +1 -0
  11. package/dist/composables/index.d.ts +7 -0
  12. package/dist/composables/index.js +2 -2
  13. package/dist/composables/useCommandHistory.d.ts +29 -0
  14. package/dist/composables/useFileImport.d.ts +35 -0
  15. package/dist/composables/useOptimisticMutation.d.ts +28 -0
  16. package/dist/composables/useResourceCrud.d.ts +40 -0
  17. package/dist/composables/useWellPainting.d.ts +35 -0
  18. package/dist/composables/useWellPlateAdapter.d.ts +36 -0
  19. package/dist/composables/useWellPlateValidation.d.ts +27 -0
  20. package/dist/{composables-BuG5yAb7.js → composables-D9mexHSW.js} +666 -3
  21. package/dist/composables-D9mexHSW.js.map +1 -0
  22. package/dist/index.js +3 -3
  23. package/dist/install.js +1 -1
  24. package/package.json +1 -1
  25. package/src/__tests__/components/SampleSelector.test.ts +6 -3
  26. package/src/__tests__/components/SampleSelectorSampleRow.test.ts +5 -2
  27. package/src/__tests__/composables/useCommandHistory.test.ts +43 -0
  28. package/src/__tests__/composables/useFileImport.test.ts +44 -0
  29. package/src/__tests__/composables/useOptimisticMutation.test.ts +40 -0
  30. package/src/__tests__/composables/useResourceCrud.test.ts +56 -0
  31. package/src/__tests__/composables/useWellPainting.test.ts +52 -0
  32. package/src/__tests__/composables/useWellPlateAdapter.test.ts +50 -0
  33. package/src/__tests__/composables/useWellPlateValidation.test.ts +42 -0
  34. package/src/components/PluginWorkspaceView.vue +3 -3
  35. package/src/components/SampleSelector.vue +2 -0
  36. package/src/components/SampleSelectorSampleRow.vue +2 -0
  37. package/src/composables/index.ts +67 -0
  38. package/src/composables/useCommandHistory.ts +113 -0
  39. package/src/composables/useFileImport.ts +231 -0
  40. package/src/composables/useOptimisticMutation.ts +107 -0
  41. package/src/composables/useResourceCrud.ts +245 -0
  42. package/src/composables/useWellPainting.ts +187 -0
  43. package/src/composables/useWellPlateAdapter.ts +147 -0
  44. package/src/composables/useWellPlateValidation.ts +85 -0
  45. package/dist/components-CzFtQExv.js.map +0 -1
  46. package/dist/composables-BuG5yAb7.js.map +0 -1
@@ -0,0 +1,107 @@
1
+ import { computed, ref, shallowRef, type ComputedRef, type Ref, type ShallowRef } from 'vue'
2
+
3
+ export interface OptimisticMutationContext<TVariables, TSnapshot> {
4
+ variables: TVariables
5
+ snapshot: TSnapshot | undefined
6
+ }
7
+
8
+ export interface UseOptimisticMutationOptions<TVariables, TData, TSnapshot = unknown> {
9
+ mutation: (variables: TVariables) => Promise<TData>
10
+ snapshot?: (variables: TVariables) => TSnapshot
11
+ apply?: (variables: TVariables, snapshot: TSnapshot | undefined) => void
12
+ commit?: (data: TData, context: OptimisticMutationContext<TVariables, TSnapshot>) => void
13
+ rollback?: (
14
+ snapshot: TSnapshot | undefined,
15
+ variables: TVariables,
16
+ error: unknown,
17
+ ) => void
18
+ onSuccess?: (data: TData, context: OptimisticMutationContext<TVariables, TSnapshot>) => void
19
+ onError?: (error: unknown, context: OptimisticMutationContext<TVariables, TSnapshot>) => void
20
+ onSettled?: (context: OptimisticMutationContext<TVariables, TSnapshot>) => void
21
+ readErrorMessage?: (error: unknown) => string
22
+ }
23
+
24
+ export interface UseOptimisticMutationReturn<TVariables, TData, TSnapshot = unknown> {
25
+ loading: ShallowRef<boolean>
26
+ error: Ref<string | null>
27
+ data: Ref<TData | null>
28
+ lastSnapshot: Ref<TSnapshot | undefined>
29
+ isIdle: ComputedRef<boolean>
30
+ run: (variables: TVariables) => Promise<TData>
31
+ clearError: () => void
32
+ reset: () => void
33
+ }
34
+
35
+ /** Run async mutations with optional optimistic local updates and rollback. */
36
+ export function useOptimisticMutation<TVariables, TData, TSnapshot = unknown>(
37
+ options: UseOptimisticMutationOptions<TVariables, TData, TSnapshot>,
38
+ ): UseOptimisticMutationReturn<TVariables, TData, TSnapshot> {
39
+ const loading = shallowRef(false)
40
+ const error = ref<string | null>(null)
41
+ const data = ref<TData | null>(null) as Ref<TData | null>
42
+ const lastSnapshot = ref<TSnapshot | undefined>(undefined) as Ref<TSnapshot | undefined>
43
+ const isIdle = computed(() => !loading.value && data.value === null && error.value === null)
44
+
45
+ function clearError(): void {
46
+ error.value = null
47
+ }
48
+
49
+ function reset(): void {
50
+ loading.value = false
51
+ error.value = null
52
+ data.value = null
53
+ lastSnapshot.value = undefined
54
+ }
55
+
56
+ async function run(variables: TVariables): Promise<TData> {
57
+ const snapshot = options.snapshot?.(variables)
58
+ const context: OptimisticMutationContext<TVariables, TSnapshot> = { variables, snapshot }
59
+
60
+ loading.value = true
61
+ error.value = null
62
+ lastSnapshot.value = snapshot
63
+ options.apply?.(variables, snapshot)
64
+
65
+ try {
66
+ const result = await options.mutation(variables)
67
+ data.value = result
68
+ options.commit?.(result, context)
69
+ options.onSuccess?.(result, context)
70
+ return result
71
+ } catch (err) {
72
+ options.rollback?.(snapshot, variables, err)
73
+ error.value = options.readErrorMessage?.(err) ?? readErrorMessage(err)
74
+ options.onError?.(err, context)
75
+ throw err
76
+ } finally {
77
+ loading.value = false
78
+ options.onSettled?.(context)
79
+ }
80
+ }
81
+
82
+ return {
83
+ loading,
84
+ error,
85
+ data,
86
+ lastSnapshot,
87
+ isIdle,
88
+ run,
89
+ clearError,
90
+ reset,
91
+ }
92
+ }
93
+
94
+ function readErrorMessage(value: unknown): string {
95
+ if (value instanceof Error && value.message) return value.message
96
+ if (typeof value === 'string' && value.trim()) return value
97
+ if (
98
+ typeof value === 'object'
99
+ && value !== null
100
+ && 'message' in value
101
+ && typeof (value as { message?: unknown }).message === 'string'
102
+ && (value as { message: string }).message.trim()
103
+ ) {
104
+ return (value as { message: string }).message
105
+ }
106
+ return 'Request failed.'
107
+ }
@@ -0,0 +1,245 @@
1
+ import { computed, ref, shallowRef, type ComputedRef, type Ref, type ShallowRef } from 'vue'
2
+
3
+ export type ResourceKey = string | number
4
+
5
+ export interface ResourceCrudAdapter<TItem, TCreate = Partial<TItem>, TUpdate = Partial<TItem>, TKey extends ResourceKey = ResourceKey> {
6
+ list: () => Promise<TItem[]>
7
+ create?: (payload: TCreate) => Promise<TItem>
8
+ update?: (key: TKey, payload: TUpdate) => Promise<TItem>
9
+ remove?: (key: TKey) => Promise<void>
10
+ }
11
+
12
+ export interface UseResourceCrudOptions<
13
+ TItem,
14
+ TCreate = Partial<TItem>,
15
+ TUpdate = Partial<TItem>,
16
+ TKey extends ResourceKey = ResourceKey,
17
+ > {
18
+ adapter: ResourceCrudAdapter<TItem, TCreate, TUpdate, TKey>
19
+ initialItems?: readonly TItem[]
20
+ getKey?: (item: TItem) => TKey
21
+ readErrorMessage?: (error: unknown) => string
22
+ }
23
+
24
+ export interface UseResourceCrudReturn<TItem, TCreate, TUpdate, TKey extends ResourceKey> {
25
+ items: Ref<TItem[]>
26
+ loading: ShallowRef<boolean>
27
+ saving: ShallowRef<boolean>
28
+ deleting: ShallowRef<boolean>
29
+ error: Ref<string | null>
30
+ isBusy: ComputedRef<boolean>
31
+ load: () => Promise<TItem[]>
32
+ createItem: (payload: TCreate) => Promise<TItem>
33
+ updateItem: (key: TKey, payload: TUpdate) => Promise<TItem>
34
+ deleteItem: (keyOrItem: TKey | TItem) => Promise<void>
35
+ setItems: (items: readonly TItem[]) => void
36
+ upsertLocal: (item: TItem) => void
37
+ removeLocal: (keyOrItem: TKey | TItem) => void
38
+ clearError: () => void
39
+ }
40
+
41
+ /** Common list/create/update/delete state wrapper for generated plugin clients. */
42
+ export function useResourceCrud<
43
+ TItem,
44
+ TCreate = Partial<TItem>,
45
+ TUpdate = Partial<TItem>,
46
+ TKey extends ResourceKey = ResourceKey,
47
+ >(
48
+ options: UseResourceCrudOptions<TItem, TCreate, TUpdate, TKey>,
49
+ ): UseResourceCrudReturn<TItem, TCreate, TUpdate, TKey> {
50
+ const items = ref<TItem[]>([...(options.initialItems ?? [])]) as Ref<TItem[]>
51
+ const loading = shallowRef(false)
52
+ const saving = shallowRef(false)
53
+ const deleting = shallowRef(false)
54
+ const error = ref<string | null>(null)
55
+ const isBusy = computed(() => loading.value || saving.value || deleting.value)
56
+
57
+ const readMessage = options.readErrorMessage ?? readErrorMessage
58
+ const getKey = options.getKey ?? defaultGetKey<TKey, TItem>
59
+
60
+ function clearError(): void {
61
+ error.value = null
62
+ }
63
+
64
+ function setError(err: unknown): void {
65
+ error.value = readMessage(err)
66
+ }
67
+
68
+ function setItems(nextItems: readonly TItem[]): void {
69
+ items.value = [...nextItems]
70
+ }
71
+
72
+ function upsertLocal(item: TItem): void {
73
+ const key = getKey(item)
74
+ const index = items.value.findIndex(existing => getKey(existing) === key)
75
+ if (index === -1) {
76
+ items.value = [...items.value, item]
77
+ return
78
+ }
79
+ items.value = [
80
+ ...items.value.slice(0, index),
81
+ item,
82
+ ...items.value.slice(index + 1),
83
+ ]
84
+ }
85
+
86
+ function removeLocal(keyOrItem: TKey | TItem): void {
87
+ const key = resolveKey(keyOrItem, getKey)
88
+ items.value = items.value.filter(item => getKey(item) !== key)
89
+ }
90
+
91
+ async function load(): Promise<TItem[]> {
92
+ loading.value = true
93
+ clearError()
94
+ try {
95
+ const result = await options.adapter.list()
96
+ setItems(result)
97
+ return result
98
+ } catch (err) {
99
+ setError(err)
100
+ throw err
101
+ } finally {
102
+ loading.value = false
103
+ }
104
+ }
105
+
106
+ async function createItem(payload: TCreate): Promise<TItem> {
107
+ if (!options.adapter.create) {
108
+ throw new Error('[MINT SDK] Resource adapter does not implement create().')
109
+ }
110
+ saving.value = true
111
+ clearError()
112
+ try {
113
+ const result = await options.adapter.create(payload)
114
+ upsertLocal(result)
115
+ return result
116
+ } catch (err) {
117
+ setError(err)
118
+ throw err
119
+ } finally {
120
+ saving.value = false
121
+ }
122
+ }
123
+
124
+ async function updateItem(key: TKey, payload: TUpdate): Promise<TItem> {
125
+ if (!options.adapter.update) {
126
+ throw new Error('[MINT SDK] Resource adapter does not implement update().')
127
+ }
128
+ saving.value = true
129
+ clearError()
130
+ try {
131
+ const result = await options.adapter.update(key, payload)
132
+ upsertLocal(result)
133
+ return result
134
+ } catch (err) {
135
+ setError(err)
136
+ throw err
137
+ } finally {
138
+ saving.value = false
139
+ }
140
+ }
141
+
142
+ async function deleteItem(keyOrItem: TKey | TItem): Promise<void> {
143
+ if (!options.adapter.remove) {
144
+ throw new Error('[MINT SDK] Resource adapter does not implement remove().')
145
+ }
146
+ const key = resolveKey(keyOrItem, getKey)
147
+ deleting.value = true
148
+ clearError()
149
+ try {
150
+ await options.adapter.remove(key)
151
+ removeLocal(key)
152
+ } catch (err) {
153
+ setError(err)
154
+ throw err
155
+ } finally {
156
+ deleting.value = false
157
+ }
158
+ }
159
+
160
+ return {
161
+ items,
162
+ loading,
163
+ saving,
164
+ deleting,
165
+ error,
166
+ isBusy,
167
+ load,
168
+ createItem,
169
+ updateItem,
170
+ deleteItem,
171
+ setItems,
172
+ upsertLocal,
173
+ removeLocal,
174
+ clearError,
175
+ }
176
+ }
177
+
178
+ export interface PluginResourceClientOptions<
179
+ TClient,
180
+ TItem,
181
+ TCreate = Partial<TItem>,
182
+ TUpdate = Partial<TItem>,
183
+ TKey extends ResourceKey = ResourceKey,
184
+ > {
185
+ client: TClient
186
+ list: (client: TClient) => Promise<TItem[]>
187
+ create?: (client: TClient, payload: TCreate) => Promise<TItem>
188
+ update?: (client: TClient, key: TKey, payload: TUpdate) => Promise<TItem>
189
+ remove?: (client: TClient, key: TKey) => Promise<void>
190
+ }
191
+
192
+ export function createPluginResourceClient<
193
+ TClient,
194
+ TItem,
195
+ TCreate = Partial<TItem>,
196
+ TUpdate = Partial<TItem>,
197
+ TKey extends ResourceKey = ResourceKey,
198
+ >(
199
+ options: PluginResourceClientOptions<TClient, TItem, TCreate, TUpdate, TKey>,
200
+ ): ResourceCrudAdapter<TItem, TCreate, TUpdate, TKey> {
201
+ return {
202
+ list: () => options.list(options.client),
203
+ create: options.create
204
+ ? payload => options.create!(options.client, payload)
205
+ : undefined,
206
+ update: options.update
207
+ ? (key, payload) => options.update!(options.client, key, payload)
208
+ : undefined,
209
+ remove: options.remove
210
+ ? key => options.remove!(options.client, key)
211
+ : undefined,
212
+ }
213
+ }
214
+
215
+ function defaultGetKey<TKey extends ResourceKey, TItem>(item: TItem): TKey {
216
+ if (item && typeof item === 'object' && 'id' in item) {
217
+ return (item as { id: ResourceKey }).id as TKey
218
+ }
219
+ throw new Error('[MINT SDK] Resource items need an id field or a getKey option.')
220
+ }
221
+
222
+ function resolveKey<TItem, TKey extends ResourceKey>(
223
+ keyOrItem: TKey | TItem,
224
+ getKey: (item: TItem) => TKey,
225
+ ): TKey {
226
+ if (typeof keyOrItem === 'string' || typeof keyOrItem === 'number') {
227
+ return keyOrItem as TKey
228
+ }
229
+ return getKey(keyOrItem as TItem)
230
+ }
231
+
232
+ function readErrorMessage(value: unknown): string {
233
+ if (value instanceof Error && value.message) return value.message
234
+ if (typeof value === 'string' && value.trim()) return value
235
+ if (
236
+ typeof value === 'object'
237
+ && value !== null
238
+ && 'message' in value
239
+ && typeof (value as { message?: unknown }).message === 'string'
240
+ && (value as { message: string }).message.trim()
241
+ ) {
242
+ return (value as { message: string }).message
243
+ }
244
+ return 'Request failed.'
245
+ }
@@ -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
+ }