Skip to content

Commit 86e0994

Browse files
authored
feat: introduce TecanDeckView component for visualizing labware layout (#315)
1 parent bb5f5a4 commit 86e0994

15 files changed

Lines changed: 888 additions & 7 deletions

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
branches:
55
- master
66
- alpha
7+
- beta
78

89
jobs:
910
# https://github.com/semantic-release/semantic-release/blob/master/docs/recipes/ci-configurations/github-actions.md#githubworkflowsreleaseyml-configuration-for-node-projects

src/Plate/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,15 @@ import { FlowDirection } from './types';
22

33
/** Used internally when rendering the Plate component. */
44
export const PLATE_FLOW = 'row' as const satisfies FlowDirection;
5+
6+
/** Width of the row label column (scales with font size) */
7+
export const ROW_LABEL_WIDTH = '3ch';
8+
9+
/** Well column width in uniform mode (proportional spacing) */
10+
export const WELL_COLUMN_WIDTH_UNIFORM = '4fr';
11+
12+
/** Well column width in compact mode (fits content) */
13+
export const WELL_COLUMN_WIDTH_COMPACT = 'auto';
14+
15+
/** Gap between grid cells */
16+
export const GRID_GAP = '3px';

src/Plate/coordinateSystem6x4.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { CoordinateSystem } from './types';
2+
3+
export const COORDINATE_SYSTEM_6X4 = {
4+
rows: ['A', 'B', 'C', 'D'],
5+
columns: [1, 2, 3, 4, 5, 6],
6+
} as const satisfies CoordinateSystem;
7+
8+
export type CoordinateSystem6x4 = typeof COORDINATE_SYSTEM_6X4;

src/Plate/index.stories.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { StoryFn } from '@storybook/react-webpack5';
22
import React from 'react';
3-
import { action } from 'storybook/test';
3+
import { action } from 'storybook/actions';
44

55
import { PALETTE } from '../theme';
66

@@ -18,6 +18,15 @@ export default {
1818
args: {
1919
isDraggable: false,
2020
loading: false,
21+
wellSizing: 'uniform',
22+
},
23+
argTypes: {
24+
wellSizing: {
25+
control: { type: 'select' },
26+
options: ['uniform', 'compact'],
27+
description:
28+
"Controls well sizing. Row labels always use 3ch. 'uniform' gives equal proportional width (spacious), 'compact' fits content (minimal width).",
29+
},
2130
},
2231
};
2332

src/Plate/index.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import { MllSpinnerIcon } from '../Spinner';
77
import { ColumnLabel } from './ColumnLabel';
88
import { RowLabel } from './RowLabel';
99
import { Well } from './Well';
10-
import { PLATE_FLOW } from './constants';
10+
import {
11+
GRID_GAP,
12+
PLATE_FLOW,
13+
ROW_LABEL_WIDTH,
14+
WELL_COLUMN_WIDTH_COMPACT,
15+
WELL_COLUMN_WIDTH_UNIFORM,
16+
} from './constants';
1117
import { CoordinateSystem, PlateProps } from './types';
1218
import {
1319
allCoordinateSystemPositions,
@@ -20,6 +26,7 @@ import {
2026
export * from './constants';
2127
export * from './coordinateSystem12x8';
2228
export * from './coordinateSystem2x16';
29+
export * from './coordinateSystem6x4';
2330
export * from './types';
2431
export * from './utils';
2532
export { GENERAL_WELL_STYLE } from './wellUtils';
@@ -30,6 +37,7 @@ export function Plate<TCoordinateSystem extends CoordinateSystem>({
3037
dndContextProps,
3138
isDraggable,
3239
loading,
40+
wellSizing = 'uniform',
3341
}: PlateProps<TCoordinateSystem>) {
3442
if (data) {
3543
assertUniquePositions(data);
@@ -50,10 +58,14 @@ export function Plate<TCoordinateSystem extends CoordinateSystem>({
5058
<div
5159
style={{
5260
display: 'grid',
53-
gridTemplateColumns: `1fr${' 4fr'.repeat(
54-
coordinateSystem.columns.length,
55-
)}`,
56-
gridGap: '3px',
61+
gridTemplateColumns: `${ROW_LABEL_WIDTH} repeat(${
62+
coordinateSystem.columns.length
63+
}, ${
64+
wellSizing === 'compact'
65+
? WELL_COLUMN_WIDTH_COMPACT
66+
: WELL_COLUMN_WIDTH_UNIFORM
67+
})`,
68+
gridGap: GRID_GAP,
5769
}}
5870
>
5971
{/* takes up the space in the upper left corner between A and 1 */}

src/Plate/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,11 @@ export type PlateProps<TCoordinateSystem extends CoordinateSystem> = {
2828
isDraggable?: Maybe<boolean>;
2929
/** Do not add props.dndContextProps conditionally, as it leads to problems. Use props.isDraggable instead. */
3030
dndContextProps?: Maybe<Props>;
31+
/**
32+
* Controls the sizing behavior for well columns.
33+
* Row label column always uses ROW_LABEL_WIDTH (scales with font size).
34+
* - 'uniform': Wells have equal proportional width (spacious layout) - default
35+
* - 'compact': Wells fit their content (minimal width)
36+
*/
37+
wellSizing?: 'uniform' | 'compact';
3138
};

src/Table/index.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { StoryFn } from '@storybook/react-webpack5';
22
import React, { useState } from 'react';
3-
import { action } from 'storybook/test';
3+
import { action } from 'storybook/actions';
44

55
import { THEME } from '../theme';
66

src/TecanDeckView/EmptyLabware.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as React from 'react';
2+
3+
import { PALETTE } from '../theme';
4+
5+
export function EmptyLabware({ shortLabel }: { shortLabel: string }) {
6+
return (
7+
<div
8+
style={{
9+
display: 'inline-flex',
10+
alignItems: 'center',
11+
justifyContent: 'center',
12+
border: `1px dashed ${PALETTE.gray3}`,
13+
borderRadius: '2px',
14+
padding: '8px 4px',
15+
backgroundColor: PALETTE.gray1,
16+
opacity: 0.7,
17+
fontSize: '9px',
18+
fontWeight: 500,
19+
color: PALETTE.gray6,
20+
whiteSpace: 'nowrap',
21+
}}
22+
>
23+
{shortLabel}
24+
</div>
25+
);
26+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as React from 'react';
2+
3+
import { PALETTE } from '../theme';
4+
5+
/* eslint-disable-next-line @mll-lab/no-color-literals -- Hardware-specific color must match physical label appearance */
6+
const LABEL_BACKGROUND_COLOR = '#e0e559';
7+
8+
const LABEL_STYLE = {
9+
backgroundColor: LABEL_BACKGROUND_COLOR,
10+
padding: '2px 6px',
11+
fontSize: '11px',
12+
fontWeight: 600,
13+
color: PALETTE.black,
14+
whiteSpace: 'nowrap',
15+
alignSelf: 'flex-start',
16+
} as const;
17+
18+
const CONTAINER_STYLE = {
19+
display: 'inline-flex',
20+
flexDirection: 'column',
21+
alignItems: 'flex-start',
22+
borderRadius: '4px',
23+
padding: '4px',
24+
} as const;
25+
26+
export function LabwareDetailItem({
27+
backgroundColor = PALETTE.white,
28+
content,
29+
shortLabel,
30+
}: {
31+
shortLabel: string;
32+
content: React.ReactElement;
33+
backgroundColor?: string;
34+
}) {
35+
return (
36+
<div
37+
style={{
38+
...CONTAINER_STYLE,
39+
backgroundColor,
40+
border: `1px solid ${PALETTE.gray4}`,
41+
boxShadow: `0 2px 4px ${PALETTE.black}1a`,
42+
}}
43+
>
44+
{content}
45+
<div style={LABEL_STYLE}>{shortLabel}</div>
46+
</div>
47+
);
48+
}

0 commit comments

Comments
 (0)