Skip to content

Commit d3bc908

Browse files
committed
Merge branch 'tecan-deck-view' into beta
2 parents 23a92c3 + 1377872 commit d3bc908

4 files changed

Lines changed: 109 additions & 19 deletions

File tree

src/TecanDeckView/EmptyLabware.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export function EmptyLabware({ shortLabel }: { shortLabel: string }) {
1313
borderRadius: '2px',
1414
padding: '8px 4px',
1515
backgroundColor: PALETTE.gray1,
16-
opacity: 0.4,
17-
fontSize: '8px',
16+
opacity: 0.7,
17+
fontSize: '9px',
1818
fontWeight: 500,
1919
color: PALETTE.gray6,
2020
whiteSpace: 'nowrap',

src/TecanDeckView/TecanDeckView.tsx

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { PALETTE } from '../theme';
55
import { EmptyLabware } from './EmptyLabware';
66
import { LabwareDetailItem } from './LabwareDetailItem';
77
import {
8+
getFilledLabwaresByColumn,
9+
getFilledLabwaresByRow,
810
GRID_LAYOUT,
911
isLeftColumn,
1012
isRightColumn,
@@ -36,11 +38,9 @@ const CONTAINER_STYLE = {
3638

3739
const GRID_CONTAINER_STYLE = {
3840
display: 'grid',
39-
gap: '8px',
4041
gridTemplateAreas: GRID_LAYOUT.map((row) => `"${row.join(' ')}"`).join(
4142
'\n ',
4243
),
43-
gridTemplateRows: 'auto auto auto',
4444
padding: '8px',
4545
backgroundColor: PALETTE.gray1,
4646
borderRadius: '4px',
@@ -68,6 +68,42 @@ const RIGHT_COLUMN_STYLE = {
6868
gridArea: 'rightColumn',
6969
} as const;
7070

71+
// Helper functions for dynamic grid sizing
72+
73+
/**
74+
* Calculates dynamic grid-template-columns based on which columns have content.
75+
* Empty columns get minimal width (40-60px), filled columns get 1fr.
76+
*/
77+
function calculateGridTemplateColumns(labwares: TecanLabwares): string {
78+
const filledByColumn = getFilledLabwaresByColumn(labwares);
79+
80+
const columnSizes = [
81+
'auto', // Left column (mmPlate)
82+
(filledByColumn[1]?.length ?? 0) > 0 ? '1fr' : 'minmax(40px, 60px)', // Column 1
83+
(filledByColumn[2]?.length ?? 0) > 0 ? '1fr' : 'minmax(40px, 60px)', // Column 2
84+
(filledByColumn[3]?.length ?? 0) > 0 ? '1fr' : 'minmax(40px, 60px)', // Column 3
85+
'auto', // Right column (destPcr1/2)
86+
];
87+
88+
return columnSizes.join(' ');
89+
}
90+
91+
/**
92+
* Calculates dynamic grid-template-rows based on which rows have content.
93+
* Empty rows get minimal height (40-60px), filled rows get auto.
94+
*/
95+
function calculateGridTemplateRows(labwares: TecanLabwares): string {
96+
const filledByRow = getFilledLabwaresByRow(labwares);
97+
98+
const rowSizes = [
99+
(filledByRow[0]?.length ?? 0) > 0 ? 'auto' : 'minmax(40px, 60px)', // Row 0
100+
(filledByRow[1]?.length ?? 0) > 0 ? 'auto' : 'minmax(40px, 60px)', // Row 1
101+
(filledByRow[2]?.length ?? 0) > 0 ? 'auto' : 'minmax(40px, 60px)', // Row 2
102+
];
103+
104+
return rowSizes.join(' ');
105+
}
106+
71107
export function TecanDeckView({ labwares }: { labwares: TecanLabwares }) {
72108
const containerRef = React.useRef<HTMLDivElement>(null);
73109
const [availableWidth, setAvailableWidth] = React.useState<number>();
@@ -106,6 +142,16 @@ export function TecanDeckView({ labwares }: { labwares: TecanLabwares }) {
106142
setScaleFactor(calculatedScale);
107143
}, [availableWidth]);
108144

145+
// Calculate dynamic grid styling based on content
146+
const gridStyle = React.useMemo(
147+
() => ({
148+
gridTemplateColumns: calculateGridTemplateColumns(labwares),
149+
gridTemplateRows: calculateGridTemplateRows(labwares),
150+
gap: '8px',
151+
}),
152+
[labwares],
153+
);
154+
109155
// Group labwares into spanning columns and main grid in a single pass
110156
const { leftColumnLabwares, rightColumnLabwares, mainGridLabwares } =
111157
React.useMemo(() => {
@@ -121,7 +167,7 @@ export function TecanDeckView({ labwares }: { labwares: TecanLabwares }) {
121167
style: {
122168
...LABWARE_ITEM_BASE_STYLE,
123169
gridArea: key,
124-
alignSelf: metadata.gridPosition.alignment,
170+
alignSelf: 'center',
125171
},
126172
};
127173

@@ -159,6 +205,7 @@ export function TecanDeckView({ labwares }: { labwares: TecanLabwares }) {
159205
<div
160206
style={{
161207
...GRID_CONTAINER_STYLE,
208+
...gridStyle,
162209
transform: `scale(${scaleFactor})`,
163210
transformOrigin: 'top left',
164211
}}

src/TecanDeckView/labwareMetadata.ts

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GridCell, GridPosition, LabwareKey } from './types';
1+
import { GridCell, GridPosition, LabwareKey, TecanLabwares } from './types';
22

33
// Column constants
44
export const COLUMN_LEFT_SPANNING = 0; // Left column that spans all three rows (mmPlate)
@@ -23,73 +23,73 @@ export const LABWARE_METADATA: Record<LabwareKey, LabwareMetadata> = {
2323
label: 'MM Plate',
2424
shortLabel: 'MM',
2525
color: LABWARE_COLOR_MM_PLATE,
26-
gridPosition: { row: 0, column: 0, alignment: 'center' },
26+
gridPosition: { row: 0, column: 0 },
2727
},
2828
aPlate: {
2929
label: 'A Plate (200µl #1)',
3030
shortLabel: 'A',
3131
color: LABWARE_COLOR_STANDARD_PLATE,
32-
gridPosition: { row: 0, column: 1, alignment: 'start' },
32+
gridPosition: { row: 0, column: 1 },
3333
},
3434
nemoDilution: {
3535
label: 'NeMo Dilution',
3636
shortLabel: 'NeMoDilution',
3737
color: LABWARE_COLOR_STANDARD_PLATE,
38-
gridPosition: { row: 0, column: 2, alignment: 'start' },
38+
gridPosition: { row: 0, column: 2 },
3939
},
4040
destPcr: {
4141
label: 'Dest PCR',
4242
shortLabel: 'DestPCR',
4343
color: LABWARE_COLOR_STANDARD_PLATE,
44-
gridPosition: { row: 0, column: 3, alignment: 'start' },
44+
gridPosition: { row: 0, column: 3 },
4545
},
4646
destPcr1: {
4747
label: 'Dest PCR 1',
4848
shortLabel: 'DestPCR1',
4949
color: LABWARE_COLOR_STANDARD_PLATE,
50-
gridPosition: { row: 0, column: COLUMN_RIGHT_SPANNING, alignment: 'start' },
50+
gridPosition: { row: 0, column: COLUMN_RIGHT_SPANNING },
5151
},
5252
destPcr2: {
5353
label: 'Dest PCR 2',
5454
shortLabel: 'DestPCR2',
5555
color: LABWARE_COLOR_STANDARD_PLATE,
56-
gridPosition: { row: 0, column: COLUMN_RIGHT_SPANNING, alignment: 'start' },
56+
gridPosition: { row: 0, column: COLUMN_RIGHT_SPANNING },
5757
},
5858
bPlate: {
5959
label: 'B Plate (200µl #2)',
6060
shortLabel: 'B',
6161
color: LABWARE_COLOR_STANDARD_PLATE,
62-
gridPosition: { row: 1, column: 1, alignment: 'center' },
62+
gridPosition: { row: 1, column: 1 },
6363
},
6464
nemoDestPcr2: {
6565
label: 'NeMo Dest PCR 2',
6666
shortLabel: 'DestPCR2',
6767
color: LABWARE_COLOR_STANDARD_PLATE,
68-
gridPosition: { row: 1, column: 2, alignment: 'center' },
68+
gridPosition: { row: 1, column: 2 },
6969
},
7070
destLc: {
7171
label: 'Dest LC',
7272
shortLabel: 'DestLC',
7373
color: LABWARE_COLOR_STANDARD_PLATE,
74-
gridPosition: { row: 1, column: 3, alignment: 'center' },
74+
gridPosition: { row: 1, column: 3 },
7575
},
7676
nemoWater: {
7777
label: 'NeMo Water',
7878
shortLabel: 'NeMoWasser',
7979
color: LABWARE_COLOR_STANDARD_PLATE,
80-
gridPosition: { row: 2, column: 1, alignment: 'end' },
80+
gridPosition: { row: 2, column: 1 },
8181
},
8282
nemoDestTaqMan: {
8383
label: 'Dest TaqMan',
8484
shortLabel: 'Dest-TaqMan',
8585
color: LABWARE_COLOR_TAQMAN_DARK,
86-
gridPosition: { row: 2, column: 2, alignment: 'end' },
86+
gridPosition: { row: 2, column: 2 },
8787
},
8888
fluidX: {
8989
label: 'FluidX',
9090
shortLabel: 'FluidX',
9191
color: LABWARE_COLOR_FLUIDX,
92-
gridPosition: { row: 2, column: 3, alignment: 'end' },
92+
gridPosition: { row: 2, column: 3 },
9393
},
9494
};
9595

@@ -112,3 +112,47 @@ export function isLeftColumn(key: LabwareKey) {
112112
export function isRightColumn(key: LabwareKey) {
113113
return LABWARE_METADATA[key].gridPosition.column === COLUMN_RIGHT_SPANNING;
114114
}
115+
116+
/**
117+
* Returns a map of column numbers to arrays of filled labware keys.
118+
* Used to determine which columns have content for dynamic grid sizing.
119+
*/
120+
export function getFilledLabwaresByColumn(
121+
labwares: TecanLabwares,
122+
): Record<number, Array<LabwareKey>> {
123+
const filledByColumn: Record<number, Array<LabwareKey>> = {};
124+
125+
(Object.keys(LABWARE_METADATA) as Array<LabwareKey>).forEach((key) => {
126+
if (labwares[key]?.content) {
127+
const { column } = LABWARE_METADATA[key].gridPosition;
128+
if (!filledByColumn[column]) {
129+
filledByColumn[column] = [];
130+
}
131+
filledByColumn[column]!.push(key);
132+
}
133+
});
134+
135+
return filledByColumn;
136+
}
137+
138+
/**
139+
* Returns a map of row numbers to arrays of filled labware keys.
140+
* Used to determine which rows have content for dynamic grid sizing.
141+
*/
142+
export function getFilledLabwaresByRow(
143+
labwares: TecanLabwares,
144+
): Record<number, Array<LabwareKey>> {
145+
const filledByRow: Record<number, Array<LabwareKey>> = {};
146+
147+
(Object.keys(LABWARE_METADATA) as Array<LabwareKey>).forEach((key) => {
148+
if (labwares[key]?.content) {
149+
const { row } = LABWARE_METADATA[key].gridPosition;
150+
if (!filledByRow[row]) {
151+
filledByRow[row] = [];
152+
}
153+
filledByRow[row]!.push(key);
154+
}
155+
});
156+
157+
return filledByRow;
158+
}

src/TecanDeckView/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export type LabwareConfig = {
3232
export type GridPosition = {
3333
row: 0 | 1 | 2;
3434
column: 0 | 1 | 2 | 3 | 4;
35-
alignment: 'start' | 'center' | 'end';
3635
};
3736

3837
export type GridCell = LabwareKey | 'leftColumn' | 'rightColumn';

0 commit comments

Comments
 (0)