@@ -4,8 +4,18 @@ import { PALETTE } from '../theme';
44
55import { EmptyLabware } from './EmptyLabware' ;
66import { LabwareDetailItem } from './LabwareDetailItem' ;
7- import { LABWARE_METADATA } from './labwareMetadata' ;
8- import { LabwareConfig , LabwareKey , TecanLabwares } from './types' ;
7+ import {
8+ GRID_LAYOUT ,
9+ isLeftColumn ,
10+ isRightColumn ,
11+ LABWARE_METADATA ,
12+ } from './labwareMetadata' ;
13+ import {
14+ LabwareConfig ,
15+ LabwareKey ,
16+ LabwareWithStyle ,
17+ TecanLabwares ,
18+ } from './types' ;
919
1020// Responsive scaling constants
1121const GRID_PADDING = 8 ; // Padding around the grid container (from style.padding)
@@ -15,21 +25,22 @@ const GRID_OVERHEAD = GRID_PADDING * 2 + GRID_GAP * (COLUMN_COUNT - 1); // Total
1525const COLUMN_OVERHEAD = 60 ; // Estimated width per column for borders and padding
1626const BASE_CONTENT_WIDTH = 1400 ; // Designed content width at 1.0 scale factor
1727const SCALE_SAFETY_MARGIN = 0.95 ; // 5% safety margin to prevent overflow
28+ const MIN_SCALE_FACTOR = 0.1 ; // Minimum scale to keep content readable
29+ const MAX_SCALE_FACTOR = 1.0 ; // Maximum scale (100% of designed size)
30+ const INITIAL_SCALE_FACTOR = 0.7 ; // Initial scale before container width is measured
1831
1932// Static styles
2033const CONTAINER_STYLE = {
2134 width : '100%' ,
2235} as const ;
2336
24- const GRID_CONTAINER_BASE_STYLE = {
37+ const GRID_CONTAINER_STYLE = {
2538 display : 'grid' ,
26- gridTemplateColumns : 'auto auto auto auto auto' ,
2739 gap : '8px' ,
28- gridTemplateAreas : `
29- "mmPlate aPlate nemoDilution destPcr rightColumn"
30- "mmPlate bPlate nemoDestPcr2 destLc rightColumn"
31- "mmPlate nemoWater nemoDestTaqMan fluidX rightColumn"
32- ` ,
40+ gridTemplateAreas : GRID_LAYOUT . map ( ( row ) => `"${ row . join ( ' ' ) } "` ) . join (
41+ '\n ' ,
42+ ) ,
43+ gridTemplateRows : 'auto auto auto' ,
3344 padding : '8px' ,
3445 backgroundColor : PALETTE . gray1 ,
3546 borderRadius : '4px' ,
@@ -40,18 +51,27 @@ const LABWARE_ITEM_BASE_STYLE = {
4051 justifySelf : 'center' ,
4152} as const ;
4253
43- const RIGHT_COLUMN_STYLE = {
44- gridArea : 'rightColumn' ,
54+ // Shared style for columns that span all 3 grid rows
55+ const SPANNING_COLUMN_BASE_STYLE = {
4556 display : 'grid' ,
46- gridTemplateRows : 'auto auto' ,
4757 alignSelf : 'center' ,
4858 gap : '8px' ,
4959} as const ;
5060
61+ const LEFT_COLUMN_STYLE = {
62+ ...SPANNING_COLUMN_BASE_STYLE ,
63+ gridArea : 'leftColumn' ,
64+ } as const ;
65+
66+ const RIGHT_COLUMN_STYLE = {
67+ ...SPANNING_COLUMN_BASE_STYLE ,
68+ gridArea : 'rightColumn' ,
69+ } as const ;
70+
5171export function TecanDeckView ( { labwares } : { labwares : TecanLabwares } ) {
5272 const containerRef = React . useRef < HTMLDivElement > ( null ) ;
5373 const [ availableWidth , setAvailableWidth ] = React . useState < number > ( ) ;
54- const [ scaleFactor , setScaleFactor ] = React . useState ( 0.7 ) ;
74+ const [ scaleFactor , setScaleFactor ] = React . useState ( INITIAL_SCALE_FACTOR ) ;
5575
5676 React . useEffect ( ( ) => {
5777 const container = containerRef . current ;
@@ -84,49 +104,48 @@ export function TecanDeckView({ labwares }: { labwares: TecanLabwares }) {
84104 SCALE_SAFETY_MARGIN ;
85105
86106 const calculatedScale = Math . max (
87- 0.1 ,
88- Math . min ( 1.0 , availableContentWidth / BASE_CONTENT_WIDTH ) ,
107+ MIN_SCALE_FACTOR ,
108+ Math . min ( MAX_SCALE_FACTOR , availableContentWidth / BASE_CONTENT_WIDTH ) ,
89109 ) ;
90110 setScaleFactor ( calculatedScale ) ;
91111 } , [ availableWidth ] ) ;
92112
93- const allLabwares : Array < LabwareConfig > = React . useMemo ( ( ) => {
94- const allLabwareKeys = Object . keys ( LABWARE_METADATA ) as Array < LabwareKey > ;
95- return allLabwareKeys . map ( ( key ) => ( {
96- key,
97- ...LABWARE_METADATA [ key ] ,
98- content : labwares [ key ] ?. content ,
99- } ) ) ;
100- } , [ labwares ] ) ;
101-
102- const isRightColumnAndNeedContainer = ( key : LabwareKey ) =>
103- key === 'destPcr1' || key === 'destPcr2' ;
104-
105- const ROWS = [
106- {
107- keys : new Set < LabwareKey > ( [
108- 'aPlate' ,
109- 'nemoDilution' ,
110- 'destPcr' ,
111- 'destPcr1' ,
112- 'destPcr2' ,
113- ] ) ,
114- alignment : 'start' as const ,
115- } ,
116- {
117- keys : new Set < LabwareKey > ( [ 'bPlate' , 'nemoDestPcr2' , 'destLc' ] ) ,
118- alignment : 'center' as const ,
119- } ,
120- {
121- keys : new Set < LabwareKey > ( [ 'nemoWater' , 'nemoDestTaqMan' , 'fluidX' ] ) ,
122- alignment : 'end' as const ,
123- } ,
124- ] ;
125-
126- const getVerticalAlignment = ( key : LabwareKey ) : string => {
127- const row = ROWS . find ( ( r ) => r . keys . has ( key ) ) ;
128- return row ?. alignment ?? 'center' ;
129- } ;
113+ // Group labwares into spanning columns and main grid in a single pass
114+ const { leftColumnLabwares, rightColumnLabwares, mainGridLabwares } =
115+ React . useMemo ( ( ) => {
116+ const allLabwareKeys = Object . keys ( LABWARE_METADATA ) as Array < LabwareKey > ;
117+
118+ return allLabwareKeys . reduce (
119+ ( acc , key ) => {
120+ const metadata = LABWARE_METADATA [ key ] ;
121+ const labware : LabwareWithStyle = {
122+ key,
123+ ...metadata ,
124+ content : labwares [ key ] ?. content ,
125+ style : {
126+ ...LABWARE_ITEM_BASE_STYLE ,
127+ gridArea : key ,
128+ alignSelf : metadata . gridPosition . alignment ,
129+ } ,
130+ } ;
131+
132+ if ( isLeftColumn ( key ) ) {
133+ acc . leftColumnLabwares . push ( labware ) ;
134+ } else if ( isRightColumn ( key ) ) {
135+ acc . rightColumnLabwares . push ( labware ) ;
136+ } else {
137+ acc . mainGridLabwares . push ( labware ) ;
138+ }
139+
140+ return acc ;
141+ } ,
142+ {
143+ leftColumnLabwares : [ ] as Array < LabwareWithStyle > ,
144+ rightColumnLabwares : [ ] as Array < LabwareWithStyle > ,
145+ mainGridLabwares : [ ] as Array < LabwareWithStyle > ,
146+ } ,
147+ ) ;
148+ } , [ labwares ] ) ;
130149
131150 const renderLabware = ( labware : LabwareConfig ) =>
132151 labware . content ? (
@@ -140,46 +159,25 @@ export function TecanDeckView({ labwares }: { labwares: TecanLabwares }) {
140159 < EmptyLabware shortLabel = { labware . shortLabel } />
141160 ) ;
142161
143- // Check if a row contains only empty labware
144- const isRowEmpty = ( rowKeys : Set < LabwareKey > ) : boolean =>
145- Array . from ( rowKeys ) . every ( ( key ) => ! labwares [ key ] ?. content ) ;
146-
147- const EMPTY_ROW_SIZE = '0.3fr' ;
148- const CONTENT_ROW_SIZE = '1fr' ;
149-
150- const gridTemplateRows = ROWS . map ( ( row ) =>
151- isRowEmpty ( row . keys ) ? EMPTY_ROW_SIZE : CONTENT_ROW_SIZE ,
152- ) . join ( ' ' ) ;
153-
154162 return (
155163 < div ref = { containerRef } style = { CONTAINER_STYLE } >
156- < div
157- style = { {
158- ...GRID_CONTAINER_BASE_STYLE ,
159- gridTemplateRows,
160- } }
161- >
162- { allLabwares
163- . filter ( ( labware ) => ! isRightColumnAndNeedContainer ( labware . key ) )
164- . map ( ( labware ) => (
165- < div
166- key = { labware . key }
167- style = { {
168- ...LABWARE_ITEM_BASE_STYLE ,
169- gridArea : labware . key ,
170- alignSelf : getVerticalAlignment ( labware . key ) ,
171- } }
172- >
173- { renderLabware ( labware ) }
174- </ div >
164+ < div style = { GRID_CONTAINER_STYLE } >
165+ < div style = { LEFT_COLUMN_STYLE } >
166+ { leftColumnLabwares . map ( ( labware ) => (
167+ < div key = { labware . key } > { renderLabware ( labware ) } </ div >
175168 ) ) }
169+ </ div >
170+
171+ { mainGridLabwares . map ( ( labware ) => (
172+ < div key = { labware . key } style = { labware . style } >
173+ { renderLabware ( labware ) }
174+ </ div >
175+ ) ) }
176176
177177 < div style = { RIGHT_COLUMN_STYLE } >
178- { allLabwares
179- . filter ( ( labware ) => isRightColumnAndNeedContainer ( labware . key ) )
180- . map ( ( labware ) => (
181- < div key = { labware . key } > { renderLabware ( labware ) } </ div >
182- ) ) }
178+ { rightColumnLabwares . map ( ( labware ) => (
179+ < div key = { labware . key } > { renderLabware ( labware ) } </ div >
180+ ) ) }
183181 </ div >
184182 </ div >
185183 </ div >
0 commit comments