From 88ba8a3f9b0ba2e2fa3935bc8e3852100abede44 Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Fri, 9 Oct 2026 16:57:41 +0200 Subject: [PATCH 1/6] fix(MasterMix): let the table fill a card narrower than its maximum width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Claude Code --- src/MasterMix/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MasterMix/index.tsx b/src/MasterMix/index.tsx index e7391be3..90132127 100644 --- a/src/MasterMix/index.tsx +++ b/src/MasterMix/index.tsx @@ -25,6 +25,7 @@ export { * `max-width` on `.mll-ant-table-wrapper`. */ const MixContent = styled.div` + width: 100%; max-width: 400px; `; From 280aed6d9935310f7d667fdbe40978fb7455e2a9 Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Mon, 12 Oct 2026 09:10:39 +0200 Subject: [PATCH 2/6] feat(MasterMix): add bodyPadding, headerSpacing and indentStepInPixels to compact the card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leaving them out keeps the current look. 🤖 Generated with Claude Code --- src/MasterMix/MasterMix.test.tsx | 53 ++++++++++++++++++++++++++++++ src/MasterMix/MasterMixRowName.tsx | 13 +++++--- src/MasterMix/MixTable.tsx | 22 ++++++++++--- src/MasterMix/index.stories.tsx | 17 ++++++++++ src/MasterMix/index.tsx | 33 ++++++++++++++++--- src/MasterMix/types.ts | 10 +++++- src/MasterMix/volumeColumns.tsx | 17 +++++++--- 7 files changed, 146 insertions(+), 19 deletions(-) diff --git a/src/MasterMix/MasterMix.test.tsx b/src/MasterMix/MasterMix.test.tsx index c86c71bf..5c52f76b 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'; @@ -158,4 +160,55 @@ describe('MasterMix', () => { 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('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..9ad7112e 100644 --- a/src/MasterMix/MasterMixRowName.tsx +++ b/src/MasterMix/MasterMixRowName.tsx @@ -2,7 +2,7 @@ import React, { ReactNode } from 'react'; import styled from 'styled-components'; const PIPETTED_MARK = '✓'; -const INDENT_STEP_IN_PIXELS = 20; +const DEFAULT_INDENT_STEP_IN_PIXELS = 20; const MARK_GAP_IN_PIXELS = 5; /** @@ -10,11 +10,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 +22,20 @@ const Indent = styled.span<{ $level: number }>` export function MasterMixRowName({ level, pipetted, + indentStepInPixels = DEFAULT_INDENT_STEP_IN_PIXELS, children, }: { level: number; pipetted: boolean; + indentStepInPixels?: number; 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 90132127..069475ed 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, @@ -29,6 +30,24 @@ const MixContent = styled.div` max-width: 400px; `; +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: ${props.$headerSpacing}; + } + .mll-ant-card-head-title .mll-ant-typography { + margin-bottom: 0; + } + `} +`; + /** * Shows what to pipette for a given number of reactions, or as a plain recipe in `mode="recipe"`. * Ingredients added per reaction turn the master mix into a part of the reaction mix. @@ -39,12 +58,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}{' '} @@ -59,8 +83,9 @@ export function MasterMix({ ingredients={ingredients} perReactionIngredients={perReactionIngredients} scaling={scaling} + indentStepInPixels={indentStepInPixels} /> - + ); } diff --git a/src/MasterMix/types.ts b/src/MasterMix/types.ts index 1a909a87..4c633a29 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,17 @@ export type RecipeMode = { pipettingLoss?: never; }; +export type MasterMixStyle = { + bodyPadding?: CSSProperties['padding']; + /** Vertical padding of the title row. */ + headerSpacing?: CSSProperties['padding']; + 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..77d9c732 100644 --- a/src/MasterMix/volumeColumns.tsx +++ b/src/MasterMix/volumeColumns.tsx @@ -18,11 +18,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: number | undefined; +}): Array { return [ { title: 'Name', @@ -30,6 +36,7 @@ export function volumeColumns( {record.title} From 58e49bd092ca19668f128ef3523ac8b146ece2c8 Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Mon, 12 Oct 2026 09:10:39 +0200 Subject: [PATCH 3/6] Revert "fix(MasterMix): let the table fill a card narrower than its maximum width" This reverts commit 88ba8a3f9b0ba2e2fa3935bc8e3852100abede44. --- src/MasterMix/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/MasterMix/index.tsx b/src/MasterMix/index.tsx index 069475ed..bac71fde 100644 --- a/src/MasterMix/index.tsx +++ b/src/MasterMix/index.tsx @@ -26,7 +26,6 @@ export { * `max-width` on `.mll-ant-table-wrapper`. */ const MixContent = styled.div` - width: 100%; max-width: 400px; `; From ba14cc1f9b7cef2c6f52cd2d9ec607c008bf1c4f Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Mon, 12 Oct 2026 10:26:19 +0200 Subject: [PATCH 4/6] fix(MasterMix): give a numeric headerSpacing a pixel unit and type it as paddingBlock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Claude Code --- src/MasterMix/MasterMix.test.tsx | 17 +++++++++++++++++ src/MasterMix/MasterMixRowName.tsx | 4 +++- src/MasterMix/index.tsx | 4 +++- src/MasterMix/types.ts | 3 +-- src/MasterMix/volumeColumns.tsx | 3 ++- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/MasterMix/MasterMix.test.tsx b/src/MasterMix/MasterMix.test.tsx index 5c52f76b..289daec9 100644 --- a/src/MasterMix/MasterMix.test.tsx +++ b/src/MasterMix/MasterMix.test.tsx @@ -188,6 +188,23 @@ describe('MasterMix', () => { }); }); + 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( diff --git a/src/MasterMix/MasterMixRowName.tsx b/src/MasterMix/MasterMixRowName.tsx index 9ad7112e..4f6d16e8 100644 --- a/src/MasterMix/MasterMixRowName.tsx +++ b/src/MasterMix/MasterMixRowName.tsx @@ -1,6 +1,8 @@ import React, { ReactNode } from 'react'; import styled from 'styled-components'; +import { MasterMixStyle } from './types'; + const PIPETTED_MARK = '✓'; const DEFAULT_INDENT_STEP_IN_PIXELS = 20; const MARK_GAP_IN_PIXELS = 5; @@ -27,7 +29,7 @@ export function MasterMixRowName({ }: { level: number; pipetted: boolean; - indentStepInPixels?: number; + indentStepInPixels?: MasterMixStyle['indentStepInPixels']; children: ReactNode; }) { return ( diff --git a/src/MasterMix/index.tsx b/src/MasterMix/index.tsx index bac71fde..db4d2d40 100644 --- a/src/MasterMix/index.tsx +++ b/src/MasterMix/index.tsx @@ -39,7 +39,9 @@ const MixCard = styled(Card)<{ min-height: 0; } .mll-ant-card-head-title { - padding-block: ${props.$headerSpacing}; + padding-block: ${typeof props.$headerSpacing === 'number' + ? `${props.$headerSpacing}px` + : props.$headerSpacing}; } .mll-ant-card-head-title .mll-ant-typography { margin-bottom: 0; diff --git a/src/MasterMix/types.ts b/src/MasterMix/types.ts index 4c633a29..fdcc76ba 100644 --- a/src/MasterMix/types.ts +++ b/src/MasterMix/types.ts @@ -33,8 +33,7 @@ export type RecipeMode = { export type MasterMixStyle = { bodyPadding?: CSSProperties['padding']; - /** Vertical padding of the title row. */ - headerSpacing?: CSSProperties['padding']; + headerSpacing?: CSSProperties['paddingBlock']; indentStepInPixels?: number; }; diff --git a/src/MasterMix/volumeColumns.tsx b/src/MasterMix/volumeColumns.tsx index 77d9c732..354d749c 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, @@ -27,7 +28,7 @@ export function volumeColumns({ scaling: PipettingScaling | undefined; pipettedKeys: Array; withinReactionMix: boolean; - indentStepInPixels: number | undefined; + indentStepInPixels: MasterMixStyle['indentStepInPixels']; }): Array { return [ { From db2a9653abf1280f4fa2d61b229313a6f49f219e Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Mon, 12 Oct 2026 14:55:37 +0200 Subject: [PATCH 5/6] fix(MasterMix): keep each volume on one line with its unit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Claude Code --- src/MasterMix/MasterMix.test.tsx | 20 ++++++++++++++++++++ src/MasterMix/pipettingLossTableColumn.tsx | 2 +- src/MasterMix/volumeColumns.tsx | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/MasterMix/MasterMix.test.tsx b/src/MasterMix/MasterMix.test.tsx index 289daec9..3856b064 100644 --- a/src/MasterMix/MasterMix.test.tsx +++ b/src/MasterMix/MasterMix.test.tsx @@ -49,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( –; case 'masterMixIngredient': case 'masterMixTotal': - return <>{totalVolume(record, args)} µl; + return <>{totalVolume(record, args)} Âµl; } }, }; diff --git a/src/MasterMix/volumeColumns.tsx b/src/MasterMix/volumeColumns.tsx index 354d749c..6fd89022 100644 --- a/src/MasterMix/volumeColumns.tsx +++ b/src/MasterMix/volumeColumns.tsx @@ -53,7 +53,7 @@ export function volumeColumns({ : undefined, }), render: (_: unknown, record: MasterMixTableRow) => ( - <>{record.volume.toFixed(1)} µl + <>{record.volume.toFixed(1)} Âµl ), }, ...(scaling ? [pipettingLossTableColumn(scaling)] : []), From 7a3ecb2eed56961b2cddca490277e43aa2f92c9a Mon Sep 17 00:00:00 2001 From: Simon Bigelmayr Date: Mon, 12 Oct 2026 14:59:11 +0200 Subject: [PATCH 6/6] fix(MasterMix): widen the table to 480px so long names and the loss header stay on one line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Claude Code --- src/MasterMix/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MasterMix/index.tsx b/src/MasterMix/index.tsx index db4d2d40..a4e80a6b 100644 --- a/src/MasterMix/index.tsx +++ b/src/MasterMix/index.tsx @@ -26,7 +26,7 @@ export { * `max-width` on `.mll-ant-table-wrapper`. */ const MixContent = styled.div` - max-width: 400px; + max-width: 480px; `; const MixCard = styled(Card)<{