diff --git a/src/MasterMix/MasterMix.test.tsx b/src/MasterMix/MasterMix.test.tsx
index c86c71bf..3856b064 100644
--- a/src/MasterMix/MasterMix.test.tsx
+++ b/src/MasterMix/MasterMix.test.tsx
@@ -1,6 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
+import { Provider } from '../Provider';
+
import { MasterMixIngredient } from './types';
import { MasterMix } from './index';
@@ -47,6 +49,26 @@ describe('MasterMix', () => {
expect(screen.getByText('901.8 µl')).toBeInTheDocument();
});
+ it('keeps each volume together with its unit', () => {
+ const keepWhitespace = (text: string) => text;
+
+ render(
+ ,
+ );
+
+ expect(
+ screen.getByText('79.5\u00A0µl', { normalizer: keepWhitespace }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('901.8\u00A0µl', { normalizer: keepWhitespace }),
+ ).toBeInTheDocument();
+ });
+
it('renders as reaction mix containing the master mix', () => {
render(
{
expect(screen.queryAllByTitle('pipettiert')).toHaveLength(0);
});
+
+ /* eslint-disable testing-library/no-container, testing-library/no-node-access -- antd wrappers have no role */
+ it('applies the given style', () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.querySelector('.mll-ant-card-body')).toHaveStyle({
+ padding: '12px',
+ });
+ expect(container.querySelector('.mll-ant-card-head-title')).toHaveStyle({
+ paddingBlock: '4px',
+ });
+ expect(screen.getByText('Water').firstElementChild).toHaveStyle({
+ width: '24px',
+ });
+ });
+
+ it('adds pixels to a numeric header spacing', () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.querySelector('.mll-ant-card-head-title')).toHaveStyle({
+ paddingBlock: '4px',
+ });
+ });
+
+ it('keeps the default style without style props', () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ expect(container.querySelector('.mll-ant-card-body')).not.toHaveAttribute(
+ 'style',
+ );
+ expect(container.querySelector('.mll-ant-card-head-title')).not.toHaveStyle(
+ { paddingBlock: '4px' },
+ );
+ expect(screen.getByText('Water').firstElementChild).toHaveStyle({
+ width: '40px',
+ });
+ });
+ /* eslint-enable testing-library/no-container, testing-library/no-node-access */
});
diff --git a/src/MasterMix/MasterMixRowName.tsx b/src/MasterMix/MasterMixRowName.tsx
index abb79887..4f6d16e8 100644
--- a/src/MasterMix/MasterMixRowName.tsx
+++ b/src/MasterMix/MasterMixRowName.tsx
@@ -1,8 +1,10 @@
import React, { ReactNode } from 'react';
import styled from 'styled-components';
+import { MasterMixStyle } from './types';
+
const PIPETTED_MARK = '✓';
-const INDENT_STEP_IN_PIXELS = 20;
+const DEFAULT_INDENT_STEP_IN_PIXELS = 20;
const MARK_GAP_IN_PIXELS = 5;
/**
@@ -10,11 +12,11 @@ const MARK_GAP_IN_PIXELS = 5;
* nor detaches from its row. The offset separates the mark from the name without moving
* the name on any level.
*/
-const Indent = styled.span<{ $level: number }>`
+const Indent = styled.span<{ $widthInPixels: number }>`
display: inline-block;
position: relative;
right: ${MARK_GAP_IN_PIXELS}px;
- width: ${(props) => props.$level * INDENT_STEP_IN_PIXELS}px;
+ width: ${(props) => props.$widthInPixels}px;
text-align: right;
color: ${(props) => props.theme.successColor};
`;
@@ -22,15 +24,20 @@ const Indent = styled.span<{ $level: number }>`
export function MasterMixRowName({
level,
pipetted,
+ indentStepInPixels = DEFAULT_INDENT_STEP_IN_PIXELS,
children,
}: {
level: number;
pipetted: boolean;
+ indentStepInPixels?: MasterMixStyle['indentStepInPixels'];
children: ReactNode;
}) {
return (
<>
-
+
{pipetted ? PIPETTED_MARK : null}
{children}
diff --git a/src/MasterMix/MixTable.tsx b/src/MasterMix/MixTable.tsx
index fb0e7225..aef4132e 100644
--- a/src/MasterMix/MixTable.tsx
+++ b/src/MasterMix/MixTable.tsx
@@ -8,12 +8,18 @@ import {
} from './VolumeTable';
import { hasPerReactionIngredients } from './hasPerReactionIngredients';
import { mixRows } from './mixRows';
-import { MasterMixTableRow, PipettingScaling, ReactionMix } from './types';
+import {
+ MasterMixStyle,
+ MasterMixTableRow,
+ PipettingScaling,
+ ReactionMix,
+} from './types';
import { volumeColumns } from './volumeColumns';
-type MixTableProps = ReactionMix & {
- scaling?: PipettingScaling;
-};
+type MixTableProps = ReactionMix &
+ Pick & {
+ scaling?: PipettingScaling;
+ };
function rowClassName(
record: MasterMixTableRow,
@@ -35,6 +41,7 @@ export function MixTable({
ingredients,
perReactionIngredients,
scaling,
+ indentStepInPixels,
}: MixTableProps) {
const [pipettedKeys, setPipettedKeys] = useState>([]);
@@ -60,7 +67,12 @@ export function MixTable({
}
: {})
}
- columns={volumeColumns(scaling, pipettedKeys, withinReactionMix)}
+ columns={volumeColumns({
+ scaling,
+ pipettedKeys,
+ withinReactionMix,
+ indentStepInPixels,
+ })}
/>
);
}
diff --git a/src/MasterMix/index.stories.tsx b/src/MasterMix/index.stories.tsx
index 00e6645c..894fcf66 100644
--- a/src/MasterMix/index.stories.tsx
+++ b/src/MasterMix/index.stories.tsx
@@ -99,3 +99,20 @@ export function Recipe({ name, ingredients }: StoryProps): ReactElement {
/>
);
}
+
+export function NarrowCard(props: StoryProps): ReactElement {
+ return (
+
+
+
+ );
+}
diff --git a/src/MasterMix/index.tsx b/src/MasterMix/index.tsx
index e7391be3..a4e80a6b 100644
--- a/src/MasterMix/index.tsx
+++ b/src/MasterMix/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
import { Card } from '../Card';
import { Typography } from '../Typography';
@@ -7,11 +7,12 @@ import { Typography } from '../Typography';
import { MixTable } from './MixTable';
import { hasPerReactionIngredients } from './hasPerReactionIngredients';
import { MASTER_MIX_LABEL } from './mixRows';
-import { MasterMixProps } from './types';
+import { MasterMixProps, MasterMixStyle } from './types';
export { reactionVolume } from './reactionVolume';
export {
MasterMixProps,
+ MasterMixStyle,
MasterMixIngredient,
ReactionMix,
PipettingLoss,
@@ -25,7 +26,27 @@ export {
* `max-width` on `.mll-ant-table-wrapper`.
*/
const MixContent = styled.div`
- max-width: 400px;
+ max-width: 480px;
+`;
+
+const MixCard = styled(Card)<{
+ $headerSpacing: MasterMixStyle['headerSpacing'];
+}>`
+ ${(props) =>
+ props.$headerSpacing != null &&
+ css`
+ .mll-ant-card-head {
+ min-height: 0;
+ }
+ .mll-ant-card-head-title {
+ padding-block: ${typeof props.$headerSpacing === 'number'
+ ? `${props.$headerSpacing}px`
+ : props.$headerSpacing};
+ }
+ .mll-ant-card-head-title .mll-ant-typography {
+ margin-bottom: 0;
+ }
+ `}
`;
/**
@@ -38,12 +59,17 @@ export function MasterMix({
pipettingLoss,
ingredients,
perReactionIngredients,
+ bodyPadding,
+ headerSpacing,
+ indentStepInPixels,
}: MasterMixProps) {
/** Keyed on the scaling data itself, so a caller without types degrades to the recipe. */
const scaling = pipettingLoss != null ? { count, pipettingLoss } : undefined;
return (
-
{name}{' '}
@@ -58,8 +84,9 @@ export function MasterMix({
ingredients={ingredients}
perReactionIngredients={perReactionIngredients}
scaling={scaling}
+ indentStepInPixels={indentStepInPixels}
/>
-
+
);
}
diff --git a/src/MasterMix/pipettingLossTableColumn.tsx b/src/MasterMix/pipettingLossTableColumn.tsx
index a634a6c2..55bc45c5 100644
--- a/src/MasterMix/pipettingLossTableColumn.tsx
+++ b/src/MasterMix/pipettingLossTableColumn.tsx
@@ -96,7 +96,7 @@ export function pipettingLossTableColumn(
return <>–>;
case 'masterMixIngredient':
case 'masterMixTotal':
- return <>{totalVolume(record, args)} µl>;
+ return <>{totalVolume(record, args)} µl>;
}
},
};
diff --git a/src/MasterMix/types.ts b/src/MasterMix/types.ts
index 1a909a87..fdcc76ba 100644
--- a/src/MasterMix/types.ts
+++ b/src/MasterMix/types.ts
@@ -1,6 +1,6 @@
import { Modify } from '@mll-lab/js-utils';
import { ColumnType } from 'antd/es/table';
-import { ReactNode } from 'react';
+import { CSSProperties, ReactNode } from 'react';
export type MasterMixIngredient = {
key: number;
@@ -31,9 +31,16 @@ export type RecipeMode = {
pipettingLoss?: never;
};
+export type MasterMixStyle = {
+ bodyPadding?: CSSProperties['padding'];
+ headerSpacing?: CSSProperties['paddingBlock'];
+ indentStepInPixels?: number;
+};
+
export type MasterMixProps = {
name: string;
} & ReactionMix &
+ MasterMixStyle &
(PipettingScaling | RecipeMode);
export type PipettingLossAbsolute = { type: 'absolute'; count: number };
diff --git a/src/MasterMix/volumeColumns.tsx b/src/MasterMix/volumeColumns.tsx
index 31f49218..6fd89022 100644
--- a/src/MasterMix/volumeColumns.tsx
+++ b/src/MasterMix/volumeColumns.tsx
@@ -5,6 +5,7 @@ import { REFERENCE_VOLUME_CLASS } from './VolumeTable';
import { indentLevel } from './indentLevel';
import { pipettingLossTableColumn } from './pipettingLossTableColumn';
import {
+ MasterMixStyle,
MasterMixTableRow,
PipettingLossTableColumn,
PipettingScaling,
@@ -18,11 +19,17 @@ function belongsToMasterMix(record: MasterMixTableRow): boolean {
);
}
-export function volumeColumns(
- scaling: PipettingScaling | undefined,
- pipettedKeys: Array,
- withinReactionMix: boolean,
-): Array {
+export function volumeColumns({
+ scaling,
+ pipettedKeys,
+ withinReactionMix,
+ indentStepInPixels,
+}: {
+ scaling: PipettingScaling | undefined;
+ pipettedKeys: Array;
+ withinReactionMix: boolean;
+ indentStepInPixels: MasterMixStyle['indentStepInPixels'];
+}): Array {
return [
{
title: 'Name',
@@ -30,6 +37,7 @@ export function volumeColumns(
{record.title}
@@ -45,7 +53,7 @@ export function volumeColumns(
: undefined,
}),
render: (_: unknown, record: MasterMixTableRow) => (
- <>{record.volume.toFixed(1)} µl>
+ <>{record.volume.toFixed(1)} µl>
),
},
...(scaling ? [pipettingLossTableColumn(scaling)] : []),